feat: KVM虚拟化管理平台初始版本

This commit is contained in:
admin
2026-04-30 15:51:48 +08:00
commit fac8ab7470
42 changed files with 5621 additions and 0 deletions
View File
+207
View File
@@ -0,0 +1,207 @@
"""资源监控路由"""
from fastapi import APIRouter, HTTPException
from app.libvirt_conn import libvirt_conn
import libvirt
import time
import threading
router = APIRouter()
# 简易内存缓存
_stats_cache = {}
_cache_lock = threading.Lock()
@router.get("/overview")
async def monitor_overview():
"""宿主机总览监控"""
conn = libvirt_conn.conn
# 宿主机信息
host_info = conn.getInfo()
hostname = conn.getHostname()
# CPU 使用率(通过 node info
cpu_stats = conn.getCPUStats(-1, 0) # 全局 CPU 统计
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 = {}
for line in f:
parts = line.split()
if len(parts) >= 2:
meminfo[parts[0].rstrip(":")] = int(parts[1])
mem_total_mb = meminfo.get("MemTotal", 0) // 1024
mem_available_mb = meminfo.get("MemAvailable", 0) // 1024
mem_used_mb = mem_total_mb - mem_available_mb
mem_percent = round(mem_used_mb / mem_total_mb * 100, 1) if mem_total_mb > 0 else 0
except Exception:
mem_total_mb = memory_total_kb // 1024
mem_used_mb = 0
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
return {
"hostname": hostname,
"cpu": {
"cores": host_info[2],
"speed_mhz": host_info[3],
"usage_percent": cpu_percent,
},
"memory": {
"total_mb": mem_total_mb,
"used_mb": mem_used_mb,
"available_mb": mem_available_mb,
"usage_percent": mem_percent,
},
"vms": {
"total": len(domains),
"running": running,
"stopped": stopped,
},
}
@router.get("/vm/{name}")
async def monitor_vm(name: str):
"""获取虚拟机实时监控数据"""
conn = libvirt_conn.conn
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
if not dom.isActive():
return {"name": name, "state": "stopped", "cpu_percent": 0, "memory": {}}
# CPU 百分比
cpu_percent = _get_vm_cpu_percent(dom)
# 内存
mem_stats = {}
try:
raw = dom.memoryStats()
mem_stats = {
"rss_mb": raw.get("rss", 0) // 1024 if "rss" in raw else 0,
"actual_mb": raw.get("actual", 0) // 1024 if "actual" in raw else 0,
"available_mb": raw.get("available", 0) // 1024 if "available" in raw else 0,
"usage_percent": round(
raw.get("rss", 0) / raw.get("actual", 1) * 100, 1
) if "rss" in raw and "actual" in raw else 0,
}
except Exception:
pass
# 磁盘IO
disk_stats = _get_vm_disk_stats(dom)
# 网络IO
net_stats = _get_vm_net_stats(dom)
return {
"name": name,
"state": "running",
"cpu_percent": cpu_percent,
"memory": mem_stats,
"disk": disk_stats,
"network": net_stats,
}
def _get_vm_cpu_percent(dom) -> 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)
if prev:
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)
return cpu_percent
except Exception:
return 0.0
def _get_vm_disk_stats(dom) -> list:
"""获取虚拟机磁盘IO"""
from lxml import etree
stats = []
try:
xml = etree.fromstring(dom.XMLDesc(0).encode())
for disk in xml.findall(".//disk[@device='disk']"):
target = disk.find("target")
if target is not None:
dev = target.get("dev", "")
try:
s = dom.blockStats(dev)
stats.append({
"dev": dev,
"read_bytes": s[1],
"write_bytes": s[3],
"read_requests": s[0],
"write_requests": s[2],
})
except Exception:
pass
except Exception:
pass
return stats
def _get_vm_net_stats(dom) -> list:
"""获取虚拟机网络IO"""
from lxml import etree
stats = []
try:
xml = etree.fromstring(dom.XMLDesc(0).encode())
for iface in xml.findall(".//interface"):
target = iface.find("target")
if target is not None:
dev = target.get("dev", "")
try:
s = dom.interfaceStats(dev)
stats.append({
"dev": dev,
"rx_bytes": s[0],
"tx_bytes": s[4],
"rx_packets": s[1],
"tx_packets": s[5],
"rx_errors": s[2],
"tx_errors": s[6],
})
except Exception:
pass
except Exception:
pass
return stats
+175
View File
@@ -0,0 +1,175 @@
"""网络管理路由"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from typing import Optional, List
from lxml import etree
from app.libvirt_conn import libvirt_conn
import libvirt
router = APIRouter()
class NetworkCreate(BaseModel):
name: str = Field(..., description="网络名称")
mode: str = Field("nat", description="模式: nat/bridge/isolated")
subnet: str = Field("192.168.100.0/24", description="子网")
bridge: Optional[str] = Field(None, description="桥接网卡名(mode=bridge时必填)")
dhcp_start: Optional[str] = Field(None, description="DHCP起始IP")
dhcp_end: Optional[str] = Field(None, description="DHCP结束IP")
@router.get("/list")
async def list_networks():
"""列出所有网络"""
conn = libvirt_conn.conn
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"
ip_elem = xml.find("ip")
address = ip_elem.get("address", "") if ip_elem is not None else ""
netmask = ip_elem.get("netmask", "") if ip_elem is not None else ""
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", ""),
}
# 活跃租约
leases = []
try:
for lease in net.DHCPLeases():
leases.append({
"ip": lease.get("ipaddr", ""),
"mac": lease.get("mac", ""),
"hostname": lease.get("hostname", ""),
"expiry": lease.get("expirytime", 0),
})
except Exception:
pass
result.append({
"name": net.name(),
"active": net.isActive() == 1,
"persistent": net.isPersistent() == 1,
"autostart": net.autostart() == 1,
"mode": mode,
"address": address,
"netmask": netmask,
"bridge": bridge_name,
"dhcp": dhcp_range,
"leases": leases,
})
return {"networks": result, "total": len(result)}
@router.get("/detail/{name}")
async def get_network(name: str):
"""获取网络详情"""
conn = libvirt_conn.conn
try:
net = conn.networkLookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"网络 '{name}' 不存在")
xml_str = net.XMLDesc(0)
return {"name": name, "xml": xml_str, "active": net.isActive() == 1}
@router.post("/create")
async def create_network(net: NetworkCreate):
"""创建网络"""
if net.mode == "bridge" and not net.bridge:
raise HTTPException(status_code=400, detail="桥接模式必须指定桥接网卡")
if net.mode == "bridge":
xml = f"""<network>
<name>{net.name}</name>
<forward mode='bridge'/>
<bridge name='{net.bridge}'/>
</network>"""
else:
# NAT或隔离模式
import ipaddress
network = ipaddress.ip_network(net.subnet, strict=False)
gateway = str(network.network_address + 1)
dhcp_xml = ""
if net.mode == "nat":
start = net.dhcp_start or str(network.network_address + 2)
end = net.dhcp_end or str(network.network_address + 254)
dhcp_xml = f"""
<dhcp>
<range start='{start}' end='{end}'/>
</dhcp>"""
forward_xml = f"<forward mode='{net.mode}'/>" if net.mode == "nat" else ""
netmask = str(network.netmask)
xml = f"""<network>
<name>{net.name}</name>
{forward_xml}
<bridge name='virbr-{net.name[:8]}' stp='on' delay='0'/>
<ip address='{gateway}' netmask='{netmask}'>{dhcp_xml}
</ip>
</network>"""
with libvirt_conn.get_rw() as rw_conn:
try:
n = rw_conn.networkDefineXML(xml)
n.setAutostart(1)
n.create()
return {"message": f"网络 '{net.name}' 创建成功"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"创建网络失败: {str(e)}")
@router.delete("/delete/{name}")
async def delete_network(name: str):
"""删除网络"""
with libvirt_conn.get_rw() as rw_conn:
try:
net = rw_conn.networkLookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"网络 '{name}' 不存在")
if net.isActive():
net.destroy()
net.undefine()
return {"message": f"网络 '{name}' 已删除"}
@router.post("/action/{name}")
async def network_action(name: str, action: str):
"""网络操作: start/stop"""
with libvirt_conn.get_rw() as rw_conn:
try:
net = rw_conn.networkLookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"网络 '{name}' 不存在")
try:
if action == "start":
net.create()
elif action == "stop":
net.destroy()
else:
raise HTTPException(status_code=400, detail=f"不支持的操作: {action}")
return {"message": f"网络 '{name}' {action} 成功"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}")
+114
View File
@@ -0,0 +1,114 @@
"""快照管理路由"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from typing import Optional
from lxml import etree
from app.libvirt_conn import libvirt_conn
import libvirt
router = APIRouter()
class SnapshotCreate(BaseModel):
name: str = Field(..., description="快照名称")
description: Optional[str] = Field(None, description="快照描述")
@router.get("/list/{vm_name}")
async def list_snapshots(vm_name: str):
"""列出虚拟机的所有快照"""
conn = libvirt_conn.conn
try:
dom = conn.lookupByName(vm_name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{vm_name}' 不存在")
snapshots = []
try:
for snap in dom.listAllSnapshots(0):
xml = etree.fromstring(snap.getXMLDesc().encode())
desc = xml.find("description")
state = xml.find("state")
creation = xml.find("creationTime")
snapshots.append({
"name": snap.getName(),
"state": state.text if state is not None else "",
"description": desc.text if desc is not None else "",
"creation_time": int(creation.text) if creation is not None else 0,
"is_current": snap.isCurrent() == 1,
})
except libvirt.libvirtError:
pass # 没有快照
return {"vm": vm_name, "snapshots": snapshots, "total": len(snapshots)}
@router.post("/create/{vm_name}")
async def create_snapshot(vm_name: str, snap: SnapshotCreate):
"""创建快照"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(vm_name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{vm_name}' 不存在")
desc_xml = f"<description>{snap.description}</description>" if snap.description else ""
xml = f"""<domainsnapshot>
<name>{snap.name}</name>
{desc_xml}
</domainsnapshot>"""
try:
dom.snapshotCreateXML(xml, 0)
return {"message": f"快照 '{snap.name}' 创建成功"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"创建快照失败: {str(e)}")
@router.post("/revert/{vm_name}/{snap_name}")
async def revert_snapshot(vm_name: str, snap_name: str):
"""恢复快照"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(vm_name)
snap = dom.snapshotLookupByName(snap_name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail="虚拟机或快照不存在")
try:
dom.revertToSnapshot(snap)
return {"message": f"已恢复到快照 '{snap_name}'"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"恢复快照失败: {str(e)}")
@router.delete("/delete/{vm_name}/{snap_name}")
async def delete_snapshot(vm_name: str, snap_name: str):
"""删除快照"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(vm_name)
snap = dom.snapshotLookupByName(snap_name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail="虚拟机或快照不存在")
try:
snap.delete(0)
return {"message": f"快照 '{snap_name}' 已删除"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"删除快照失败: {str(e)}")
@router.get("/detail/{vm_name}/{snap_name}")
async def get_snapshot_detail(vm_name: str, snap_name: str):
"""获取快照详情"""
conn = libvirt_conn.conn
try:
dom = conn.lookupByName(vm_name)
snap = dom.snapshotLookupByName(snap_name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail="虚拟机或快照不存在")
return {"name": snap_name, "vm": vm_name, "xml": snap.getXMLDesc()}
+180
View File
@@ -0,0 +1,180 @@
"""存储池管理路由"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from typing import Optional
from lxml import etree
import os
from app.libvirt_conn import libvirt_conn
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():
"""列出所有存储池"""
conn = libvirt_conn.conn
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):
"""获取存储池详情"""
conn = libvirt_conn.conn
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):
"""创建存储池"""
with libvirt_conn.get_rw() 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):
"""删除存储池"""
with libvirt_conn.get_rw() 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):
"""在存储池中创建卷"""
with libvirt_conn.get_rw() 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):
"""删除卷"""
with libvirt_conn.get_rw() 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():
"""列出可用的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)}
+387
View File
@@ -0,0 +1,387 @@
"""虚拟机管理路由"""
from fastapi import APIRouter, HTTPException
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.utils import parse_vm_info, generate_vm_xml
import libvirt
router = APIRouter()
# ===== 请求模型 =====
class VMCreate(BaseModel):
name: str = Field(..., description="虚拟机名称")
memory_mb: int = Field(2048, description="内存大小(MB)")
vcpus: int = Field(2, description="CPU核心数")
disk_gb: int = Field(20, description="磁盘大小(GB)")
pool_name: str = Field("default", description="存储池名称")
iso_path: Optional[str] = Field(None, description="ISO安装镜像路径")
network: str = Field("default", description="网络名称")
description: Optional[str] = Field(None, description="描述")
class VMAction(BaseModel):
action: str = Field(..., description="操作: start/stop/restart/pause/resume/force_stop")
class VMClone(BaseModel):
new_name: str = Field(..., description="新虚拟机名称")
# ===== API =====
@router.get("/list")
async def list_vms():
"""获取所有虚拟机列表"""
conn = libvirt_conn.conn
domains = conn.listAllDomains(0)
vms = []
for dom in domains:
try:
vm_info = parse_vm_info(dom)
vms.append(vm_info)
except Exception as e:
vms.append({
"name": dom.name(),
"uuid": dom.UUIDString(),
"state": "error",
"error": str(e),
})
return {"vms": vms, "total": len(vms)}
@router.get("/detail/{name}")
async def get_vm_detail(name: str):
"""获取虚拟机详情"""
conn = libvirt_conn.conn
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
info = parse_vm_info(dom)
# 运行中的虚拟机获取更多动态信息
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", []):
if disk.get("dev"):
stats = dom.blockStats(disk["dev"])
block_stats.append({
"dev": disk["dev"],
"read_bytes": stats[1],
"write_bytes": stats[3],
"read_requests": stats[0],
"write_requests": stats[2],
})
info["block_stats"] = block_stats
except Exception:
pass
# 网络统计
try:
net_stats = []
for i, iface in enumerate(info.get("interfaces", [])):
if iface.get("mac"):
stats = dom.interfaceStats(iface["mac"])
net_stats.append({
"mac": iface["mac"],
"rx_bytes": stats[0],
"tx_bytes": stats[4],
"rx_packets": stats[1],
"tx_packets": stats[5],
})
info["net_stats"] = net_stats
except Exception:
pass
return info
@router.post("/create")
async def create_vm(vm: VMCreate):
"""创建虚拟机"""
conn = libvirt_conn.conn
# 检查名称是否已存在
try:
conn.lookupByName(vm.name)
raise HTTPException(status_code=400, detail=f"虚拟机 '{vm.name}' 已存在")
except libvirt.libvirtError:
pass # 不存在,继续创建
with libvirt_conn.get_rw() as rw_conn:
try:
# 确定磁盘路径
pool = rw_conn.storagePoolLookupByName(vm.pool_name)
pool_info = pool.info()
pool_xml = etree.fromstring(pool.XMLDesc(0).encode())
target = pool_xml.find(".//target/path")
pool_path = target.text if target is not None else "/var/lib/libvirt/images"
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>
<allocation unit='GiB'>1</allocation>
<target>
<format type='qcow2'/>
<permissions>
<mode>0644</mode>
</permissions>
</target>
</volume>"""
pool.createXML(vol_xml, 0)
# 生成虚拟机XML
vm_xml = generate_vm_xml(
name=vm.name,
memory_mb=vm.memory_mb,
vcpus=vm.vcpus,
disk_path=disk_path,
disk_size_gb=vm.disk_gb,
iso_path=vm.iso_path,
network=vm.network,
)
# 定义并启动
dom = rw_conn.defineXML(vm_xml)
dom.create()
return {"message": f"虚拟机 '{vm.name}' 创建成功", "name": vm.name}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"创建虚拟机失败: {str(e)}")
@router.post("/action/{name}")
async def vm_action(name: str, action: VMAction):
"""虚拟机操作"""
conn = libvirt_conn.conn
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
with libvirt_conn.get_rw() as rw_conn:
try:
rw_dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
act = action.action
try:
if act == "start":
rw_dom.create()
msg = f"虚拟机 '{name}' 已启动"
elif act == "stop":
rw_dom.shutdown()
msg = f"虚拟机 '{name}' 正在关闭"
elif act == "force_stop":
rw_dom.destroy()
msg = f"虚拟机 '{name}' 已强制关闭"
elif act == "restart":
rw_dom.reboot(libvirt.VIR_DOMAIN_REBOOT_DEFAULT)
msg = f"虚拟机 '{name}' 正在重启"
elif act == "pause":
rw_dom.suspend()
msg = f"虚拟机 '{name}' 已暂停"
elif act == "resume":
rw_dom.resume()
msg = f"虚拟机 '{name}' 已恢复"
else:
raise HTTPException(status_code=400, detail=f"不支持的操作: {act}")
return {"message": msg}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}")
@router.delete("/delete/{name}")
async def delete_vm(name: str, force: bool = False):
"""删除虚拟机"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
state, _ = dom.info()[0:2]
if state == libvirt.VIR_DOMAIN_RUNNING:
if force:
dom.destroy()
else:
raise HTTPException(
status_code=400,
detail=f"虚拟机 '{name}' 正在运行,请先关闭或使用 force=true"
)
# 获取磁盘路径
xml_desc = dom.XMLDesc(0)
tree = etree.fromstring(xml_desc.encode())
disk_files = []
for disk in tree.findall(".//disk[@device='disk']/source"):
f = disk.get("file")
if f:
disk_files.append(f)
# 取消定义
dom.undefine()
# 删除磁盘文件
for f in disk_files:
try:
os.remove(f)
except Exception:
pass
return {"message": f"虚拟机 '{name}' 已删除", "removed_disks": disk_files}
@router.post("/clone/{name}")
async def clone_vm(name: str, clone: VMClone):
"""克隆虚拟机"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
# 检查新名称是否已存在
try:
rw_conn.lookupByName(clone.new_name)
raise HTTPException(status_code=400, detail=f"虚拟机 '{clone.new_name}' 已存在")
except libvirt.libvirtError:
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']"):
source = disk.find("source")
if source is not None:
old_path = source.get("file", "")
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" % (
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255),
)
mac.set("address", mac_addr)
# 复制磁盘
old_disk_path = ""
new_disk_path = ""
for disk in tree.findall(".//disk[@device='disk']/source"):
old_disk_path = disk.get("file", "")
new_disk_path = old_disk_path.replace(f"{name}.qcow2", f"{clone.new_name}.qcow2")
if old_disk_path and os.path.exists(old_disk_path):
import subprocess
subprocess.run(
["qemu-img", "create", "-f", "qcow2", "-b", old_disk_path, "-F", "qcow2", new_disk_path],
check=True,
capture_output=True,
)
# 定义新虚拟机
new_xml = etree.tostring(tree, encoding="unicode")
rw_conn.defineXML(new_xml)
return {"message": f"虚拟机 '{name}' 已克隆为 '{clone.new_name}'"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"克隆失败: {str(e)}")
@router.get("/xml/{name}")
async def get_vm_xml(name: str):
"""获取虚拟机 XML 配置"""
conn = libvirt_conn.conn
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
return {"name": name, "xml": dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)}
@router.put("/xml/{name}")
async def update_vm_xml(name: str, xml: dict):
"""更新虚拟机 XML 配置"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
xml_str = xml.get("xml", "")
if not xml_str:
raise HTTPException(status_code=400, detail="XML不能为空")
try:
rw_conn.defineXML(xml_str)
return {"message": f"虚拟机 '{name}' 配置已更新"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"更新失败: {str(e)}")
@router.post("/migrate/{name}")
async def migrate_vm(name: str, dest_uri: str, live: bool = True):
"""迁移虚拟机"""
with libvirt_conn.get_rw() as rw_conn:
try:
dom = rw_conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
flags = libvirt.VIR_MIGRATE_LIVE if live else 0
flags |= libvirt.VIR_MIGRATE_PERSIST_DEST
try:
dest_conn = libvirt.open(dest_uri)
dom.migrate(dest_conn, flags, None, None, 0)
return {"message": f"虚拟机 '{name}' 已迁移到 {dest_uri}"}
except libvirt.libvirtError as e:
raise HTTPException(status_code=500, detail=f"迁移失败: {str(e)}")