基于PyQT5的ssh远程登录控制的APP
APP可以通过ssh登录多台设备
代码部分
功能模块代码
# -*- coding: utf-8 -*-
"""
SSH多设备管理工具 - PyQt5版本
支持多个设备同时SSH登录和控制
"""
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QGridLayout, QGroupBox, QLabel,
QLineEdit, QPushButton, QTextEdit, QComboBox,
QTreeWidget, QTreeWidgetItem, QMessageBox,
QInputDialog, QMenu, QSplitter, QFrame, QCheckBox,
QTabWidget, QSizePolicy)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QMutex
from PyQt5.QtGui import QFont, QIcon, QPixmap, QTextCursor
import paramiko
import json
import os
from datetime import datetime
import threading
import queue
import time
class SSHConnectionThread(QThread):
"""SSH连接线程"""
connection_success = pyqtSignal(str, object)
connection_failed = pyqtSignal(str, str)
def __init__(self, device_id, ip, port, username, password):
super().__init__()
self.device_id = device_id
self.ip = ip
self.port = port
self.username = username
self.password = password
def run(self):
try:
# 创建SSH客户端
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接
ssh.connect(self.ip, port=self.port, username=self.username,
password=self.password, timeout=10)
self.connection_success.emit(self.device_id, ssh)
except Exception as e:
self.connection_failed.emit(self.device_id, str(e))
class SSHCommandThread(QThread):
"""SSH命令执行线程"""
command_finished = pyqtSignal(str, str, str, str)
def __init__(self, device_id, ssh_connection, command):
super().__init__()
self.device_id = device_id
self.ssh_connection = ssh_connection
self.command = command
def run(self):
try:
# 执行命令
stdin, stdout, stderr = self.ssh_connection.exec_command(self.command)
# 读取输出
output = stdout.read().decode('utf-8', errors='ignore')
error = stderr.read().decode('utf-8', errors='ignore')
self.command_finished.emit(self.device_id, self.command, output, error)
except Exception as e:
self.command_finished.emit(self.device_id, self.command, "", str(e))
class SSHMultiTool(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("SSH多设备管理工具")
self.setGeometry(100, 100, 1400, 900)
# SSH连接字典
self.ssh_connections = {}
# 历史数据文件
self.history_file = "ip_history.json"
# 线程管理
self.running_threads = []
self.command_queue = []
self.current_device_id = None
# 加载历史数据
self.load_ip_history()
# 创建UI
self.init_ui()
# 设置样式
self.set_styles()
def init_ui(self):
"""初始化用户界面"""
# 创建中央窗口
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QHBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# 创建分隔器
splitter = QSplitter(Qt.Horizontal)
main_layout.addWidget(splitter)
# 左侧区域 - 提示和日志
left_widget = self.create_left_panel()
splitter.addWidget(left_widget)
# 中间区域 - 设备控制
middle_widget = self.create_middle_panel()
splitter.addWidget(middle_widget)
# 右侧区域 - 设备管理
right_widget = self.create_right_panel()
splitter.addWidget(right_widget)
# 设置分隔器比例
splitter.setSizes([300, 600, 300])
# 初始化数据
self.refresh_history_display()
def create_left_panel(self):
"""创建左侧面板"""
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
# 使用标签页组织左侧内容
left_tabs = QTabWidget()
# 操作提示标签页
tips_widget = QWidget()
tips_layout = QVBoxLayout(tips_widget)
self.tips_text = QTextEdit()
self.tips_text.setReadOnly(True)
# 添加初始提示
initial_tips = """欢迎使用SSH多设备管理工具!
操作说明:
1. 在右侧添加设备IP地址
2. 点击连接按钮进行SSH登录
3. 在中间区域选择预设命令或输入自定义命令
4. 支持同时控制多个设备
5. 历史IP会自动保存,方便下次使用
预设命令功能:
• 勾选需要的命令组合
• 点击"执行选中命令"批量执行
• 点击"全选"选择所有命令
• 点击"清空"清除所有选择
快捷键:
• Enter键:快速执行命令
• 双击设备:快速连接
• 右键菜单:设备操作选项
安全提示:
• 请确保网络环境安全
• 建议使用SSH密钥认证
• 执行命令前请仔细确认"""
self.tips_text.setText(initial_tips)
tips_layout.addWidget(self.tips_text)
# 命令输出标签页
output_widget = QWidget()
output_layout = QVBoxLayout(output_widget)
# 输出操作按钮
output_button_layout = QHBoxLayout()
self.clear_output_btn = QPushButton("清空输出")
self.clear_output_btn.clicked.connect(self.clear_output)
self.clear_output_btn.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1976D2;
}
""")
output_button_layout.addWidget(self.clear_output_btn)
output_button_layout.addStretch()
self.output_text = QTextEdit()
self.output_text.setReadOnly(True)
self.output_text.setFont(QFont("Consolas", 10))
output_layout.addLayout(output_button_layout)
output_layout.addWidget(self.output_text)
# 操作日志标签页
log_widget = QWidget()
log_layout = QVBoxLayout(log_widget)
# 日志操作按钮
log_button_layout = QHBoxLayout()
self.clear_log_btn = QPushButton("清空日志")
self.clear_log_btn.clicked.connect(self.clear_log)
self.clear_log_btn.setStyleSheet("""
QPushButton {
background-color: #FF9800;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #F57C00;
}
""")
log_button_layout.addWidget(self.clear_log_btn)
log_button_layout.addStretch()
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
log_layout.addLayout(log_button_layout)
log_layout.addWidget(self.log_text)
# 添加所有标签页
left_tabs.addTab(tips_widget, "操作提示")
left_tabs.addTab(output_widget, "命令输出")
left_tabs.addTab(log_widget, "操作日志")
# 设置标签页样式
left_tabs.setStyleSheet("""
QTabWidget::pane {
border: 1px solid #ddd;
background-color: white;
}
QTabWidget::tab-bar {
alignment: left;
}
QTabBar::tab {
background-color: #f0f0f0;
padding: 8px 16px;
margin-right: 2px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background-color: #4CAF50;
color: white;
font-weight: bold;
}
QTabBar::tab:hover {
background-color: #e0e0e0;
}
""")
# 添加到左侧布局
left_layout.addWidget(left_tabs)
return left_widget
def create_middle_panel(self):
"""创建中间面板"""
middle_widget = QWidget()
middle_layout = QVBoxLayout(middle_widget)
middle_layout.setSpacing(5)
# 设备控制组 - 合理布局
control_group = QGroupBox("设备控制")
control_layout = QVBoxLayout(control_group)
control_layout.setSpacing(12)
control_layout.setContentsMargins(10, 10, 10, 10)
# 连接状态
status_layout = QHBoxLayout()
status_layout.addWidget(QLabel("已连接设备:"))
self.connected_label = QLabel("无连接")
self.connected_label.setStyleSheet("color: red; font-weight: bold;")
status_layout.addWidget(self.connected_label)
status_layout.addStretch()
control_layout.addLayout(status_layout)
# 设备选择
device_layout = QHBoxLayout()
device_layout.addWidget(QLabel("选择设备:"))
self.device_combo = QComboBox()
self.device_combo.setMinimumWidth(200)
device_layout.addWidget(self.device_combo)
device_layout.addStretch()
control_layout.addLayout(device_layout)
# 命令输入
cmd_layout = QHBoxLayout()
cmd_layout.addWidget(QLabel("命令:"))
self.cmd_entry = QLineEdit()
self.cmd_entry.setPlaceholderText("输入要执行的命令...")
self.cmd_entry.returnPressed.connect(self.execute_command)
cmd_layout.addWidget(self.cmd_entry)
control_layout.addLayout(cmd_layout)
# 按钮区域
button_layout = QHBoxLayout()
self.execute_btn = QPushButton("执行命令")
self.execute_btn.clicked.connect(self.execute_command)
button_layout.addWidget(self.execute_btn)
self.execute_all_btn = QPushButton("全部执行")
self.execute_all_btn.clicked.connect(self.execute_all_command)
button_layout.addWidget(self.execute_all_btn)
self.disconnect_all_btn = QPushButton("断开所有")
self.disconnect_all_btn.clicked.connect(self.disconnect_all)
button_layout.addWidget(self.disconnect_all_btn)
self.stop_all_btn = QPushButton("停止执行")
self.stop_all_btn.clicked.connect(self.stop_all_commands)
self.stop_all_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #d32f2f;
}
""")
button_layout.addWidget(self.stop_all_btn)
button_layout.addStretch()
control_layout.addLayout(button_layout)
# 预设命令勾选区域
preset_group = QGroupBox("常用命令")
preset_layout = QVBoxLayout(preset_group)
preset_layout.setSpacing(12)
preset_layout.setContentsMargins(10, 10, 10, 10)
# 定义分组命令
self.preset_commands = {
"系统信息": [
("系统信息", "uname -a"),
("CPU信息", "lscpu"),
("系统负载", "uptime"),
("查看用户", "who")
],
"资源监控": [
("磁盘使用", "df -h"),
("内存使用", "free -h"),
("进程列表", "ps aux"),
("系统服务", "systemctl list-units --type=service")
],
"网络文件": [
("网络连接", "netstat -tuln"),
("当前目录", "pwd"),
("列出文件", "ls -la"),
("环境变量", "env")
]
}
# 存储勾选框
self.command_checkboxes = {}
# 创建分组勾选框 - 恢复垂直布局
for group_name, commands in self.preset_commands.items():
group_frame = QFrame()
group_frame.setFrameStyle(QFrame.Box)
group_frame.setLineWidth(1)
group_layout = QVBoxLayout(group_frame)
group_layout.setContentsMargins(12, 10, 12, 10)
group_layout.setSpacing(8)
# 分组标题
group_label = QLabel(group_name)
group_label.setStyleSheet("font-weight: bold; color: #2196F3; font-size: 25px; margin-bottom: 10px;")
group_layout.addWidget(group_label)
# 创建该分组的勾选框 - 使用网格布局
checkbox_grid = QGridLayout()
checkbox_grid.setSpacing(15)
row = 0
col = 0
for cmd_name, cmd_text in commands:
checkbox = QCheckBox(cmd_name)
checkbox.setStyleSheet("""
QCheckBox {
font-size: 20px;
font-weight: 500;
padding: 8px;
spacing: 8px;
color: #333333;
}
QCheckBox::indicator {
width: 25px;
height: 25px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #cccccc;
border-radius: 4px;
background-color: white;
}
QCheckBox::indicator:checked {
border: 2px solid #2196F3;
border-radius: 4px;
background-color: #2196F3;
}
QCheckBox:hover {
background-color: #f5f5f5;
border-radius: 4px;
}
""")
self.command_checkboxes[cmd_name] = (checkbox, cmd_text)
checkbox_grid.addWidget(checkbox, row, col)
col += 1
if col >= 2: # 每行显示2个勾选框
col = 0
row += 1
group_layout.addLayout(checkbox_grid)
preset_layout.addWidget(group_frame)
# 操作按钮
preset_button_layout = QHBoxLayout()
self.execute_selected_btn = QPushButton("执行选中命令")
self.execute_selected_btn.clicked.connect(self.execute_selected_commands)
self.execute_selected_btn.setStyleSheet("""
QPushButton {
background-color: #FF9800;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #F57C00;
}
""")
preset_button_layout.addWidget(self.execute_selected_btn)
self.select_all_btn = QPushButton("全选")
self.select_all_btn.clicked.connect(self.select_all_commands)
self.select_all_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #45a049;
}
""")
preset_button_layout.addWidget(self.select_all_btn)
self.clear_all_btn = QPushButton("清空")
self.clear_all_btn.clicked.connect(self.clear_all_commands)
self.clear_all_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #d32f2f;
}
""")
preset_button_layout.addWidget(self.clear_all_btn)
preset_button_layout.addStretch()
preset_layout.addLayout(preset_button_layout)
control_layout.addWidget(preset_group)
# 设置控件大小策略,防止过度拉伸
control_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# 添加到中间布局
middle_layout.addWidget(control_group)
middle_layout.addStretch() # 添加弹性空间推动控件到顶部
return middle_widget
def create_right_panel(self):
"""创建右侧面板"""
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
# 设备管理组
manage_group = QGroupBox("设备管理")
manage_layout = QVBoxLayout(manage_group)
# 添加设备区域
add_frame = QFrame()
add_layout = QGridLayout(add_frame)
# IP地址
add_layout.addWidget(QLabel("IP地址:"), 0, 0)
self.ip_entry = QLineEdit()
self.ip_entry.setPlaceholderText("192.168.1.100")
add_layout.addWidget(self.ip_entry, 0, 1)
# 端口
add_layout.addWidget(QLabel("端口:"), 1, 0)
self.port_entry = QLineEdit()
self.port_entry.setText("22")
add_layout.addWidget(self.port_entry, 1, 1)
# 用户名
add_layout.addWidget(QLabel("用户名:"), 2, 0)
self.username_entry = QLineEdit()
self.username_entry.setPlaceholderText("admin")
add_layout.addWidget(self.username_entry, 2, 1)
# 连接按钮
self.connect_btn = QPushButton("连接设备")
self.connect_btn.clicked.connect(self.connect_device)
add_layout.addWidget(self.connect_btn, 3, 0, 1, 2)
manage_layout.addWidget(add_frame)
# 历史设备
history_label = QLabel("历史设备:")
history_label.setStyleSheet("font-weight: bold; margin-top: 10px;")
manage_layout.addWidget(history_label)
# 设备树
self.history_tree = QTreeWidget()
self.history_tree.setHeaderLabels(["设备", "状态"])
self.history_tree.setColumnWidth(0, 180)
self.history_tree.setColumnWidth(1, 80)
self.history_tree.itemDoubleClicked.connect(self.on_history_double_click)
self.history_tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.history_tree.customContextMenuRequested.connect(self.show_context_menu)
manage_layout.addWidget(self.history_tree)
# 添加到右侧布局
right_layout.addWidget(manage_group)
return right_widget
def set_styles(self):
"""设置样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:pressed {
background-color: #3d8b40;
}
QLineEdit {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
QComboBox {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
QTextEdit {
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
QTreeWidget {
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
QTreeWidget::item {
padding: 5px;
}
QTreeWidget::item:selected {
background-color: #3daee9;
}
""")
def log_message(self, message):
"""记录日志消息"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_entry = f"[{timestamp}] {message}"
self.log_text.append(log_entry)
self.log_text.moveCursor(QTextCursor.End)
def update_output(self, message):
"""更新输出区域"""
self.output_text.append(message)
self.output_text.moveCursor(QTextCursor.End)
def load_ip_history(self):
"""加载IP历史记录"""
if os.path.exists(self.history_file):
try:
with open(self.history_file, 'r', encoding='utf-8') as f:
self.ip_history = json.load(f)
except:
self.ip_history = {}
else:
self.ip_history = {}
def save_ip_history(self):
"""保存IP历史记录"""
try:
with open(self.history_file, 'w', encoding='utf-8') as f:
json.dump(self.ip_history, f, ensure_ascii=False, indent=2)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存历史记录失败: {e}")
def refresh_history_display(self):
"""刷新历史设备显示"""
# 清空树
self.history_tree.clear()
# 添加历史设备
for device_id, device_info in self.ip_history.items():
ip = device_info.get('ip', '')
port = device_info.get('port', '22')
username = device_info.get('username', '')
display_name = f"{username}@{ip}:{port}"
# 检查连接状态
status = "已连接" if device_id in self.ssh_connections else "未连接"
item = QTreeWidgetItem([display_name, status])
if device_id in self.ssh_connections:
item.setForeground(1, Qt.blue)
self.history_tree.addTopLevelItem(item)
# 更新设备选择下拉框
self.device_combo.clear()
for device_id in self.ssh_connections.keys():
device_info = self.ip_history.get(device_id, {})
ip = device_info.get('ip', device_id)
username = device_info.get('username', '')
self.device_combo.addItem(f"{username}@{ip}")
# 更新连接状态显示
if self.ssh_connections:
conn_count = len(self.ssh_connections)
self.connected_label.setText(f"{conn_count} 个设备已连接")
self.connected_label.setStyleSheet("color: green; font-weight: bold;")
else:
self.connected_label.setText("无连接")
self.connected_label.setStyleSheet("color: red; font-weight: bold;")
def connect_device(self):
"""连接设备"""
ip = self.ip_entry.text().strip()
port = self.port_entry.text().strip()
username = self.username_entry.text().strip()
if not ip or not username:
QMessageBox.warning(self, "警告", "请输入IP地址和用户名")
return
try:
port = int(port) if port else 22
except ValueError:
QMessageBox.warning(self, "警告", "端口号必须是数字")
return
# 生成设备ID
device_id = f"{username}@{ip}:{port}"
if device_id in self.ssh_connections:
QMessageBox.warning(self, "警告", "设备已连接")
return
# 获取密码
password, ok = QInputDialog.getText(self, "密码",
f"请输入 {username}@{ip} 的密码:",
QLineEdit.Password)
if not ok or not password:
return
# 创建连接线程
self.connect_thread = SSHConnectionThread(device_id, ip, port, username, password)
self.connect_thread.connection_success.connect(self.on_connection_success)
self.connect_thread.connection_failed.connect(self.on_connection_failed)
self.log_message(f"正在连接 {device_id}...")
self.connect_btn.setEnabled(False)
self.connect_thread.start()
def on_connection_success(self, device_id, ssh_connection):
"""连接成功处理"""
self.ssh_connections[device_id] = ssh_connection
# 解析设备信息
parts = device_id.split('@')
username = parts[0]
ip_port = parts[1]
ip, port = ip_port.split(':')
# 保存到历史记录
self.ip_history[device_id] = {
'ip': ip,
'port': int(port),
'username': username,
'last_connect': datetime.now().isoformat()
}
self.save_ip_history()
self.log_message(f"成功连接 {device_id}")
self.connect_btn.setEnabled(True)
self.refresh_history_display()
def on_connection_failed(self, device_id, error_message):
"""连接失败处理"""
self.log_message(f"连接失败 {device_id}: {error_message}")
QMessageBox.critical(self, "连接失败", f"无法连接到 {device_id}\n错误: {error_message}")
self.connect_btn.setEnabled(True)
def execute_command(self):
"""执行命令"""
command = self.cmd_entry.text().strip()
if not command:
QMessageBox.warning(self, "警告", "请输入命令")
return
selected_device = self.device_combo.currentText()
if not selected_device:
QMessageBox.warning(self, "警告", "请选择设备")
return
# 查找对应的设备ID
device_id = None
for dev_id, dev_info in self.ip_history.items():
if f"{dev_info.get('username', '')}@{dev_info.get('ip', '')}" == selected_device:
device_id = dev_id
break
if not device_id or device_id not in self.ssh_connections:
QMessageBox.critical(self, "错误", "设备未连接")
return
# 停止当前正在运行的批量命令
self.stop_all_threads()
self.command_queue.clear()
# 创建命令执行线程
self.command_thread = SSHCommandThread(device_id, self.ssh_connections[device_id], command)
self.command_thread.command_finished.connect(self.on_single_command_finished)
self.log_message(f"在 {selected_device} 上执行: {command}")
self.update_output(f"\n=== {selected_device} 执行: {command} ===")
self.execute_btn.setEnabled(False)
self.running_threads.append(self.command_thread)
self.command_thread.start()
# 清空命令输入
self.cmd_entry.clear()
def execute_preset_command(self, command):
"""执行预设命令"""
if not command:
return
selected_device = self.device_combo.currentText()
if not selected_device:
QMessageBox.warning(self, "警告", "请选择设备")
return
# 查找对应的设备ID
device_id = None
for dev_id, dev_info in self.ip_history.items():
if f"{dev_info.get('username', '')}@{dev_info.get('ip', '')}" == selected_device:
device_id = dev_id
break
if not device_id or device_id not in self.ssh_connections:
QMessageBox.critical(self, "错误", "设备未连接")
return
# 停止当前正在运行的批量命令
self.stop_all_threads()
self.command_queue.clear()
# 将命令显示在输入框中
self.cmd_entry.setText(command)
# 创建命令执行线程
self.command_thread = SSHCommandThread(device_id, self.ssh_connections[device_id], command)
self.command_thread.command_finished.connect(self.on_single_command_finished)
self.log_message(f"在 {selected_device} 上执行预设命令: {command}")
self.update_output(f"\n=== {selected_device} 执行: {command} ===")
self.execute_btn.setEnabled(False)
self.running_threads.append(self.command_thread)
self.command_thread.start()
def execute_selected_commands(self):
"""执行选中的命令"""
selected_device = self.device_combo.currentText()
if not selected_device:
QMessageBox.warning(self, "警告", "请选择设备")
return
# 查找对应的设备ID
device_id = None
for dev_id, dev_info in self.ip_history.items():
if f"{dev_info.get('username', '')}@{dev_info.get('ip', '')}" == selected_device:
device_id = dev_id
break
if not device_id or device_id not in self.ssh_connections:
QMessageBox.critical(self, "错误", "设备未连接")
return
# 获取选中的命令
selected_commands = []
for cmd_name, (checkbox, command) in self.command_checkboxes.items():
if checkbox.isChecked():
selected_commands.append((cmd_name, command))
self.log_message(f"已选中命令: {cmd_name}")
if not selected_commands:
QMessageBox.warning(self, "警告", "请至少选择一个命令")
self.log_message("执行失败: 没有选中任何命令")
return
# 停止所有正在运行的线程
self.stop_all_threads()
# 设置命令队列
self.command_queue = selected_commands.copy()
self.current_device_id = device_id
# 开始执行命令队列
self.log_message(f"在 {selected_device} 上顺序执行 {len(selected_commands)} 个选中命令")
self.update_output(f"\n=== {selected_device} 批量执行 {len(selected_commands)} 个命令 ===")
# 执行第一个命令
self.execute_next_command_in_queue()
def stop_all_threads(self):
"""停止所有正在运行的线程"""
for thread in self.running_threads:
try:
if thread.isRunning():
thread.terminate()
thread.wait(1000) # 等待最多1秒
except:
pass
self.running_threads.clear()
self.log_message("已停止所有正在运行的线程")
def stop_all_commands(self):
"""停止所有命令执行"""
self.stop_all_threads()
self.command_queue.clear()
self.execute_btn.setEnabled(True)
self.log_message("用户手动停止所有命令执行")
self.update_output("\n=== 用户手动停止执行 ===")
def clear_log(self):
"""清空操作日志"""
self.log_text.clear()
self.log_message("日志已清空")
def clear_output(self):
"""清空命令输出"""
self.output_text.clear()
self.update_output("=== 输出已清空 ===")
def execute_next_command_in_queue(self):
"""执行队列中的下一个命令"""
if not self.command_queue:
self.log_message("所有命令执行完成")
self.update_output("\n=== 所有命令执行完成 ===")
return
if not self.current_device_id or self.current_device_id not in self.ssh_connections:
self.log_message("设备连接已断开,停止执行")
self.command_queue.clear()
return
# 获取下一个命令
cmd_name, command = self.command_queue.pop(0)
self.log_message(f"执行: {cmd_name} ({command})")
self.update_output(f"\n--- 执行 {cmd_name} ---")
# 创建命令执行线程
thread = SSHCommandThread(self.current_device_id, self.ssh_connections[self.current_device_id], command)
thread.command_finished.connect(self.on_batch_command_finished)
# 保存线程引用
self.running_threads.append(thread)
thread.start()
def select_all_commands(self):
"""全选所有命令"""
count = 0
for cmd_name, (checkbox, command) in self.command_checkboxes.items():
checkbox.setChecked(True)
count += 1
self.log_message(f"已全选所有预设命令,共 {count} 个")
def clear_all_commands(self):
"""清空所有选择"""
count = 0
for cmd_name, (checkbox, command) in self.command_checkboxes.items():
checkbox.setChecked(False)
count += 1
self.log_message(f"已清空所有选择,共 {count} 个")
def on_batch_command_finished(self, device_id, command, output, error):
"""批量命令执行完成处理"""
device_info = self.ip_history.get(device_id, {})
display_name = f"{device_info.get('username', '')}@{device_info.get('ip', '')}"
if output:
self.update_output(output)
if error:
self.update_output(f"错误: {error}")
self.log_message(f"命令执行完成: {command}")
# 清理已完成的线程
sender = self.sender()
if sender in self.running_threads:
self.running_threads.remove(sender)
# 执行下一个命令
self.execute_next_command_in_queue()
def on_single_command_finished(self, device_id, command, output, error):
"""单个命令执行完成处理"""
device_info = self.ip_history.get(device_id, {})
display_name = f"{device_info.get('username', '')}@{device_info.get('ip', '')}"
if output:
self.update_output(output)
if error:
self.update_output(f"错误: {error}")
self.log_message(f"命令执行完成: {command}")
# 清理已完成的线程
sender = self.sender()
if sender in self.running_threads:
self.running_threads.remove(sender)
# 重新启用执行按钮
self.execute_btn.setEnabled(True)
def on_command_finished(self, device_id, command, output, error):
"""命令执行完成处理"""
device_info = self.ip_history.get(device_id, {})
display_name = f"{device_info.get('username', '')}@{device_info.get('ip', '')}"
if output:
self.update_output(output)
if error:
self.update_output(f"错误: {error}")
self.log_message(f"命令执行完成: {command}")
self.execute_btn.setEnabled(True)
def execute_all_command(self):
"""在所有设备上执行命令"""
command = self.cmd_entry.text().strip()
if not command:
QMessageBox.warning(self, "警告", "请输入命令")
return
if not self.ssh_connections:
QMessageBox.warning(self, "警告", "没有连接的设备")
return
# 停止当前正在运行的命令
self.stop_all_threads()
self.command_queue.clear()
self.log_message("开始在所有设备上执行命令")
# 在所有设备上执行
for device_id, ssh_connection in self.ssh_connections.items():
device_info = self.ip_history.get(device_id, {})
display_name = f"{device_info.get('username', '')}@{device_info.get('ip', '')}"
self.log_message(f"在 {display_name} 上执行: {command}")
self.update_output(f"\n=== {display_name} 执行: {command} ===")
# 创建命令执行线程
thread = SSHCommandThread(device_id, ssh_connection, command)
thread.command_finished.connect(self.on_single_command_finished)
self.running_threads.append(thread)
thread.start()
self.log_message("全部设备命令已启动")
# 清空命令输入
self.cmd_entry.clear()
def disconnect_all(self):
"""断开所有连接"""
if not self.ssh_connections:
QMessageBox.information(self, "提示", "没有活动连接")
return
for device_id, ssh_connection in self.ssh_connections.items():
try:
ssh_connection.close()
self.log_message(f"断开连接: {device_id}")
except:
pass
self.ssh_connections.clear()
self.refresh_history_display()
QMessageBox.information(self, "成功", "所有连接已断开")
def on_history_double_click(self, item, column):
"""历史设备双击事件"""
self.connect_selected_device()
def show_context_menu(self, position):
"""显示右键菜单"""
item = self.history_tree.itemAt(position)
if item:
self.history_tree.setCurrentItem(item)
menu = QMenu(self)
menu.addAction("连接", self.connect_selected_device)
menu.addAction("断开", self.disconnect_selected_device)
menu.addAction("删除", self.delete_selected_device)
menu.exec_(self.history_tree.mapToGlobal(position))
def connect_selected_device(self):
"""连接选中的设备"""
current_item = self.history_tree.currentItem()
if not current_item:
return
device_text = current_item.text(0)
# 解析设备信息
try:
# 格式: username@ip:port
if "@" in device_text and ":" in device_text:
user_host, port = device_text.split(":")
username, ip = user_host.split("@")
# 填充到输入框
self.ip_entry.setText(ip)
self.port_entry.setText(port)
self.username_entry.setText(username)
# 连接
self.connect_device()
except:
QMessageBox.critical(self, "错误", "解析设备信息失败")
def disconnect_selected_device(self):
"""断开选中的设备"""
current_item = self.history_tree.currentItem()
if not current_item:
return
device_text = current_item.text(0)
# 查找对应的device_id
device_id = None
for dev_id, dev_info in self.ip_history.items():
if f"{dev_info.get('username', '')}@{dev_info.get('ip', '')}:{dev_info.get('port', '22')}" == device_text:
device_id = dev_id
break
if device_id and device_id in self.ssh_connections:
try:
self.ssh_connections[device_id].close()
del self.ssh_connections[device_id]
self.log_message(f"断开连接: {device_id}")
self.refresh_history_display()
except Exception as e:
QMessageBox.critical(self, "错误", f"断开连接失败: {e}")
else:
QMessageBox.information(self, "提示", "设备未连接")
def delete_selected_device(self):
"""删除选中的设备"""
current_item = self.history_tree.currentItem()
if not current_item:
return
device_text = current_item.text(0)
reply = QMessageBox.question(self, "确认", f"确定要删除设备 {device_text} 吗?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
# 查找对应的device_id
device_id = None
for dev_id, dev_info in self.ip_history.items():
if f"{dev_info.get('username', '')}@{dev_info.get('ip', '')}:{dev_info.get('port', '22')}" == device_text:
device_id = dev_id
break
if device_id:
# 先断开连接
if device_id in self.ssh_connections:
try:
self.ssh_connections[device_id].close()
del self.ssh_connections[device_id]
except:
pass
# 从历史记录中删除
del self.ip_history[device_id]
self.save_ip_history()
self.refresh_history_display()
self.log_message(f"删除设备: {device_text}")
def closeEvent(self, event):
"""程序关闭时的处理"""
# 停止所有线程
self.stop_all_threads()
# 断开所有连接
for ssh_connection in self.ssh_connections.values():
try:
ssh_connection.close()
except:
pass
event.accept()
主函数代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SSH多设备管理工具 - 程序入口
"""
import sys
from PyQt5.QtWidgets import QApplication
from ssh_multi_tool import SSHMultiTool
def main():
app = QApplication(sys.argv)
app.setApplicationName("SSH多设备管理工具")
window = SSHMultiTool()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
运行效果


浙公网安备 33010602011771号