파일
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

180 라인
6.2 KiB
Python

"""存储池管理路由"""
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional
from lxml import etree
import os
from app.libvirt_conn import conn_pool
import libvirt
router = APIRouter()
class PoolCreate(BaseModel):
name: str = Field(..., description="存储池名称")
path: str = Field(..., description="存储池路径")
type: str = Field("dir", description="存储池类型: dir/fs/logical")
class VolCreate(BaseModel):
name: str = Field(..., description="卷名称")
capacity_gb: int = Field(20, description="容量(GB)")
format: str = Field("qcow2", description="格式: qcow2/raw")
@router.get("/pools")
async def list_pools(host_id: str = Query("local")):
"""列出所有存储池"""
conn = conn_pool.get_conn(host_id)
pools = conn.listAllStoragePools(0)
result = []
for pool in pools:
info = pool.info()
xml = etree.fromstring(pool.XMLDesc(0).encode())
target = xml.find(".//target/path")
result.append({
"name": pool.name(),
"state": ["inactive", "building", "running", "degraded", "inaccessible"][info[0]] if info[0] < 5 else "unknown",
"capacity_gb": round(info[1] / (1024**3), 2),
"allocation_gb": round(info[2] / (1024**3), 2),
"available_gb": round(info[3] / (1024**3), 2),
"path": target.text if target is not None else "",
"autostart": pool.autostart() == 1,
"persistent": pool.isPersistent() == 1,
})
return {"pools": result, "total": len(result)}
@router.get("/pool/{name}")
async def get_pool(name: str, host_id: str = Query("local")):
"""获取存储池详情"""
conn = conn_pool.get_conn(host_id)
try:
pool = conn.storagePoolLookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"存储池 '{name}' 不存在")
info = pool.info()
xml = etree.fromstring(pool.XMLDesc(0).encode())
volumes = []
try:
for vol_name in pool.listVolumes():
vol = pool.storageVolLookupByName(vol_name)
vol_info = vol.info()
volumes.append({
"name": vol_name,
"path": vol.path(),
"type": vol_info[0],
"capacity_gb": round(vol_info[1] / (1024**3), 2),
"allocation_gb": round(vol_info[2] / (1024**3), 2),
})
except Exception:
pass
target = xml.find(".//target/path")
return {
"name": pool.name(),
"state": ["inactive", "building", "running", "degraded", "inaccessible"][info[0]] if info[0] < 5 else "unknown",
"capacity_gb": round(info[1] / (1024**3), 2),
"allocation_gb": round(info[2] / (1024**3), 2),
"available_gb": round(info[3] / (1024**3), 2),
"path": target.text if target is not None else "",
"autostart": pool.autostart() == 1,
"volumes": volumes,
"volume_count": len(volumes),
}
@router.post("/pool/create")
async def create_pool(pool: PoolCreate, host_id: str = Query("local")):
"""创建存储池"""
with conn_pool.get_rw(host_id) as rw_conn:
xml = f"""<pool type='{pool.type}'>
<name>{pool.name}</name>
<target>
<path>{pool.path}</path>
</target>
</pool>"""
try:
os.makedirs(pool.path, exist_ok=True)
p = rw_conn.storagePoolDefineXML(xml, 0)
p.setAutostart(1)
p.create(0)
return {"message": f"存储池 '{pool.name}' 创建成功"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"创建存储池失败: {str(e)}")
@router.delete("/pool/{name}")
async def delete_pool(name: str, host_id: str = Query("local")):
"""删除存储池"""
with conn_pool.get_rw(host_id) as rw_conn:
try:
pool = rw_conn.storagePoolLookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"存储池 '{name}' 不存在")
if pool.info()[0] == libvirt.VIR_STORAGE_POOL_RUNNING:
pool.destroy()
pool.undefine()
return {"message": f"存储池 '{name}' 已删除"}
@router.post("/pool/{name}/volume")
async def create_volume(name: str, vol: VolCreate, host_id: str = Query("local")):
"""在存储池中创建卷"""
with conn_pool.get_rw(host_id) as rw_conn:
try:
pool = rw_conn.storagePoolLookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"存储池 '{name}' 不存在")
vol_xml = f"""<volume>
<name>{vol.name}</name>
<capacity unit='GiB'>{vol.capacity_gb}</capacity>
<allocation unit='GiB'>1</allocation>
<target>
<format type='{vol.format}'/>
</target>
</volume>"""
try:
pool.createXML(vol_xml, 0)
return {"message": f"'{vol.name}' 创建成功"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"创建卷失败: {str(e)}")
@router.delete("/pool/{pool_name}/volume/{vol_name}")
async def delete_volume(pool_name: str, vol_name: str, host_id: str = Query("local")):
"""删除卷"""
with conn_pool.get_rw(host_id) as rw_conn:
try:
pool = rw_conn.storagePoolLookupByName(pool_name)
vol = pool.storageVolLookupByName(vol_name)
vol.delete(0)
return {"message": f"'{vol_name}' 已删除"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"删除卷失败: {str(e)}")
@router.get("/isos")
async def list_isos(host_id: str = Query("local")):
"""列出可用的ISO镜像"""
iso_dirs = ["/var/lib/libvirt/iso", "/isos", "/mnt/isos"]
isos = []
for d in iso_dirs:
if os.path.isdir(d):
for f in os.listdir(d):
if f.lower().endswith(".iso"):
fp = os.path.join(d, f)
size = os.path.getsize(fp)
isos.append({
"name": f,
"path": fp,
"size_gb": round(size / (1024**3), 2),
})
return {"isos": isos, "total": len(isos)}