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
+96
View File
@@ -0,0 +1,96 @@
"""认证路由"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.auth import (
authenticate_user, create_access_token, get_password_hash,
get_current_user, User, UserCreate, PasswordChange, Token,
get_users_db, verify_password,
)
router = APIRouter()
@router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""用户登录,获取 JWT Token"""
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user["username"], "role": user["role"]})
return Token(
access_token=access_token,
username=user["username"],
role=user["role"],
)
@router.get("/me", response_model=User)
async def get_me(current_user: User = Depends(get_current_user)):
"""获取当前用户信息"""
return current_user
@router.post("/change-password")
async def change_password(
pwd: PasswordChange,
current_user: User = Depends(get_current_user),
):
"""修改密码"""
db = get_users_db()
user = db.get(current_user.username)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if not verify_password(pwd.old_password, user["hashed_password"]):
raise HTTPException(status_code=400, detail="旧密码错误")
user["hashed_password"] = get_password_hash(pwd.new_password)
return {"message": "密码修改成功"}
@router.post("/register")
async def register(user_create: UserCreate, current_user: User = Depends(get_current_user)):
"""注册新用户(仅管理员)"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可注册新用户")
db = get_users_db()
if user_create.username in db:
raise HTTPException(status_code=400, detail="用户名已存在")
db[user_create.username] = {
"username": user_create.username,
"hashed_password": get_password_hash(user_create.password),
"role": user_create.role,
}
return {"message": f"用户 '{user_create.username}' 创建成功"}
@router.get("/users")
async def list_users(current_user: User = Depends(get_current_user)):
"""列出所有用户(仅管理员)"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可查看用户列表")
db = get_users_db()
return {
"users": [
{"username": u["username"], "role": u["role"]}
for u in db.values()
]
}
@router.delete("/users/{username}")
async def delete_user(username: str, current_user: User = Depends(get_current_user)):
"""删除用户(仅管理员,不能删除自己)"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可删除用户")
if username == current_user.username:
raise HTTPException(status_code=400, detail="不能删除自己")
db = get_users_db()
if username not in db:
raise HTTPException(status_code=404, detail="用户不存在")
del db[username]
return {"message": f"用户 '{username}' 已删除"}
+116
View File
@@ -0,0 +1,116 @@
"""主机管理路由"""
from fastapi import APIRouter, HTTPException
from app.hosts import (
list_hosts, get_host, add_host, remove_host,
test_connection, update_host_status, HostCreate,
)
from app.libvirt_conn import conn_pool
import time
router = APIRouter()
@router.get("/list")
async def api_list_hosts():
"""列出所有已注册主机"""
hosts = list_hosts()
result = []
for h in hosts:
info = h.model_dump()
# 尝试获取实时状态
try:
mgr = conn_pool.get(h.id)
conn = mgr.conn
alive = conn.isAlive()
info["status"] = "online" if alive else "offline"
if alive:
host_data = conn.getInfo()
info["cpu_cores"] = host_data[2]
info["memory_mb"] = host_data[1] # getInfo()[1] 已经是 MiB 单位
info["hostname"] = conn.getHostname()
# 虚拟机数
domains = conn.listAllDomains(0)
info["vm_total"] = len(domains)
info["vm_running"] = sum(1 for d in domains if d.isActive())
update_host_status(h.id, info["status"])
except Exception:
info["status"] = "offline"
update_host_status(h.id, "offline")
result.append(info)
return {"hosts": result, "total": len(result)}
@router.get("/detail/{host_id}")
async def api_get_host(host_id: str):
"""获取单台主机详情"""
host = get_host(host_id)
if not host:
raise HTTPException(status_code=404, detail=f"主机 '{host_id}' 不存在")
info = host.model_dump()
try:
mgr = conn_pool.get(host_id)
info["host_info"] = mgr.get_host_info()
info["status"] = "online"
# 虚拟机数
domains = mgr.conn.listAllDomains(0)
info["vm_total"] = len(domains)
info["vm_running"] = sum(1 for d in domains if d.isActive())
update_host_status(host_id, "online")
except Exception as e:
info["status"] = "offline"
info["error"] = str(e)
update_host_status(host_id, "offline")
return info
@router.post("/add")
async def api_add_host(req: HostCreate):
"""添加新主机"""
# 先测试连接
result = test_connection(req.uri)
if not result["success"]:
raise HTTPException(status_code=400, detail=f"连接测试失败: {result['error']}")
host = add_host(req)
# 更新状态为在线
update_host_status(host.id, "online")
return {"message": f"主机 '{host.name}' 添加成功", "host": host.model_dump()}
@router.delete("/delete/{host_id}")
async def api_delete_host(host_id: str):
"""删除主机"""
if host_id == "local":
raise HTTPException(status_code=400, detail="不能删除本机")
if not remove_host(host_id):
raise HTTPException(status_code=404, detail=f"主机 '{host_id}' 不存在")
conn_pool.remove(host_id)
return {"message": f"主机 '{host_id}' 已删除"}
@router.post("/test")
async def api_test_connection(req: HostCreate):
"""测试连接(不添加主机)"""
result = test_connection(req.uri)
return result
@router.post("/refresh/{host_id}")
async def api_refresh_host(host_id: str):
"""刷新主机状态"""
host = get_host(host_id)
if not host:
raise HTTPException(status_code=404, detail=f"主机 '{host_id}' 不存在")
try:
# 重新建立连接
conn_pool.remove(host_id)
mgr = conn_pool.get(host_id)
conn = mgr.conn
alive = conn.isAlive()
status = "online" if alive else "offline"
update_host_status(host_id, status)
return {"status": status, "hostname": conn.getHostname()}
except Exception as e:
update_host_status(host_id, "offline")
conn_pool.remove(host_id)
return {"status": "offline", "error": str(e)}
+10 -25
View File
@@ -1,6 +1,6 @@
"""资源监控路由"""
from fastapi import APIRouter, HTTPException
from app.libvirt_conn import libvirt_conn
from fastapi import APIRouter, HTTPException, Query
from app.libvirt_conn import conn_pool
import libvirt
import time
import threading
@@ -13,23 +13,19 @@ _cache_lock = threading.Lock()
@router.get("/overview")
async def monitor_overview():
async def monitor_overview(host_id: str = Query("local")):
"""宿主机总览监控"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
# 宿主机信息
host_info = conn.getInfo()
hostname = conn.getHostname()
# CPU 使用率(通过 node info
cpu_stats = conn.getCPUStats(-1, 0) # 全局 CPU 统计
cpu_stats = conn.getCPUStats(-1, 0)
cpu_total = cpu_stats.get("user", 0) + cpu_stats.get("system", 0) + cpu_stats.get("idle", 0)
cpu_used = cpu_stats.get("user", 0) + cpu_stats.get("system", 0)
cpu_percent = round(cpu_used / cpu_total * 100, 1) if cpu_total > 0 else 0
# 内存
memory_total_kb = host_info[1]
# 获取可用内存
try:
with open("/proc/meminfo", "r") as f:
meminfo = {}
@@ -48,7 +44,6 @@ async def monitor_overview():
mem_available_mb = mem_total_mb
mem_percent = 0
# 虚拟机统计
domains = conn.listAllDomains(0)
running = sum(1 for d in domains if d.isActive())
stopped = len(domains) - running
@@ -75,9 +70,9 @@ async def monitor_overview():
@router.get("/vm/{name}")
async def monitor_vm(name: str):
async def monitor_vm(name: str, host_id: str = Query("local")):
"""获取虚拟机实时监控数据"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
@@ -86,10 +81,9 @@ async def monitor_vm(name: str):
if not dom.isActive():
return {"name": name, "state": "stopped", "cpu_percent": 0, "memory": {}}
# CPU 百分比
cpu_percent = _get_vm_cpu_percent(dom)
cache_key = f"{host_id}_{name}"
cpu_percent = _get_vm_cpu_percent(dom, cache_key)
# 内存
mem_stats = {}
try:
raw = dom.memoryStats()
@@ -104,10 +98,7 @@ async def monitor_vm(name: str):
except Exception:
pass
# 磁盘IO
disk_stats = _get_vm_disk_stats(dom)
# 网络IO
net_stats = _get_vm_net_stats(dom)
return {
@@ -120,17 +111,13 @@ async def monitor_vm(name: str):
}
def _get_vm_cpu_percent(dom) -> float:
def _get_vm_cpu_percent(dom, cache_key: str) -> float:
"""计算虚拟机 CPU 使用率"""
cache_key = f"cpu_{dom.name()}"
try:
# 第一次采样
info1 = dom.info()
cpu_time1 = info1[2]
t1 = time.time()
# 从缓存获取上一次数据
with _cache_lock:
prev = _stats_cache.get(cache_key)
@@ -138,13 +125,11 @@ def _get_vm_cpu_percent(dom) -> float:
cpu_time0, t0 = prev
elapsed = t1 - t0
cpu_diff = cpu_time1 - cpu_time0
# CPU时间单位是纳秒
cpu_percent = round((cpu_diff / 1e9) / elapsed * 100, 1)
cpu_percent = min(cpu_percent, 100.0)
else:
cpu_percent = 0.0
# 更新缓存
with _cache_lock:
_stats_cache[cache_key] = (cpu_time1, t1)
+13 -20
View File
@@ -1,10 +1,10 @@
"""网络管理路由"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional, List
from lxml import etree
from app.libvirt_conn import libvirt_conn
from app.libvirt_conn import conn_pool
import libvirt
router = APIRouter()
@@ -20,15 +20,14 @@ class NetworkCreate(BaseModel):
@router.get("/list")
async def list_networks():
async def list_networks(host_id: str = Query("local")):
"""列出所有网络"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
networks = conn.listAllNetworks(0)
result = []
for net in networks:
xml = etree.fromstring(net.XMLDesc(0).encode())
# 解析网络信息
forward = xml.find("forward")
mode = forward.get("mode", "isolated") if forward is not None else "isolated"
@@ -39,18 +38,13 @@ async def list_networks():
bridge = xml.find("bridge")
bridge_name = bridge.get("name", "") if bridge is not None else ""
# DHCP范围
dhcp_range = None
dhcp = xml.find(".//dhcp")
if dhcp is not None:
r = dhcp.find("range")
if r is not None:
dhcp_range = {
"start": r.get("start", ""),
"end": r.get("end", ""),
}
dhcp_range = {"start": r.get("start", ""), "end": r.get("end", "")}
# 活跃租约
leases = []
try:
for lease in net.DHCPLeases():
@@ -79,9 +73,9 @@ async def list_networks():
@router.get("/detail/{name}")
async def get_network(name: str):
async def get_network(name: str, host_id: str = Query("local")):
"""获取网络详情"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
net = conn.networkLookupByName(name)
except libvirt.libvirtError:
@@ -92,7 +86,7 @@ async def get_network(name: str):
@router.post("/create")
async def create_network(net: NetworkCreate):
async def create_network(net: NetworkCreate, host_id: str = Query("local")):
"""创建网络"""
if net.mode == "bridge" and not net.bridge:
raise HTTPException(status_code=400, detail="桥接模式必须指定桥接网卡")
@@ -104,7 +98,6 @@ async def create_network(net: NetworkCreate):
<bridge name='{net.bridge}'/>
</network>"""
else:
# NAT或隔离模式
import ipaddress
network = ipaddress.ip_network(net.subnet, strict=False)
gateway = str(network.network_address + 1)
@@ -129,7 +122,7 @@ async def create_network(net: NetworkCreate):
</ip>
</network>"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
n = rw_conn.networkDefineXML(xml)
n.setAutostart(1)
@@ -140,9 +133,9 @@ async def create_network(net: NetworkCreate):
@router.delete("/delete/{name}")
async def delete_network(name: str):
async def delete_network(name: str, host_id: str = Query("local")):
"""删除网络"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
net = rw_conn.networkLookupByName(name)
except libvirt.libvirtError:
@@ -155,9 +148,9 @@ async def delete_network(name: str):
@router.post("/action/{name}")
async def network_action(name: str, action: str):
async def network_action(name: str, action: str, host_id: str = Query("local")):
"""网络操作: start/stop"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
net = rw_conn.networkLookupByName(name)
except libvirt.libvirtError:
+13 -13
View File
@@ -1,10 +1,10 @@
"""快照管理路由"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional
from lxml import etree
from app.libvirt_conn import libvirt_conn
from app.libvirt_conn import conn_pool
import libvirt
router = APIRouter()
@@ -16,9 +16,9 @@ class SnapshotCreate(BaseModel):
@router.get("/list/{vm_name}")
async def list_snapshots(vm_name: str):
async def list_snapshots(vm_name: str, host_id: str = Query("local")):
"""列出虚拟机的所有快照"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(vm_name)
except libvirt.libvirtError:
@@ -40,15 +40,15 @@ async def list_snapshots(vm_name: str):
"is_current": snap.isCurrent() == 1,
})
except libvirt.libvirtError:
pass # 没有快照
pass
return {"vm": vm_name, "snapshots": snapshots, "total": len(snapshots)}
@router.post("/create/{vm_name}")
async def create_snapshot(vm_name: str, snap: SnapshotCreate):
async def create_snapshot(vm_name: str, snap: SnapshotCreate, host_id: str = Query("local")):
"""创建快照"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(vm_name)
except libvirt.libvirtError:
@@ -68,9 +68,9 @@ async def create_snapshot(vm_name: str, snap: SnapshotCreate):
@router.post("/revert/{vm_name}/{snap_name}")
async def revert_snapshot(vm_name: str, snap_name: str):
async def revert_snapshot(vm_name: str, snap_name: str, host_id: str = Query("local")):
"""恢复快照"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(vm_name)
snap = dom.snapshotLookupByName(snap_name)
@@ -85,9 +85,9 @@ async def revert_snapshot(vm_name: str, snap_name: str):
@router.delete("/delete/{vm_name}/{snap_name}")
async def delete_snapshot(vm_name: str, snap_name: str):
async def delete_snapshot(vm_name: str, snap_name: str, host_id: str = Query("local")):
"""删除快照"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(vm_name)
snap = dom.snapshotLookupByName(snap_name)
@@ -102,9 +102,9 @@ async def delete_snapshot(vm_name: str, snap_name: str):
@router.get("/detail/{vm_name}/{snap_name}")
async def get_snapshot_detail(vm_name: str, snap_name: str):
async def get_snapshot_detail(vm_name: str, snap_name: str, host_id: str = Query("local")):
"""获取快照详情"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(vm_name)
snap = dom.snapshotLookupByName(snap_name)
+15 -16
View File
@@ -1,11 +1,11 @@
"""存储池管理路由"""
from fastapi import APIRouter, HTTPException
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 libvirt_conn
from app.libvirt_conn import conn_pool
import libvirt
router = APIRouter()
@@ -24,9 +24,9 @@ class VolCreate(BaseModel):
@router.get("/pools")
async def list_pools():
async def list_pools(host_id: str = Query("local")):
"""列出所有存储池"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
pools = conn.listAllStoragePools(0)
result = []
for pool in pools:
@@ -47,9 +47,9 @@ async def list_pools():
@router.get("/pool/{name}")
async def get_pool(name: str):
async def get_pool(name: str, host_id: str = Query("local")):
"""获取存储池详情"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
pool = conn.storagePoolLookupByName(name)
except libvirt.libvirtError:
@@ -58,7 +58,6 @@ async def get_pool(name: str):
info = pool.info()
xml = etree.fromstring(pool.XMLDesc(0).encode())
# 获取卷列表
volumes = []
try:
for vol_name in pool.listVolumes():
@@ -89,9 +88,9 @@ async def get_pool(name: str):
@router.post("/pool/create")
async def create_pool(pool: PoolCreate):
async def create_pool(pool: PoolCreate, host_id: str = Query("local")):
"""创建存储池"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
xml = f"""<pool type='{pool.type}'>
<name>{pool.name}</name>
<target>
@@ -109,9 +108,9 @@ async def create_pool(pool: PoolCreate):
@router.delete("/pool/{name}")
async def delete_pool(name: str):
async def delete_pool(name: str, host_id: str = Query("local")):
"""删除存储池"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
pool = rw_conn.storagePoolLookupByName(name)
except libvirt.libvirtError:
@@ -125,9 +124,9 @@ async def delete_pool(name: str):
@router.post("/pool/{name}/volume")
async def create_volume(name: str, vol: VolCreate):
async def create_volume(name: str, vol: VolCreate, host_id: str = Query("local")):
"""在存储池中创建卷"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
pool = rw_conn.storagePoolLookupByName(name)
except libvirt.libvirtError:
@@ -149,9 +148,9 @@ async def create_volume(name: str, vol: VolCreate):
@router.delete("/pool/{pool_name}/volume/{vol_name}")
async def delete_volume(pool_name: str, vol_name: str):
async def delete_volume(pool_name: str, vol_name: str, host_id: str = Query("local")):
"""删除卷"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
pool = rw_conn.storagePoolLookupByName(pool_name)
vol = pool.storageVolLookupByName(vol_name)
@@ -162,7 +161,7 @@ async def delete_volume(pool_name: str, vol_name: str):
@router.get("/isos")
async def list_isos():
async def list_isos(host_id: str = Query("local")):
"""列出可用的ISO镜像"""
iso_dirs = ["/var/lib/libvirt/iso", "/isos", "/mnt/isos"]
isos = []
+23 -35
View File
@@ -1,11 +1,11 @@
"""虚拟机管理路由"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional, List
from lxml import etree
import os
from app.libvirt_conn import libvirt_conn
from app.libvirt_conn import conn_pool
from app.utils import parse_vm_info, generate_vm_xml
import libvirt
@@ -36,9 +36,9 @@ class VMClone(BaseModel):
# ===== API =====
@router.get("/list")
async def list_vms():
async def list_vms(host_id: str = Query("local")):
"""获取所有虚拟机列表"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
domains = conn.listAllDomains(0)
vms = []
for dom in domains:
@@ -56,9 +56,9 @@ async def list_vms():
@router.get("/detail/{name}")
async def get_vm_detail(name: str):
async def get_vm_detail(name: str, host_id: str = Query("local")):
"""获取虚拟机详情"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
@@ -69,20 +69,17 @@ async def get_vm_detail(name: str):
# 运行中的虚拟机获取更多动态信息
if info["state"] == "running":
try:
# CPU 时间
_, _, cpu_time, _ = dom.info()
info["cpu_time_ns"] = cpu_time
except Exception:
pass
# 内存使用
try:
mem_stats = dom.memoryStats()
info["memory_stats"] = mem_stats
except Exception:
pass
# 块设备统计
try:
block_stats = []
for disk in info.get("disks", []):
@@ -99,7 +96,6 @@ async def get_vm_detail(name: str):
except Exception:
pass
# 网络统计
try:
net_stats = []
for i, iface in enumerate(info.get("interfaces", [])):
@@ -120,18 +116,18 @@ async def get_vm_detail(name: str):
@router.post("/create")
async def create_vm(vm: VMCreate):
async def create_vm(vm: VMCreate, host_id: str = Query("local")):
"""创建虚拟机"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
# 检查名称是否已存在
try:
conn.lookupByName(vm.name)
raise HTTPException(status_code=400, detail=f"虚拟机 '{vm.name}' 已存在")
except libvirt.libvirtError:
pass # 不存在,继续创建
pass
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
# 确定磁盘路径
pool = rw_conn.storagePoolLookupByName(vm.pool_name)
@@ -143,7 +139,6 @@ async def create_vm(vm: VMCreate):
disk_path = os.path.join(pool_path, f"{vm.name}.qcow2")
# 创建 qcow2 磁盘
# 创建卷的 XML
vol_xml = f"""<volume>
<name>{vm.name}.qcow2</name>
<capacity unit='GiB'>{vm.disk_gb}</capacity>
@@ -179,15 +174,15 @@ async def create_vm(vm: VMCreate):
@router.post("/action/{name}")
async def vm_action(name: str, action: VMAction):
async def vm_action(name: str, action: VMAction, host_id: str = Query("local")):
"""虚拟机操作"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
rw_dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
@@ -221,9 +216,9 @@ async def vm_action(name: str, action: VMAction):
@router.delete("/delete/{name}")
async def delete_vm(name: str, force: bool = False):
async def delete_vm(name: str, force: bool = False, host_id: str = Query("local")):
"""删除虚拟机"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
@@ -262,9 +257,9 @@ async def delete_vm(name: str, force: bool = False):
@router.post("/clone/{name}")
async def clone_vm(name: str, clone: VMClone):
async def clone_vm(name: str, clone: VMClone, host_id: str = Query("local")):
"""克隆虚拟机"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
@@ -278,19 +273,15 @@ async def clone_vm(name: str, clone: VMClone):
pass
try:
# 获取源虚拟机 XML
xml_desc = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)
tree = etree.fromstring(xml_desc.encode())
# 修改名称
tree.find("name").text = clone.new_name
# 修改 UUID(删除让libvirt自动生成)
uuid_elem = tree.find("uuid")
if uuid_elem is not None:
tree.remove(uuid_elem)
# 修改磁盘路径
import uuid as uuid_mod
new_uuid = str(uuid_mod.uuid4())[:8]
for disk in tree.findall(".//disk[@device='disk']"):
@@ -300,7 +291,6 @@ async def clone_vm(name: str, clone: VMClone):
new_path = old_path.replace(f"{name}.qcow2", f"{clone.new_name}.qcow2")
source.set("file", new_path)
# 修改 MAC 地址
for mac in tree.findall(".//interface/mac"):
import random
mac_addr = "52:54:00:%02x:%02x:%02x" % (
@@ -310,7 +300,6 @@ async def clone_vm(name: str, clone: VMClone):
)
mac.set("address", mac_addr)
# 复制磁盘
old_disk_path = ""
new_disk_path = ""
for disk in tree.findall(".//disk[@device='disk']/source"):
@@ -325,7 +314,6 @@ async def clone_vm(name: str, clone: VMClone):
capture_output=True,
)
# 定义新虚拟机
new_xml = etree.tostring(tree, encoding="unicode")
rw_conn.defineXML(new_xml)
@@ -336,9 +324,9 @@ async def clone_vm(name: str, clone: VMClone):
@router.get("/xml/{name}")
async def get_vm_xml(name: str):
async def get_vm_xml(name: str, host_id: str = Query("local")):
"""获取虚拟机 XML 配置"""
conn = libvirt_conn.conn
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
@@ -348,9 +336,9 @@ async def get_vm_xml(name: str):
@router.put("/xml/{name}")
async def update_vm_xml(name: str, xml: dict):
async def update_vm_xml(name: str, xml: dict, host_id: str = Query("local")):
"""更新虚拟机 XML 配置"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
@@ -368,9 +356,9 @@ async def update_vm_xml(name: str, xml: dict):
@router.post("/migrate/{name}")
async def migrate_vm(name: str, dest_uri: str, live: bool = True):
async def migrate_vm(name: str, dest_uri: str, live: bool = True, host_id: str = Query("local")):
"""迁移虚拟机"""
with libvirt_conn.get_rw() as rw_conn:
with conn_pool.get_rw(host_id) as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError: