Files
kvm-manager/backend/app/main.py
T
admin 8ccccf8f52 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版本兼容性修复
2026-05-07 12:41:10 +08:00

182 regels
6.4 KiB
Python

"""FastAPI 主应用"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from app.config import settings
from app.routers import vm, storage, network, snapshot, monitor, auth, host as host_router
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="KVM 虚拟化管理平台 API",
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(auth.router, prefix=f"{settings.API_PREFIX}/auth", tags=["认证"])
app.include_router(host_router.router, prefix=f"{settings.API_PREFIX}/hosts", tags=["主机管理"])
app.include_router(vm.router, prefix=f"{settings.API_PREFIX}/vm", tags=["虚拟机管理"])
app.include_router(storage.router, prefix=f"{settings.API_PREFIX}/storage", tags=["存储管理"])
app.include_router(network.router, prefix=f"{settings.API_PREFIX}/network", tags=["网络管理"])
app.include_router(snapshot.router, prefix=f"{settings.API_PREFIX}/snapshot", tags=["快照管理"])
app.include_router(monitor.router, prefix=f"{settings.API_PREFIX}/monitor", tags=["资源监控"])
@app.get("/")
async def root():
return {"name": settings.APP_NAME, "version": settings.APP_VERSION}
@app.get(f"{settings.API_PREFIX}/host")
async def host_info():
"""获取本机宿主机信息(兼容旧接口)"""
from app.libvirt_conn import libvirt_conn
return libvirt_conn.get_host_info()
@app.get("/health")
async def health():
"""健康检查"""
from app.libvirt_conn import conn_pool
try:
conn = conn_pool.get_conn("local")
return {"status": "ok", "libvirt": conn.isAlive()}
except Exception as e:
return {"status": "error", "message": str(e)}
# VNC WebSocket 代理
from starlette.websockets import WebSocket
from starlette.responses import HTMLResponse
import asyncio
import socket
import struct
@app.websocket("/ws/vnc/{vm_name}")
async def vnc_websocket(websocket: WebSocket, vm_name: str, host_id: str = "local"):
"""WebSocket 代理到虚拟机 VNC"""
await websocket.accept()
from app.libvirt_conn import conn_pool
from app.hosts import get_host as get_host_info
from lxml import etree
try:
conn = conn_pool.get_conn(host_id)
dom = conn.lookupByName(vm_name)
xml_desc = dom.XMLDesc(0)
tree = etree.fromstring(xml_desc.encode())
# 获取 VNC 端口
graphics = tree.find(".//graphics[@type='vnc']")
if graphics is None:
await websocket.close(code=1000, reason="虚拟机没有 VNC 配置")
return
vnc_port = int(graphics.get("port", -1))
vnc_listen = graphics.get("listen", "127.0.0.1")
if vnc_port <= 0:
await websocket.close(code=1000, reason="VNC 端口未分配,虚拟机可能未运行")
return
# 根据主机类型决定 VNC 连接目标
host_info = get_host_info(host_id)
target_host = vnc_listen
is_remote = host_info and host_info.type != "local"
if is_remote:
from urllib.parse import urlparse
parsed = urlparse(host_info.uri)
remote_host = parsed.hostname or "127.0.0.1"
if vnc_listen in ("127.0.0.1", "localhost", ""):
# VNC 只监听本地回环,需要 SSH 隧道
if host_info.type == "ssh":
import subprocess, time
local_port = 20000 + (hash(vm_name + str(vnc_port)) % 10000)
# 先杀掉可能存在的旧隧道
subprocess.run(
["fuser", "-k", f"{local_port}/tcp"],
capture_output=True, timeout=3,
)
ssh_args = ["ssh", "-f", "-N"]
if host_info.ssh_key_path:
ssh_args.extend(["-i", host_info.ssh_key_path])
ssh_args.extend([
"-o", "StrictHostKeyChecking=no",
"-o", "ServerAliveInterval=30",
"-L", f"127.0.0.1:{local_port}:127.0.0.1:{vnc_port}",
remote_host,
])
result = subprocess.run(ssh_args, capture_output=True, timeout=10)
if result.returncode != 0:
err = result.stderr.decode(errors='replace').strip()
await websocket.close(code=1011, reason=f"SSH隧道建立失败: {err}")
return
target_host = "127.0.0.1"
vnc_port = local_port
else:
# TCP 模式但 VNC 只听 localhost,无法直接连
await websocket.close(code=1011, reason="远程主机 VNC 监听在 localhost,需要使用 SSH 模式")
return
elif vnc_listen == "0.0.0.0":
# VNC 监听所有接口,直接连远程主机 IP
target_host = remote_host
else:
# VNC 监听特定地址
target_host = vnc_listen
else:
# 本地主机
if vnc_listen in ("0.0.0.0", ""):
target_host = "127.0.0.1"
# else 保持 vnc_listen (127.0.0.1 或其他)
# 连接到 VNC 服务器
reader, writer = await asyncio.open_connection(target_host, vnc_port)
async def ws_to_vnc():
"""WebSocket -> VNC"""
try:
while True:
data = await websocket.receive_bytes()
writer.write(data)
await writer.drain()
except Exception:
pass
finally:
writer.close()
async def vnc_to_ws():
"""VNC -> WebSocket"""
try:
while True:
data = await reader.read(4096)
if not data:
break
await websocket.send_bytes(data)
except Exception:
pass
finally:
try:
await websocket.close()
except Exception:
pass
await asyncio.gather(ws_to_vnc(), vnc_to_ws())
except Exception as e:
try:
await websocket.close(code=1011, reason=str(e))
except Exception:
pass