2025-12-12 18:04:57 +08:00
|
|
|
|
"""
|
|
|
|
|
|
WorkerNode 业务逻辑服务层(Service)
|
|
|
|
|
|
|
|
|
|
|
|
负责 Worker 节点相关的业务逻辑处理
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from apps.engine.repositories import DjangoWorkerRepository
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WorkerService:
|
|
|
|
|
|
"""Worker 节点业务逻辑服务"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
|
"""初始化服务,注入 Repository 依赖"""
|
|
|
|
|
|
self.repo = DjangoWorkerRepository()
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 查询 ====================
|
|
|
|
|
|
|
|
|
|
|
|
def get_worker(self, worker_id: int):
|
|
|
|
|
|
"""根据 ID 获取 Worker 节点"""
|
|
|
|
|
|
return self.repo.get_by_id(worker_id)
|
|
|
|
|
|
|
|
|
|
|
|
def get_all_workers(self):
|
|
|
|
|
|
"""获取所有 Worker 节点查询集"""
|
|
|
|
|
|
return self.repo.get_all()
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 状态更新 ====================
|
|
|
|
|
|
|
|
|
|
|
|
def update_status(self, worker_id: int, status: str) -> bool:
|
|
|
|
|
|
"""更新 Worker 节点状态
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
worker_id: Worker ID
|
|
|
|
|
|
status: 状态 (pending/deploying/online/offline)
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.repo.update_status(worker_id, status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_worker(self, worker_id: int) -> bool:
|
|
|
|
|
|
"""删除 Worker 节点"""
|
|
|
|
|
|
return self.repo.delete_by_id(worker_id)
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 自注册 ====================
|
|
|
|
|
|
|
|
|
|
|
|
def register_worker(self, name: str, is_local: bool = True):
|
|
|
|
|
|
"""
|
|
|
|
|
|
注册 Worker 节点(本地 Worker 自注册用)
|
|
|
|
|
|
|
|
|
|
|
|
幂等操作:已存在则返回现有节点。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
name: Worker 名称
|
|
|
|
|
|
is_local: 是否为本地节点
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(WorkerNode, created) 元组
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.repo.get_or_create_by_name(
|
|
|
|
|
|
name=name,
|
|
|
|
|
|
is_local=is_local
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def remote_uninstall(
|
|
|
|
|
|
self,
|
|
|
|
|
|
worker_id: int,
|
|
|
|
|
|
ip_address: str,
|
|
|
|
|
|
ssh_port: int,
|
|
|
|
|
|
username: str,
|
|
|
|
|
|
password: str | None
|
|
|
|
|
|
) -> tuple[bool, str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
在远程主机上执行卸载脚本
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
worker_id: Worker ID(仅用于日志)
|
|
|
|
|
|
ip_address: SSH 主机地址
|
|
|
|
|
|
ssh_port: SSH 端口
|
|
|
|
|
|
username: SSH 用户名
|
|
|
|
|
|
password: SSH 密码
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(success, message) 元组
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not password:
|
|
|
|
|
|
return False, "未配置 SSH 密码,跳过远程卸载"
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import paramiko
|
|
|
|
|
|
from apps.engine.services.deploy_service import get_uninstall_script
|
|
|
|
|
|
|
|
|
|
|
|
ssh = paramiko.SSHClient()
|
|
|
|
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[卸载] 正在连接 {ip_address}...")
|
|
|
|
|
|
ssh.connect(
|
|
|
|
|
|
ip_address,
|
|
|
|
|
|
port=ssh_port,
|
|
|
|
|
|
username=username,
|
|
|
|
|
|
password=password,
|
|
|
|
|
|
timeout=30
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 上传卸载脚本
|
|
|
|
|
|
uninstall_script = get_uninstall_script()
|
|
|
|
|
|
remote_script_path = '/tmp/xingrin_uninstall.sh'
|
|
|
|
|
|
|
|
|
|
|
|
sftp = ssh.open_sftp()
|
|
|
|
|
|
with sftp.file(remote_script_path, 'w') as f:
|
|
|
|
|
|
f.write(uninstall_script)
|
|
|
|
|
|
sftp.chmod(remote_script_path, 0o755)
|
|
|
|
|
|
sftp.close()
|
|
|
|
|
|
|
|
|
|
|
|
# 执行卸载脚本
|
|
|
|
|
|
logger.info(f"[卸载] 正在执行卸载脚本...")
|
|
|
|
|
|
stdin, stdout, stderr = ssh.exec_command(f"bash {remote_script_path}")
|
|
|
|
|
|
exit_status = stdout.channel.recv_exit_status()
|
|
|
|
|
|
|
|
|
|
|
|
ssh.close()
|
|
|
|
|
|
|
|
|
|
|
|
if exit_status == 0:
|
|
|
|
|
|
logger.info(f"[卸载] Worker {worker_id} 远程卸载成功")
|
|
|
|
|
|
return True, "远程卸载成功"
|
|
|
|
|
|
else:
|
|
|
|
|
|
error = stderr.read().decode().strip()
|
|
|
|
|
|
logger.warning(f"[卸载] Worker {worker_id} 远程卸载失败: {error}")
|
|
|
|
|
|
return False, f"远程卸载失败: {error}"
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"[卸载] Worker {worker_id} 远程卸载异常: {e}")
|
|
|
|
|
|
return False, f"远程卸载异常: {str(e)}"
|
|
|
|
|
|
|
2025-12-19 19:48:01 +08:00
|
|
|
|
def execute_remote_command(
|
|
|
|
|
|
self,
|
|
|
|
|
|
ip_address: str,
|
|
|
|
|
|
ssh_port: int,
|
|
|
|
|
|
username: str,
|
|
|
|
|
|
password: str | None,
|
|
|
|
|
|
command: str
|
|
|
|
|
|
) -> tuple[bool, str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
在远程主机上执行命令
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
ip_address: SSH 主机地址
|
|
|
|
|
|
ssh_port: SSH 端口
|
|
|
|
|
|
username: SSH 用户名
|
|
|
|
|
|
password: SSH 密码
|
|
|
|
|
|
command: 要执行的命令
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(success, message) 元组
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not password:
|
|
|
|
|
|
return False, "未配置 SSH 密码"
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import paramiko
|
|
|
|
|
|
|
|
|
|
|
|
ssh = paramiko.SSHClient()
|
|
|
|
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
|
|
|
|
|
|
|
|
|
ssh.connect(
|
|
|
|
|
|
ip_address,
|
|
|
|
|
|
port=ssh_port,
|
|
|
|
|
|
username=username,
|
|
|
|
|
|
password=password,
|
|
|
|
|
|
timeout=30
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
stdin, stdout, stderr = ssh.exec_command(command, timeout=120)
|
|
|
|
|
|
exit_status = stdout.channel.recv_exit_status()
|
|
|
|
|
|
|
|
|
|
|
|
ssh.close()
|
|
|
|
|
|
|
|
|
|
|
|
if exit_status == 0:
|
|
|
|
|
|
return True, stdout.read().decode().strip()
|
|
|
|
|
|
else:
|
|
|
|
|
|
error = stderr.read().decode().strip()
|
|
|
|
|
|
return False, error
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
|
2025-12-12 18:04:57 +08:00
|
|
|
|
|
|
|
|
|
|
__all__ = ["WorkerService"]
|