import pandas as pd
from paramiko import SSHClient, AutoAddPolicy
from datetime import date
import os
import logging
import ipaddress
from time import sleep
from concurrent.futures import ThreadPoolExecutor
try:
from ncclient import manager
except ImportError:
logging.error("缺少'ncclient'模块,请使用 `pip install ncclient` 进行安装。")
raise
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("automation.log"), logging.StreamHandler()]
)
# 配置文件路径
EXCEL_FILE = r"devices.xlsx" # Windows 路径需要加 r
BACKUP_DIR = os.path.join(".", "backups", str(date.today()))
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
YANG_XML_TEMPLATE = """
"""
class NetworkDevice:
def __init__(self, ip, username, password):
self.server = ip
self.username = username
self.password = password
self.client = self._connect_ssh()
def _connect_ssh(self):
"""建立SSH连接,带重试机制"""
attempts = 0
while attempts < 3:
try:
client = SSHClient()
client.set_missing_host_key_policy(AutoAddPolicy)
client.connect(self.server, username=self.username, password=self.password)
return client
except Exception as e:
attempts += 1
logging.warning(f"SSH连接失败,正在重试 {attempts}/3 次: {e}")
sleep(5)
raise ConnectionError(f"无法连接到设备 {self.server},请检查网络或凭据。")
def execute_commands(self, commands):
"""通过SSH执行命令"""
with self.client.invoke_shell() as shell:
for cmd in commands:
logging.info(f"执行命令: {cmd.strip()}")
shell.send(cmd + "\n")
sleep(2)
output = shell.recv(99999).decode()
logging.info(output)
def save_config(self, filename):
"""保存设备配置"""
cmd = f"save force {filename}.zip\n"
self.execute_commands([cmd])
def download_config(self, remote_file, local_file):
"""通过SFTP下载设备配置"""
try:
sftp = self.client.open_sftp()
sftp.get(remote_file, local_file)
logging.info(f"配置已下载到: {local_file}")
except Exception as e:
logging.error(f"配置下载失败: {e}")
def close(self):
self.client.close()
def configure_netconf(xml, device_ip, username, password):
"""通过Netconf配置设备"""
try:
with manager.connect(
host=device_ip,
username=username,
password=password,
hostkey_verify=False,
device_params={"name": "huawei"}
) as m:
response = m.edit_config(target="running", config=xml)
logging.info(f"Netconf配置结果: {response}")
except Exception as e:
logging.error(f"Netconf配置失败: {e}")
def validate_device_info(device):
"""验证设备信息格式"""
try:
# 检查IP地址格式
ipaddress.ip_address(device["IP"])
except ValueError:
raise ValueError(f"无效的IP地址: {device['IP']}")
# 检查端口号是否为纯数字(如果有端口字段)
if "Port" in device and not str(device["Port"]).isdigit():
raise ValueError(f"无效的端口号: {device['Port']}")
def process_device(device):
"""处理单个设备:备份、巡检、配置Netconf"""
try:
validate_device_info(device)
device_instance = NetworkDevice(device["IP"], device["SSHUser"], device["SSHPassword"])
# 初次备份
today = str(date.today())
backup_filename = f"{today}_{device['DeviceName']}"
device_instance.save_config(backup_filename)
device_instance.download_config(f"{backup_filename}.zip", os.path.join(BACKUP_DIR, f"{backup_filename}.bak"))
# 配置Netconf日志服务器
xml_config = YANG_XML_TEMPLATE.replace("{{LogName}}", device["LogName"]).replace("{{LogServer}}", device["LogServer"])
configure_netconf(xml_config, device["IP"], device["NetConfUser"], device["NetConfPassword"])
except Exception as e:
logging.error(f"设备 {device['DeviceName']} 处理失败: {e}")
finally:
if 'device_instance' in locals():
device_instance.close()
def main():
"""主流程:从Excel读取设备信息并并发处理"""
try:
# 从Excel加载设备信息,忽略列名中的空格
devices = pd.read_excel(EXCEL_FILE).rename(columns=lambda x: x.strip()).to_dict(orient="records")
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(process_device, device) for device in devices]
for future in futures:
try:
future.result()
except Exception as e:
logging.error(f"设备处理失败: {e}")
except Exception as e:
logging.error(f"运行失败: {e}")
if __name__ == "__main__":
main()