现在我的功能都能实现,但是最近上了一台h3c V5版本的接入交换机,绑定命令和其他接入交换机不一样,绑定步骤是这样的,首先登录到这个交换机,输入display mac-address xxxx-xxxx-xxxx 回显如下<CDSL_SW201.28_LBG>dis mac-add 1c69-7a56-b2fb
MAC ADDR VLAN ID STATE PORT INDEX AGING TIME(s)
1c69-7a56-b2fb 116 LEARNED GigabitEthernet1/0/7 AGING
--- 1 MAC address(es) found --- 然后进入全局配置模式sys 再进入到具体的端口,如上是GigabitEthernet1/0/7 进入之后执行绑定命令 user-bind ip-address ip地址 mac-address xxxx-xxxx-xxxx(mac地址)然后再输入dis user-bind mac-address xxxx-xxxx-xxxx验证是否绑定成功,然后输入save force保存即可 因为V5的交换机不多,可以直接在代码里面写一个列表,如果在这个列表里面,就执行V5的命令 我现在的代码如下import logging
import re
import time
import threading
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
from netmiko import ConnectHandler
# ----------------------- 日志配置 -----------------------
logging.basicConfig(
filename='ip_mac_bind.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
# ----------------------- 配置参数 -----------------------
DEVICES = {
'oa_core_switch': {
'device_type': 'hp_comware',
'host': '172.17.21.1',
'username': 'user',
'password': 'CD5',
'timeout': 30
},
'aggregation_switch': {
'device_type': 'hp_comware',
'username': 'user',
'password': 'CD345',
'timeout': 30
},
'access_switch': {
'device_type': 'hp_comware',
'username': 'python',
'password': 'C345',
'timeout': 30,
'global_delay_factor': 3
}
}
# 已知汇聚交换机列表
AGGREGATION_SWITCHES = ["172.17.21.2", "172.17.21.3", "172.17.21.4", "172.17.21.22"]
# ----------------------- 工具函数 -----------------------
def format_mac_address(raw_mac):
"""格式化MAC地址为H3C格式:xxxx-xxxx-xxxx"""
cleaned = re.sub(r'[^0-9a-fA-F]', '', raw_mac)
if len(cleaned) != 12:
raise ValueError("无效的MAC地址格式")
return f"{cleaned[:4]}-{cleaned[4:8]}-{cleaned[8:12]}".lower()
def connect_with_retry(device_info, retries=3, delay=5):
"""带重试机制的设备连接"""
for attempt in range(retries):
try:
# 确保设置了合理的延迟因子
if 'global_delay_factor' not in device_info:
device_info['global_delay_factor'] = 2
conn = ConnectHandler(**device_info)
logging.info(f"成功连接 {device_info['host']}")
# 确保禁用分页
conn.send_command_timing("screen-length disable", delay_factor=2)
time.sleep(0.5) # 确保命令执行完成
return conn
except Exception as e:
logging.error(f"连接失败({attempt + 1}/{retries}) {device_info['host']}: {str(e)}")
time.sleep(delay)
raise ConnectionError(f"无法连接 {device_info['host']}")
def get_device_credentials(ip):
"""根据IP地址确定设备类型"""
if ip == DEVICES['oa_core_switch']['host']:
return DEVICES['oa_core_switch'].copy()
if ip in AGGREGATION_SWITCHES:
# 汇聚交换机使用自己的凭据
creds = DEVICES['aggregation_switch'].copy()
creds['host'] = ip
return creds
else:
# 默认使用接入交换机凭据
creds = DEVICES['access_switch'].copy()
creds['host'] = ip
return creds
def parse_switch_name(switch_name):
"""将交换机名称转换为IP地址"""
# 支持格式: SW201.55 或 sw201.55
pattern = r'^[sS][wW](\d{1,3})\.(\d{1,3})$'
match = re.match(pattern, switch_name)
if not match:
raise ValueError("交换机名称格式错误,请使用 SW交换机编号 的格式")
building = match.group(1)
switch_num = match.group(2)
# 验证数据在合理范围内
if not 0 <= int(building) <= 255:
raise ValueError(f"数据超出范围: {building}")
# 验证交换机编号在合理范围内
if not 0 <= int(switch_num) <= 255:
raise ValueError(f"交换机编号超出范围: {switch_num}")
return f"172.17.{building}.{switch_num}"
# ----------------------- 核心功能 -----------------------
def find_port_with_mac(conn, mac):
"""查找MAC地址所在的端口(支持单链路和聚合端口)"""
try:
output = conn.send_command_timing(f"display mac-address {mac}", delay_factor=4)
logging.info(f"MAC地址查询结果 ({conn.host}):\n{output}")
# 查找端口(可能是聚合端口或普通端口)
found_port = None
# 正则表达式匹配端口
port_pattern = r'(BAGG\d+|GE\S+|XGE\S+|Ten-GigabitEthernet\S+)'
for line in output.splitlines():
# 查找Learned状态的端口
if 'Learned' in line:
# 优先查找聚合端口
if 'BAGG' in line:
match = re.search(r'BAGG\d+', line)
if match:
found_port = match.group()
logging.info(f"在{conn.host}上找到聚合端口: {found_port}")
return found_port, "AGG"
# 查找物理端口
elif 'GE' in line or 'XGE' in line or 'Ten-GigabitEthernet' in line:
match = re.search(port_pattern, line)
if match:
found_port = match.group()
logging.info(f"在{conn.host}上找到物理端口: {found_port}")
return found_port, "PHY"
# 如果没找到明确的端口,尝试普通查询
for line in output.splitlines():
if 'GE' in line or 'XGE' in line or 'BAGG' in line or 'Ten-GigabitEthernet' in line:
match = re.search(port_pattern, line)
if match:
found_port = match.group()
if 'BAGG' in found_port:
logging.info(f"在{conn.host}上找到聚合端口: {found_port}")
return found_port, "AGG"
else:
logging.info(f"在{conn.host}上找到物理端口: {found_port}")
return found_port, "PHY"
logging.warning(f"在{conn.host}上未找到明确的端口")
return None, None
except Exception as e:
logging.error(f"MAC地址查询失败 ({conn.host}): {str(e)}")
return None, None
def parse_lldp_neighbor(output):
"""解析LLDP输出获取管理IP"""
for line in output.splitlines():
if 'Management address' in line and ':' in line:
ip_match = re.search(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b', line)
if ip_match:
return ip_match.group()
return None
def get_lldp_neighbor(conn, port):
"""获取LLDP邻居信息"""
try:
# 确保端口格式正确,移除多余空格和括号
port = re.sub(r'[()]', '', port).strip()
# H3C端口格式转换
if port.startswith('XGE'):
converted_port = port.replace("XGE", "Ten-GigabitEthernet")
elif port.startswith('GE'):
converted_port = port.replace("GE", "GigabitEthernet")
else:
converted_port = port
logging.info(f"在{conn.host}上查询接口 {converted_port} 的LLDP邻居信息")
output = conn.send_command_timing(
f"display lldp neighbor-information interface {converted_port} verbose",
read_timeout=90,
delay_factor=4
)
return parse_lldp_neighbor(output)
except Exception as e:
logging.error(f"在{conn.host}上LLDP查询失败: {str(e)}")
return None
def get_agg_members(conn, agg_port):
"""获取聚合组的成员端口(改进的解析逻辑)"""
try:
# 将聚合端口转换为标准格式(如BAGG12 -> Bridge-Aggregation12)
if agg_port.startswith('BAGG'):
bridge_agg = "Bridge-Aggregation" + agg_port[4:]
else:
bridge_agg = agg_port
logging.info(f"查询聚合组: {bridge_agg}")
# 使用正确的命令格式
output = conn.send_command_timing(
f"display link-aggregation verbose {bridge_agg}",
delay_factor=4
)
logging.info(f"聚合组查询结果:\n{output}")
# 解析成员端口 - 改进的解析逻辑
members = []
in_local_section = False
for line in output.splitlines():
# 查找Local部分标题
if "Local:" in line:
in_local_section = True
continue
if in_local_section:
# 检查是否进入Remote部分
if "Remote:" in line:
break
# 忽略空行
if not line.strip():
continue
# 提取物理端口(行中第一个元素)
parts = line.split()
if parts and any(port_type in parts[0] for port_type in ['GE', 'XGE', 'Ten']):
port_name = parts[0]
# 移除可能的参考端口标识 (R)
if '(' in port_name:
port_name = port_name.split('(')[0]
members.append(port_name)
logging.info(f"找到成员端口: {port_name}")
if not members:
# 尝试备用解析方法
logging.warning("使用备用方法解析成员端口")
for line in output.splitlines():
if any(port_type in line for port_type in ['GE', 'XGE', 'Ten']):
match = re.search(r'(GE\S+|XGE\S+|Ten-GigabitEthernet\S+)', line)
if match:
port_name = match.group()
if '(' in port_name:
port_name = port_name.split('(')[0]
if port_name not in members:
members.append(port_name)
logging.info(f"找到成员端口: {port_name}")
if not members:
logging.warning(f"未能在聚合组 {bridge_agg} 中找到成员端口")
else:
logging.info(f"找到 {len(members)} 个成员端口")
return members
except Exception as e:
logging.error(f"聚合组查询失败: {str(e)}")
return []
def get_next_hop_from_port(conn, port, port_type):
"""根据端口类型获取下一跳设备"""
if port_type == "AGG":
logging.info(f"处理聚合端口: {port}")
# 获取聚合组中的所有物理端口
physical_ports = get_agg_members(conn, port)
if not physical_ports:
logging.warning(f"在{conn.host}上未找到聚合端口 {port} 的成员端口")
return None
# 尝试每个物理端口查找LLDP邻居
for physical_port in physical_ports:
logging.info(f"在物理端口 {physical_port} 上查询LLDP邻居")
neighbor_ip = get_lldp_neighbor(conn, physical_port)
if neighbor_ip:
logging.info(f"在端口 {physical_port} 上找到邻居: {neighbor_ip}")
return neighbor_ip
return None
else:
logging.info(f"处理物理端口: {port}")
return get_lldp_neighbor(conn, port)
def trace_next_hop(switch_ip, mac):
"""追踪下一跳设备(改进的聚合端口处理)"""
conn = None
try:
# 获取设备凭据
device_info = get_device_credentials(switch_ip)
conn = connect_with_retry(device_info)
# 查找MAC地址所在的端口及其类型
port, port_type = find_port_with_mac(conn, mac)
if not port:
logging.warning(f"在{switch_ip}上未找到MAC地址 {mac} 对应的端口")
return None
# 查找邻居设备IP
neighbor_ip = get_next_hop_from_port(conn, port, port_type)
if neighbor_ip:
logging.info(f"在{switch_ip}上端口 {port} 找到邻居设备: {neighbor_ip}")
return neighbor_ip
else:
logging.warning(f"在{switch_ip}上端口 {port} 未找到LLDP邻居")
return None
except Exception as e:
logging.error(f"在{switch_ip}上追踪失败: {str(e)}")
return None
finally:
if conn:
conn.disconnect()
def trace_final_device(mac):
"""追踪到最终设备"""
formatted_mac = format_mac_address(mac)
logging.info(f"开始追踪 MAC: {formatted_mac}")
# 第一跳:核心交换机
core_ip = DEVICES['oa_core_switch']['host']
first_hop = trace_next_hop(core_ip, formatted_mac)
if not first_hop:
logging.warning("无法从核心交换机找到第一跳设备")
return None
# 检查第一跳设备是否是汇聚交换机
if first_hop in AGGREGATION_SWITCHES:
logging.info(f"{first_hop} 是已知汇聚交换机,继续追踪")
# 第二跳:从汇聚交换机追踪
final_device = trace_next_hop(first_hop, formatted_mac)
if not final_device:
logging.info(f"在汇聚交换机{first_hop}上未找到下层设备,使用该交换机作为最终设备")
return first_hop
return final_device
else:
logging.info(f"{first_hop} 不是已知汇聚交换机,作为最终设备")
return first_hop
def configure_device(device_ip, ip, mac):
"""在最终设备上绑定IP-MAC"""
conn = None
try:
# 获取设备凭据
device_info = get_device_credentials(device_ip)
# 为接入交换机设置更高的延迟因子
device_info['global_delay_factor'] = 3
conn = connect_with_retry(device_info)
# 进入系统视图 - 使用send_command_timing避免期望字符串问题
conn.send_command_timing("system-view", delay_factor=3)
time.sleep(1) # 确保进入系统视图
# 绑定IP-MAC
cmd = f"ip source binding ip-address {ip} mac-address {mac}"
output = conn.send_command_timing(cmd, delay_factor=3)
logging.info(f"在{device_ip}上执行命令: {cmd}\n输出: {output}")
# 保存配置 - 使用更稳健的方式处理保存确认
save_output = conn.send_command_timing("save force", delay_factor=3)
if "Are you sure" in save_output or "(Y/N)" in save_output:
save_output += conn.send_command_timing("y", delay_factor=3)
time.sleep(2) # 给保存操作足够的时间
logging.info(f"在{device_ip}上保存配置结果: {save_output}")
# 检查是否成功绑定
verify_cmd = f"display ip source binding ip-address {ip}"
verify_output = conn.send_command_timing(verify_cmd, delay_factor=3)
logging.info(f"验证绑定: {verify_output}")
# 确认MAC地址是否在输出中
if mac.lower() in verify_output.lower():
logging.info(f"在{device_ip}上成功绑定验证")
return True, device_ip
else:
logging.warning(f"在{device_ip}上未找到绑定记录")
return False, device_ip
except Exception as e:
logging.error(f"在{device_ip}上配置失败: {str(e)}")
return False, device_ip
finally:
if conn:
try:
conn.disconnect()
except:
pass
def bind_ip_mac(ip, mac):
"""主绑定流程(自动发现)"""
try:
formatted_mac = format_mac_address(mac)
logging.info(f"开始处理 自动发现: IP: {ip}, MAC: {formatted_mac}")
# 查找最终设备
device_ip = trace_final_device(formatted_mac)
if not device_ip:
return f"❌ 错误: 无法找到MAC {formatted_mac} 对应的网络设备"
# 在最终设备上配置
success, target_ip = configure_device(device_ip, ip, formatted_mac)
return f"✅ 成功: 在 {target_ip} 上绑定 {ip} - {formatted_mac}" if success else \
f"❌ 错误: 无法在 {target_ip} 上绑定 {ip} - {formatted_mac}"
except Exception as e:
error_msg = f"⚠️ 异常: {str(e)}"
logging.error(error_msg)
return error_msg
def manual_bind(ip, mac, switch_name):
"""手动绑定流程"""
try:
formatted_mac = format_mac_address(mac)
switch_ip = parse_switch_name(switch_name)
logging.info(f"开始处理 手动绑定: IP: {ip}, MAC: {formatted_mac}, 交换机: {switch_name} -> {switch_ip}")
# 在指定交换机上配置
success, target_ip = configure_device(switch_ip, ip, formatted_mac)
return f"✅ 成功: 在 {target_ip} 上手动绑定 {ip} - {formatted_mac}" if success else \
f"❌ 错误: 无法在 {target_ip} 上手动绑定 {ip} - {formatted_mac}"
except ValueError as ve:
error_msg = f"⚠️ 输入错误: {str(ve)}"
logging.error(error_msg)
return error_msg
except Exception as e:
error_msg = f"⚠️ 异常: {str(e)}"
logging.error(error_msg)
return error_msg
# ----------------------- GUI界面 -----------------------
class IPMacBindApp:
def __init__(self, root):
self.root = root
self.root.title("IP-MAC绑定工具")
self.root.geometry("900x750") # 调整尺寸以适应新输入区域
self.root.resizable(True, True)
# 设置现代主题
style = ttk.Style()
style.theme_use('clam')
# 配置颜色
style.configure("TFrame", background="#f0f0f0")
style.configure("TLabel", background="#f0f0f0", foreground="#333333")
style.configure("TButton", background="#4a86e8", foreground="white", font=("Arial", 10))
style.map("TButton", background=[('active', '#3a76d8')])
style.configure("Accent.TButton", background="#28a745", foreground="white", font=("Arial", 10, "bold"))
style.map("Accent.TButton", background=[('active', '#218838')])
style.configure("Treeview", background="white", fieldbackground="white", foreground="#333333")
style.configure("Treeview.Heading", background="#e9ecef", foreground="#333333", font=("Arial", 9, "bold"))
style.configure("TLabelframe", background="#f0f0f0", foreground="#333333")
style.configure("TLabelframe.Label", background="#f0f0f0", foreground="#333333")
style.configure("TScrollbar", background="#e9ecef")
# 配置操作按钮样式
style.configure("Delete.TButton", background="#e74c3c", foreground="white", font=("Arial", 9, "bold"))
style.map("Delete.TButton", background=[('active', '#c0392b')])
# 创建主框架
main_frame = ttk.Frame(root)
main_frame.pack(fill="both", expand=True, padx=15, pady=15)
# 标题区域
title_frame = ttk.Frame(main_frame)
title_frame.pack(fill="x", pady=(0, 10))
title_label = ttk.Label(
title_frame,
text="IP-MAC绑定工具",
font=("Arial", 16, "bold"),
foreground="#2c3e50"
)
title_label.pack(pady=5)
# ========== 新增手动绑定区域 ==========
manual_frame = ttk.LabelFrame(main_frame, text="手动交换机绑定")
manual_frame.pack(fill="x", pady=10, padx=5)
# 使用网格布局
manual_grid = ttk.Frame(manual_frame)
manual_grid.pack(fill="x", padx=10, pady=10)
# 交换机名称标签和输入框
ttk.Label(manual_grid, text="交换机名称:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
self.switch_entry = ttk.Entry(manual_grid, width=20)
self.switch_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
self.switch_entry.insert(0, "SW201.55")
# 提示文本
tip_label = ttk.Label(
manual_grid,
text="格式: SW交换机号 (示例: SW201.55 -> 172.17.201.55)",
foreground="#666666",
font=("Arial", 8)
)
tip_label.grid(row=0, column=2, padx=10, pady=5, sticky="w", columnspan=3)
# IP地址标签和输入框
ttk.Label(manual_grid, text="IP地址:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
self.manual_ip_entry = ttk.Entry(manual_grid, width=20)
self.manual_ip_entry.grid(row=1, column=1, padx=5, pady=5, sticky="w")
# MAC地址标签和输入框
ttk.Label(manual_grid, text="MAC地址:").grid(row=1, column=2, padx=5, pady=5, sticky="e")
self.manual_mac_entry = ttk.Entry(manual_grid, width=20)
self.manual_mac_entry.grid(row=1, column=3, padx=5, pady=5, sticky="w")
# 添加手动绑定按钮
manual_add_btn = ttk.Button(
manual_grid,
text="添加手动绑定任务",
command=self.add_manual_to_queue,
width=18,
style="Accent.TButton"
)
manual_add_btn.grid(row=1, column=4, padx=10, pady=5)
# 添加分隔线
separator = ttk.Separator(main_frame, orient="horizontal")
separator.pack(fill="x", pady=10)
# ========== 自动绑定区域 ==========
auto_frame = ttk.LabelFrame(main_frame, text="自动发现绑定 (通过LLDP)")
auto_frame.pack(fill="x", pady=10, padx=5)
# 使用网格布局
auto_grid = ttk.Frame(auto_frame)
auto_grid.pack(fill="x", padx=10, pady=10)
ttk.Label(auto_grid, text="IP地址:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
self.auto_ip_entry = ttk.Entry(auto_grid, width=20)
self.auto_ip_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(auto_grid, text="MAC地址:").grid(row=0, column=2, padx=5, pady=5, sticky="e")
self.auto_mac_entry = ttk.Entry(auto_grid, width=20)
self.auto_mac_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w")
auto_add_btn = ttk.Button(
auto_grid,
text="添加到队列 (自动发现)",
command=self.add_auto_to_queue,
width=18,
style="TButton"
)
auto_add_btn.grid(row=0, column=4, padx=10, pady=5)
# 分隔线下方的队列区域
separator2 = ttk.Separator(main_frame, orient="horizontal")
separator2.pack(fill="x", pady=5)
# 队列区域
queue_frame = ttk.LabelFrame(main_frame, text="绑定任务队列")
queue_frame.pack(fill="both", expand=True, pady=5)
# 创建队列树视图(添加操作列)
columns = ("type", "ip", "mac", "switch", "status", "actions")
self.queue_tree = ttk.Treeview(
queue_frame,
columns=columns,
show="headings",
height=8,
selectmode="browse"
)
self.queue_tree.heading("type", text="类型")
self.queue_tree.heading("ip", text="IP地址")
self.queue_tree.heading("mac", text="MAC地址")
self.queue_tree.heading("switch", text="目标交换机")
self.queue_tree.heading("status", text="状态")
self.queue_tree.heading("actions", text="操作")
self.queue_tree.column("type", width=80, anchor="center")
self.queue_tree.column("ip", width=120, anchor="center")
self.queue_tree.column("mac", width=150, anchor="center")
self.queue_tree.column("switch", width=150, anchor="center")
self.queue_tree.column("status", width=100, anchor="center")
self.queue_tree.column("actions", width=80, anchor="center")
# 配置状态颜色
self.queue_tree.tag_configure('waiting', foreground='#6c757d')
self.queue_tree.tag_configure('processing', foreground='#0d6efd')
self.queue_tree.tag_configure('manual', foreground='#e67e22')
self.queue_tree.tag_configure('auto', foreground='#3498db')
self.queue_tree.tag_configure('success', foreground='#198754')
self.queue_tree.tag_configure('failed', foreground='#dc3545')
# 添加滚动条
tree_scroll = ttk.Scrollbar(
queue_frame,
orient="vertical",
command=self.queue_tree.yview
)
self.queue_tree.configure(yscrollcommand=tree_scroll.set)
# 布局
self.queue_tree.pack(side="left", fill="both", expand=True, padx=5, pady=5)
tree_scroll.pack(side="right", fill="y", padx=5, pady=5)
# 操作按钮区域
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill="x", pady=10)
# 添加开始绑定按钮
self.start_btn = ttk.Button(
btn_frame,
text="开始绑定",
command=self.start_binding,
width=12,
style="Accent.TButton"
)
self.start_btn.pack(side="left", padx=5)
# 添加重试失败按钮
self.retry_btn = ttk.Button(
btn_frame,
text="重试失败",
command=self.retry_failed,
width=12,
style="TButton"
)
self.retry_btn.pack(side="left", padx=5)
# 其他操作按钮
clear_queue_btn = ttk.Button(
btn_frame,
text="清空队列",
command=self.clear_queue,
width=12,
style="TButton"
)
clear_queue_btn.pack(side="left", padx=5)
export_btn = ttk.Button(
btn_frame,
text="导出日志",
command=self.export_log,
width=12,
style="TButton"
)
export_btn.pack(side="right", padx=5)
# 日志区域
log_frame = ttk.LabelFrame(main_frame, text="操作日志")
log_frame.pack(fill="both", expand=True, pady=(0, 5))
self.log_area = scrolledtext.ScrolledText(
log_frame,
height=15,
bg="white",
fg="#333333"
)
self.log_area.pack(fill="both", expand=True, padx=5, pady=5)
self.log_area.configure(state="disabled")
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(
root,
textvariable=self.status_var,
relief="sunken",
anchor="w",
background="#e9ecef",
foreground="#6c757d",
font=("Arial", 9)
)
status_bar.pack(side="bottom", fill="x", padx=5, pady=2)
# 状态变量
self.running = False
self.queue = [] # 存储任务信息:((type, ip, mac, switch_name), status)
self.task_ids = {} # 跟踪任务ID,防止重复添加未完成的任务
self.item_ids = {} # 跟踪树项ID,防止重复添加
# 绑定双击事件
self.queue_tree.bind("<Double-1>", self.on_tree_double_click)
# 绑定右键菜单
self.context_menu = tk.Menu(root, tearoff=0)
self.context_menu.add_command(label="删除任务", command=self.delete_selected_task)
self.queue_tree.bind("<Button-3>", self.show_context_menu)
self.log_message("✅ 应用程序已启动")
self.log_message("手动绑定区域: 输入交换机名(如SW201.55)、IP和MAC,点'添加手动绑定任务'")
self.log_message("自动绑定区域: 输入IP和MAC,点'添加到队列(自动发现)'")
def log_message(self, message):
"""添加消息到日志区域"""
self.log_area.configure(state="normal")
self.log_area.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n")
self.log_area.see(tk.END)
self.log_area.configure(state="disabled")
self.status_var.set(message[:60] + "..." if len(message) > 60 else message)
def add_auto_to_queue(self):
"""添加自动绑定任务到队列"""
ip = self.auto_ip_entry.get().strip()
mac = self.auto_mac_entry.get().strip()
if not ip or not mac:
messagebox.showwarning("输入错误", "请输入IP和MAC地址")
return
try:
# 验证MAC格式
formatted_mac = format_mac_address(mac)
# 检查IP格式
if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
raise ValueError("无效的IP地址格式")
# 创建任务标识符
task_type = "auto"
task_id = f"{task_type}|{ip}|{mac}"
# 检查任务是否已存在且未完成
if task_id in self.task_ids:
existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4]
if existing_status in ["等待中", "处理中..."]:
self.log_message(f"⚠️ 任务已存在: {ip} ↔ {formatted_mac} (自动发现)")
return
# 添加到队列
self.queue.append((task_type, ip, mac, ""))
# 添加到队列树
item_id = self.queue_tree.insert(
"",
"end",
values=("自动发现", ip, formatted_mac, "待确定", "等待中", "删除"),
tags=('auto', 'waiting')
)
self.task_ids[task_id] = item_id
self.item_ids[item_id] = task_id # 添加反向映射
# 清空输入框
self.auto_ip_entry.delete(0, tk.END)
self.auto_mac_entry.delete(0, tk.END)
self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (自动发现)")
self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理")
except Exception as e:
self.log_message(f"❌ 错误: {str(e)}")
def add_manual_to_queue(self):
"""添加手动绑定任务到队列"""
switch_name = self.switch_entry.get().strip()
ip = self.manual_ip_entry.get().strip()
mac = self.manual_mac_entry.get().strip()
if not switch_name or not ip or not mac:
messagebox.showwarning("输入错误", "请输入交换机名称、IP和MAC地址")
return
try:
# 验证MAC格式
formatted_mac = format_mac_address(mac)
# 检查IP格式
if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
raise ValueError("无效的IP地址格式")
# 解析交换机名称
switch_ip = parse_switch_name(switch_name)
# 创建任务标识符
task_type = "manual"
task_id = f"{task_type}|{switch_ip}|{ip}|{formatted_mac}"
# 检查任务是否已存在且未完成
if task_id in self.task_ids:
existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4]
if existing_status in ["等待中", "处理中..."]:
self.log_message(f"⚠️ 任务已存在: {switch_name} ↔ {ip} ↔ {formatted_mac}")
return
# 添加到队列
self.queue.append((task_type, ip, mac, switch_name))
# 添加到队列树
item_id = self.queue_tree.insert(
"",
"end",
values=("手动指定", ip, formatted_mac, f"{switch_name} ({switch_ip})", "等待中", "删除"),
tags=('manual', 'waiting')
)
self.task_ids[task_id] = item_id
self.item_ids[item_id] = task_id # 添加反向映射
# 清空输入框(只清空IP和MAC,保留交换机名称)
self.manual_ip_entry.delete(0, tk.END)
self.manual_mac_entry.delete(0, tk.END)
self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (交换机: {switch_name} -> {switch_ip})")
self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理")
except Exception as e:
self.log_message(f"❌ 错误: {str(e)}")
def on_tree_double_click(self, event):
"""处理树状视图的双击事件"""
region = self.queue_tree.identify("region", event.x, event.y)
if region != "cell":
return
column = self.queue_tree.identify_column(event.x)
item_id = self.queue_tree.identify_row(event.y)
if not item_id:
return
# 如果双击的是操作列(第6列)
if column == "#6":
self.confirm_delete_task(item_id)
def confirm_delete_task(self, item_id):
"""确认删除任务"""
values = self.queue_tree.item(item_id, "values")
if not values:
return
if values[4] == "处理中...": # 第5列是状态
messagebox.showwarning("无法删除", "任务正在处理中,无法删除")
return
task_type = values[0] # 第1列是类型
ip = values[1] # 第2列是IP
mac = values[2] # 第3列是MAC
if messagebox.askyesno("确认删除", f"确定要删除 {task_type} 绑定任务吗?\nIP: {ip}\nMAC: {mac}"):
self.delete_task(item_id)
def delete_task(self, item_id):
"""从队列中删除任务"""
if item_id not in self.item_ids:
self.log_message(f"❌ 错误: 树项 {item_id} 不存在")
return
# 获取任务ID
task_id = self.item_ids[item_id]
# 从树视图中删除
self.queue_tree.delete(item_id)
# 从task_ids字典中移除
if task_id in self.task_ids:
del self.task_ids[task_id]
# 从item_ids字典中移除
if item_id in self.item_ids:
del self.item_ids[item_id]
# 从队列中移除任务
task_parts = task_id.split("|")
if len(task_parts) > 1:
task_type = task_parts[0]
ip = task_parts[1]
mac = task_parts[2] if len(task_parts) >= 3 else ""
switch = task_parts[3] if len(task_parts) >= 4 else ""
# 从队列中删除相应的任务
for task in self.queue[:]:
if (
task[0] == task_type
and task[1] == ip
and task[2] == mac
and (len(task) < 4 or task[3] == switch)
):
self.queue.remove(task)
break
self.log_message("♻️ 任务已删除")
self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理")
def show_context_menu(self, event):
"""显示右键菜单"""
item_id = self.queue_tree.identify_row(event.y)
if not item_id:
return
values = self.queue_tree.item(item_id, "values")
if not values or len(values) < 6:
return
self.queue_tree.selection_set(item_id) # 选中该行
# 创建带删除选项的菜单
self.context_menu.delete(0, tk.END)
if values[4] == "处理中...": # 状态在第5列
self.context_menu.add_command(
label="任务处理中(不可删除)"
)
else:
self.context_menu.add_command(
label="删除任务",
command=lambda id=item_id: self.confirm_delete_task(id)
)
self.context_menu.tk_popup(event.x_root, event.y_root)
def delete_selected_task(self):
"""删除当前选中的任务"""
selected = self.queue_tree.selection()
if selected:
self.confirm_delete_task(selected[0])
# 在 add_manual_to_queue 方法中修改任务ID生成方式
def add_manual_to_queue(self):
"""添加手动绑定任务到队列"""
switch_name = self.switch_entry.get().strip()
ip = self.manual_ip_entry.get().strip()
mac = self.manual_mac_entry.get().strip()
if not switch_name or not ip or not mac:
messagebox.showwarning("输入错误", "请输入交换机名称、IP和MAC地址")
return
try:
# 验证MAC格式
formatted_mac = format_mac_address(mac)
# 检查IP格式
if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
raise ValueError("无效的IP地址格式")
# 解析交换机名称
switch_ip = parse_switch_name(switch_name)
# 创建任务标识符 - 使用交换机IP而不是名称
task_type = "manual"
task_id = f"{task_type}|{switch_ip}|{ip}|{formatted_mac}" # 关键修改
# 检查任务是否已存在且未完成
if task_id in self.task_ids:
existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4]
if existing_status in ["等待中", "处理中..."]:
self.log_message(f"⚠️ 任务已存在: {switch_name} ↔ {ip} ↔ {formatted_mac}")
return
# 添加到队列
self.queue.append((task_type, ip, mac, switch_name))
# 添加到队列树
item_id = self.queue_tree.insert(
"",
"end",
values=("手动指定", ip, formatted_mac, f"{switch_name} ({switch_ip})", "等待中", "删除"),
tags=('manual', 'waiting')
)
self.task_ids[task_id] = item_id
self.item_ids[item_id] = task_id # 添加反向映射
# 清空输入框(只清空IP和MAC,保留交换机名称)
self.manual_ip_entry.delete(0, tk.END)
self.manual_mac_entry.delete(0, tk.END)
self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (交换机: {switch_name} -> {switch_ip})")
self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理")
except Exception as e:
self.log_message(f"❌ 错误: {str(e)}")
# 在 start_binding 方法中修改任务ID生成方式
# 修改 add_auto_to_queue 方法
def add_auto_to_queue(self):
"""添加自动绑定任务到队列"""
ip = self.auto_ip_entry.get().strip()
mac = self.auto_mac_entry.get().strip()
if not ip or not mac:
messagebox.showwarning("输入错误", "请输入IP和MAC地址")
return
try:
# 验证MAC格式
formatted_mac = format_mac_address(mac)
# 检查IP格式
if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
raise ValueError("无效的IP地址格式")
# 创建任务标识符
task_type = "auto"
task_id = f"{task_type}|{ip}|{formatted_mac}" # 使用格式化后的MAC
# 检查任务是否已存在且未完成
if task_id in self.task_ids:
existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4]
if existing_status in ["等待中", "处理中..."]:
self.log_message(f"⚠️ 任务已存在: {ip} ↔ {formatted_mac} (自动发现)")
return
# 添加到队列 - 只存储3个元素
self.queue.append((task_type, ip, mac))
# 添加到队列树
item_id = self.queue_tree.insert(
"",
"end",
values=("自动发现", ip, formatted_mac, "待确定", "等待中", "删除"),
tags=('auto', 'waiting')
)
self.task_ids[task_id] = item_id
self.item_ids[item_id] = task_id # 添加反向映射
# 清空输入框
self.auto_ip_entry.delete(0, tk.END)
self.auto_mac_entry.delete(0, tk.END)
self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (自动发现)")
self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理")
except Exception as e:
self.log_message(f"❌ 错误: {str(e)}")
# 修改 start_binding 方法中的任务处理部分
def start_binding(self):
"""开始绑定流程(多线程)"""
if not self.queue:
messagebox.showinfo("操作提示", "绑定队列为空")
return
if self.running:
self.log_message("⚠️ 绑定任务已在执行中")
return
self.running = True
self.start_btn.configure(state="disabled")
self.retry_btn.configure(state="disabled")
self.log_message(" 开始绑定任务处理...")
# 创建线程执行绑定
def worker():
try:
# 创建队列副本避免修改时冲突
queue_copy = self.queue.copy()
for task_info in queue_copy: # 使用[:]创建副本避免修改冲突
if not self.running:
self.log_message("❌ 绑定任务被中断")
break
# 解包任务信息
task_type = task_info[0]
ip = task_info[1]
mac = task_info[2]
# 格式化MAC地址
formatted_mac = format_mac_address(mac)
# 创建任务标识符
if task_type == "manual":
switch_name = task_info[3]
switch_ip = parse_switch_name(switch_name)
task_id = f"{task_type}|{switch_ip}|{ip}|{formatted_mac}"
else: # auto
task_id = f"{task_type}|{ip}|{formatted_mac}"
item_id = self.task_ids.get(task_id)
# 检查任务是否已被删除
if not item_id or not self.queue_tree.exists(item_id):
self.log_message(f"⏩ 跳过已删除任务: {ip} ↔ {formatted_mac}")
continue
# 更新任务状态
self.queue_tree.item(
item_id,
values=(task_type.capitalize(), ip, formatted_mac,
self.queue_tree.item(item_id, "values")[3],
"处理中...", "删除"),
tags=('processing',)
)
self.log_message(f"▶ 正在处理: {task_type}任务 - {ip} ↔ {formatted_mac}")
# 执行绑定
start_time = time.time()
if task_type == "manual":
result = manual_bind(ip, mac, switch_name)
switch_ip = parse_switch_name(switch_name)
# 更新交换机显示
self.queue_tree.item(
item_id,
values=("手动指定", ip, formatted_mac,
f"{switch_name} ({switch_ip})",
"处理中...", "删除"),
tags=('processing',)
)
else: # auto
# 调用自动绑定函数
result = bind_ip_mac(ip, mac)
elapsed = time.time() - start_time
self.log_message(f"⏱ 处理耗时: {elapsed:.2f}秒")
self.log_message(result)
# 解析结果并更新状态
if "成功" in result:
status = "✅ 已完成"
tag = 'success'
# 自动任务需要更新交换机信息
if task_type == "auto":
# 尝试从结果中提取设备IP (例如 "✅ 成功: 在 172.17.201.55 上绑定...")
match = re.search(r'在\s*(\d+\.\d+\.\d+\.\d+)\s*上', result)
if match:
device_ip = match.group(1)
self.queue_tree.item(
item_id,
values=("自动发现", ip, formatted_mac,
device_ip, status, "删除"),
tags=(tag,)
)
else:
self.queue_tree.item(
item_id,
values=("自动发现", ip, formatted_mac,
"已发现", status, "删除"),
tags=(tag,)
)
else: # manual
self.queue_tree.item(
item_id,
values=("手动指定", ip, formatted_mac,
self.queue_tree.item(item_id, "values")[3],
status, "删除"),
tags=(tag,)
)
else:
status = "❌ 失败"
tag = 'failed'
self.queue_tree.item(
item_id,
values=(task_type.capitalize(), ip, formatted_mac,
self.queue_tree.item(item_id, "values")[3],
status, "删除"),
tags=(tag,)
)
# 处理完成后从主队列中移除
if task_info in self.queue:
self.queue.remove(task_info)
# 短暂暂停避免设备过载
time.sleep(1)
self.log_message("🏁 所有绑定任务处理完成")
except Exception as e:
self.log_message(f"⚠️ 绑定过程中断: {str(e)}")
finally:
self.start_btn.configure(state="normal")
self.retry_btn.configure(state="normal")
self.running = False
self.status_var.set("就绪 | 绑定任务完成")
threading.Thread(target=worker, daemon=True).start()
def retry_failed(self):
"""重试所有失败的任务"""
# 收集所有失败的任务
failed_tasks = []
for item_id in self.queue_tree.get_children():
values = self.queue_tree.item(item_id, "values")
if values and values[4] == "❌ 失败": # 第5列是状态
ip, mac = values[1], values[2]
task_type = values[0]
if task_type == "手动指定":
switch_name_match = re.search(r'(\S+)\s*\(', values[3]) # 从"SW201.55 (172.17.201.55)"提取"SW201.55"
if switch_name_match:
switch_name = switch_name_match.group(1)
failed_tasks.append((item_id, "manual", ip, mac, switch_name))
else:
failed_tasks.append((item_id, "auto", ip, mac, ""))
if not failed_tasks:
messagebox.showinfo("操作提示", "没有失败的任务")
return
self.log_message(f"🔄 开始重试 {len(failed_tasks)} 个失败任务")
# 直接开始绑定,不修改队列
self.running = True
self.start_btn.configure(state="disabled")
self.retry_btn.configure(state="disabled")
# 创建线程执行重试
def worker():
try:
for task_info in failed_tasks:
item_id, task_type, ip, mac, switch_name = task_info
if not self.running:
self.log_message("❌ 重试任务被中断")
break
# 检查树项是否存在
if not self.queue_tree.exists(item_id):
self.log_message(f"⏩ 跳过不存在的任务: {ip} ↔ {mac}")
continue
# 更新任务状态
self.queue_tree.item(
item_id,
values=(self.queue_tree.item(item_id, "values")[0],
ip, format_mac_address(mac),
self.queue_tree.item(item_id, "values")[3],
"处理中...", "删除"),
tags=('processing',)
)
self.log_message(f"▶ 正在重试: {task_type}任务 - {ip} ↔ {format_mac_address(mac)}")
# 执行绑定
start_time = time.time()
if task_type == "manual":
result = manual_bind(ip, mac, switch_name)
else: # auto
result = bind_ip_mac(ip, mac)
elapsed = time.time() - start_time
self.log_message(f"⏱ 处理耗时: {elapsed:.2f}秒")
self.log_message(result)
# 更新任务状态
if "成功" in result:
status = "✅ 已完成"
tag = 'success'
else:
status = "❌ 失败"
tag = 'failed'
self.queue_tree.item(
item_id,
values=(task_type, ip, format_mac_address(mac),
self.queue_tree.item(item_id, "values")[3],
status, "删除"),
tags=(tag,)
)
# 短暂暂停避免设备过载
time.sleep(1)
self.log_message("🏁 所有重试任务处理完成")
except Exception as e:
self.log_message(f"⚠️ 重试过程中断: {str(e)}")
finally:
self.start_btn.configure(state="normal")
self.retry_btn.configure(state="normal")
self.running = False
self.status_var.set("就绪 | 重试任务完成")
threading.Thread(target=worker, daemon=True).start()
def clear_queue(self):
"""清除整个队列"""
if not self.queue:
return
if messagebox.askyesno("确认", "确定要清空所有绑定任务吗?"):
self.queue = []
self.task_ids = {}
self.item_ids = {}
for item in self.queue_tree.get_children():
self.queue_tree.delete(item)
self.log_message("♻️ 绑定队列已清空")
self.status_var.set("就绪")
def export_log(self):
"""导出日志到文件"""
try:
timestamp = time.strftime("%Y%m%d-%H%M%S")
filename = f"ip_mac_bind_{timestamp}.log"
with open(filename, "w", encoding="utf-8") as f:
f.write(self.log_area.get("1.0", tk.END))
self.log_message(f"📄 日志已导出到 {filename}")
messagebox.showinfo("导出成功", f"日志已保存到 {filename}")
except Exception as e:
self.log_message(f"❌ 日志导出失败: {str(e)}")
messagebox.showerror("导出失败", f"无法保存日志: {str(e)}")
# ----------------------- 主程序 -----------------------
if __name__ == "__main__":
root = tk.Tk()
app = IPMacBindApp(root)
# 居中显示窗口
window_width = 900
window_height = 750
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_x = int((screen_width - window_width) / 2)
position_y = int((screen_height - window_height) / 2)
root.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}")
root.mainloop()
最新发布