feat: 多主机纳管、用户认证、noVNC控制台、深色主题

主要功能:
- 多主机管理: 支持TCP/SSH方式纳管远程KVM主机
- 用户认证: JWT token认证, 默认admin/admin123
- noVNC控制台: 前端集成noVNC, WebSocket代理VNC连接
- 深色主题: 全局Element Plus深色主题覆盖
- 虚拟机操作: 克隆、迁移、XML编辑、快照管理
- 资源监控: CPU/内存/磁盘IO/网络流量实时监控

Bug修复:
- libvirt getInfo()内存单位修正(MiB非KiB)
- 远程主机VNC 0.0.0.0监听地址连接策略修复
- Dashboard定时器内存泄漏修复
- bcrypt版本兼容性修复
This commit is contained in:
admin
2026-05-07 12:41:10 +08:00
parent fac8ab7470
commit 8ccccf8f52
30 changed files with 1972 additions and 170 deletions
+175
View File
@@ -0,0 +1,175 @@
"""主机注册表 - 管理多台 libvirt 宿主机"""
import json
import os
import time
import uuid
import logging
from typing import Optional
from pydantic import BaseModel, Field
from app.config import settings
logger = logging.getLogger(__name__)
# 数据存储目录
DATA_DIR = os.environ.get("KVM_DATA_DIR", "/var/lib/kvm-manager")
HOSTS_FILE = os.path.join(DATA_DIR, "hosts.json")
class HostInfo(BaseModel):
"""主机信息模型"""
id: str = Field(..., description="主机唯一ID")
name: str = Field(..., description="主机名称")
uri: str = Field(..., description="libvirt 连接 URI")
type: str = Field("local", description="连接类型: local/tcp/ssh")
ssh_key_path: Optional[str] = Field(None, description="SSH 私钥路径(ssh 模式)")
status: str = "unknown"
created_at: float = Field(default_factory=time.time)
last_seen: Optional[float] = None
class HostCreate(BaseModel):
"""创建主机请求"""
name: str = Field(..., description="主机名称")
uri: str = Field(..., description="libvirt 连接 URI,如 qemu+tcp://192.168.1.2/system")
ssh_key_path: Optional[str] = Field(None, description="SSH 私钥路径(ssh 模式)")
def _detect_type(uri: str) -> str:
"""根据 URI 判断连接类型"""
if uri.startswith("qemu+ssh://"):
return "ssh"
elif uri.startswith("qemu+tcp://"):
return "tcp"
return "local"
def _ensure_data_dir():
"""确保数据目录存在"""
os.makedirs(DATA_DIR, exist_ok=True)
def _load_hosts() -> dict:
"""从文件加载主机列表"""
if not os.path.exists(HOSTS_FILE):
return {}
try:
with open(HOSTS_FILE, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {}
def _save_hosts(data: dict):
"""保存主机列表到文件"""
_ensure_data_dir()
with open(HOSTS_FILE, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def _init_local_host() -> dict:
"""初始化本机默认主机"""
return HostInfo(
id="local",
name="本机",
uri=settings.LIBVIRT_URI,
type="local",
status="unknown",
).model_dump()
def list_hosts() -> list[HostInfo]:
"""列出所有已注册主机"""
data = _load_hosts()
if not data:
# 首次运行,初始化本机
local = _init_local_host()
data["local"] = local
_save_hosts(data)
return [HostInfo(**h) for h in data.values()]
def get_host(host_id: str) -> Optional[HostInfo]:
"""获取单个主机信息"""
data = _load_hosts()
if host_id not in data:
return None
return HostInfo(**data[host_id])
def add_host(req: HostCreate) -> HostInfo:
"""添加新主机"""
data = _load_hosts()
if not data:
data["local"] = _init_local_host()
host_id = req.name.lower().replace(" ", "-").replace(".", "-")
# 确保ID唯一
if host_id in data:
host_id = f"{host_id}-{uuid.uuid4().hex[:6]}"
host_type = _detect_type(req.uri)
# 构建 SSH URI
uri = req.uri
if host_type == "ssh" and req.ssh_key_path:
# 在 URI 中嵌入 key 提示,实际连接时由 libvirt ssh driver 使用
pass
host = HostInfo(
id=host_id,
name=req.name,
uri=uri,
type=host_type,
ssh_key_path=req.ssh_key_path,
status="unknown",
created_at=time.time(),
)
data[host_id] = host.model_dump()
_save_hosts(data)
return host
def remove_host(host_id: str) -> bool:
"""删除主机(local 不可删)"""
if host_id == "local":
return False
data = _load_hosts()
if host_id not in data:
return False
del data[host_id]
_save_hosts(data)
return True
def update_host_status(host_id: str, status: str):
"""更新主机在线状态"""
data = _load_hosts()
if host_id in data:
data[host_id]["status"] = status
data[host_id]["last_seen"] = time.time()
_save_hosts(data)
def test_connection(uri: str) -> dict:
"""测试 libvirt 连接是否可用"""
import libvirt
try:
conn = libvirt.openReadOnly(uri)
if conn:
info = conn.getInfo()
result = {
"success": True,
"hostname": conn.getHostname(),
"hypervisor": conn.getType(),
"cpu_cores": info[2],
"memory_mb": info[1], # getInfo()[1] 已经是 MiB 单位
"libvirt_version": conn.getLibVersion(),
}
conn.close()
return result
else:
return {"success": False, "error": "无法建立连接"}
except libvirt.libvirtError as e:
return {"success": False, "error": str(e)}
except Exception as e:
return {"success": False, "error": str(e)}