基于 netmiko 库 网络设备批量脚本下发工具

8 阅读3分钟

这是什么?

这是一个基于 Python 和 Netmiko 开发的网络设备自动化运维工具。它支持 JSON 配置文件多模板继承,能够并发地对多台网络设备执行命令巡检或配置下发,并自动生成详细的任务日志。

项目结构

.
├── configLoader.py      # 配置解析核心逻辑(支持多模板合并)
├── main.py              # 主程序(并发执行与逻辑控制)
├── devices.json         # 设备清单与模板配置文件
├── commands.txt         # 待执行的命令列表
└── logs/                # 运行后自动生成的日志目录

快速开始

1. 安装依赖

pip install netmiko

2. 配置设备信息 (devices.json)

{
    "templates": {
        "h3c_telnet": {
            "device_type": "hp_comware_telnet",
            "username": "",
            "password": "old_password",
            "port": 23
        },
        "new_password": {
            "password": "special_password_2024"
        },
        "h3c_ssh":{
            "device_type": "hp_comware",
            "username": "admin",
            "password": "password",
            "port": 23
        }
    },
    "device_list": [
        {    # 多模板格式
            "host": "172.16.100.11",
            "use_template": ["h3c_telnet", "new_password"]
        },
        {    # 单模板格式
            "host": "172.16.100.12",
            "use_template": "h3c_ssh"
        },
        {    # 完整格式
            'device_type': 'hp_comware_telnet', 
            'password': 'Password@_', 
            'port': 23, 
            'host': '172.16.100.13'
        }
    ]
}

3. 编写命令 (commands.txt)

display version
display ip interface brief

4. 运行程序

python main.py

统计报告示例

------------------------------
统计报告:
  - 总计设备: 12
  - 成功数量: 11
  - 失败数量: 1
------------------------------
[DONE] 批量任务结束, 日志已保存至 logs

完整代码

main.py

import os
import threading  # 引入线程锁
from netmiko import ConnectHandler
from concurrent.futures import ThreadPoolExecutor, as_completed
from configLoader import load_and_merge_config

# 配置常量
DEVICE_CONFIG_FILE = 'devices.json'
COMMAND_FILE = 'commands.txt'
LOG_DIR = 'logs'

# 初始化计数器和锁
success_count = 0
error_count = 0
counter_lock = threading.Lock()

if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

def run_commands_on_device(device):
    global success_count, error_count
    host = device.get('host', 'Unknown')
    log_path = os.path.join(LOG_DIR, f"{host}.log")
    
    try:
        if not os.path.exists(COMMAND_FILE):
            raise FileNotFoundError(f"找不到命令文件: {COMMAND_FILE}")

        with open(COMMAND_FILE, 'r', encoding='utf-8') as f:
            commands = [l.strip() for l in f if l.strip()]

        print(f"[+] 正在连接: {host}...")
        
        with ConnectHandler(**device, global_delay_factor=2) as net_connect:
            # 自动处理分屏显示
            d_type = device.get('device_type', '')
            if any(x in d_type for x in ["hp_comware", "huawei"]):
                net_connect.send_command("screen-length 0 temporary")
            elif any(x in d_type for x in ["cisco_ios", "ruijie"]):
                net_connect.send_command("terminal length 0")

            output = f"--- 执行回显: {host} ---\n"
            for cmd in commands:
                print(f"  [{host}] 执行 -> {cmd}")
                res = net_connect.send_command(cmd, read_timeout=20, expect_string=r'[>\]#]')   
                output += f"\n> {cmd}\n{res}\n"
            
            with open(log_path, 'w', encoding='utf-8') as f_log:
                f_log.write(output)
                
        print(f"[OK] {host} 任务处理完毕。")
        
        # 统计成功
        with counter_lock:
            success_count += 1
        return True

    except Exception as e:
        err_msg = f"[ERROR] {host} 异常: {str(e)}"
        print(err_msg)
        with open(log_path, 'w', encoding='utf-8') as f_log:
            f_log.write(err_msg)
        
        # 统计失败
        with counter_lock:
            error_count += 1
        return False

def main():
    devices = load_and_merge_config(DEVICE_CONFIG_FILE)
    
    if not devices:
        print("[-] 未获取到有效设备信息,请检查配置。")
        return

    total_devices = len(devices)
    print(f"[*] 准备对 {total_devices} 台设备进行并发操作...\n")

    # 使用 ThreadPoolExecutor
    with ThreadPoolExecutor(max_workers=20) as executor:
        # 提交所有任务
        executor.map(run_commands_on_device, devices)

    # 任务结束后打印汇总
    print("-" * 30)
    print(f"统计报告:")
    print(f"  - 总计设备: {total_devices}")
    print(f"  - 成功数量: {success_count}")
    print(f"  - 失败数量: {error_count}")
    print("-" * 30)
    print(f"\n[DONE] 批量任务结束, 日志已保存至 {LOG_DIR}")

if __name__ == "__main__":
    main()

configLoader.py

import json
from copy import deepcopy

def load_and_merge_config(file_path):
    """
    解析 JSON 配置文件,支持多模板继承(字符串或列表)和无用户名处理
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        templates = data.get("templates", {})
        device_list = data.get("device_list", [])
        
        final_devices = []
        for dev in device_list:
            # 1. 初始化基础配置
            merged_template_config = {}
            
            # 2. 获取模板引用(可能是 字符串 或 列表)
            template_refs = dev.get("use_template", [])
            
            # 统一转为列表处理,方便后续循环
            if isinstance(template_refs, str):
                template_refs = [template_refs]
            
            # 3. 逐个合并模板:后面的模板会覆盖前面的
            for t_name in template_refs:
                template_data = templates.get(t_name, {})
                # 使用 deepcopy 确保多层字典嵌套时不会互相污染
                merged_template_config.update(deepcopy(template_data))
            
            # 4. 合并设备自身的参数:设备自身优先级最高
            full_config = {**merged_template_config, **dev}
            
            # 5. 清理辅助字段
            full_config.pop("use_template", None)
            
            # 6. 针对无用户名 Telnet 的特殊兼容处理
            # 只要 username 为空、全空格或不存在,就删除该键
            if not str(full_config.get('username', '')).strip():
                full_config.pop('username', None)
                
            final_devices.append(full_config)
        
        return final_devices

    except FileNotFoundError:
        print(f"[-] 错误: 找不到配置文件 {file_path}")
        return []
    except json.JSONDecodeError:
        print(f"[-] 错误: {file_path} 格式不正确")
        return []
    except Exception as e:
        print(f"[-] 异常: {e}")
        return []

# 测试运行 生产环境请注释
# devices = load_and_merge_config('devices.json')
# for d in devices:
#     print(d)