8ccccf8f52
主要功能: - 多主机管理: 支持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版本兼容性修复
180 lines
6.2 KiB
Python
180 lines
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)}
|