feat: 优化虚拟机列表,支持多主机聚合显示

- 新增 /vm/list-all API 聚合所有主机虚拟机
- parse_vm_info 支持 include_ip 参数控制IP获取
- VMList 添加主机选择器,显示宿主机列
- 修复 API 路径 /host/list -> /hosts/list
- 新增启动脚本 scripts/start.sh
- 新增 Guest Agent 安装脚本 scripts/install-guest-agent.sh
- 更新 README 文档
This commit is contained in:
admin
2026-05-07 14:52:45 +08:00
parent 8ccccf8f52
commit dbba1694d8
8 changed files with 899 additions and 76 deletions
+150 -5
View File
@@ -3,6 +3,7 @@ from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional, List
from lxml import etree
from app.hosts import list_hosts
import os
from app.libvirt_conn import conn_pool
@@ -36,14 +37,14 @@ class VMClone(BaseModel):
# ===== API =====
@router.get("/list")
async def list_vms(host_id: str = Query("local")):
"""获取所有虚拟机列表"""
async def list_vms(host_id: str = Query("local"), include_ip: bool = False):
"""获取指定主机所有虚拟机列表(轻量模式,默认不获取IP"""
conn = conn_pool.get_conn(host_id)
domains = conn.listAllDomains(0)
vms = []
for dom in domains:
try:
vm_info = parse_vm_info(dom)
vm_info = parse_vm_info(dom, host_id, include_ip=include_ip)
vms.append(vm_info)
except Exception as e:
vms.append({
@@ -52,7 +53,37 @@ async def list_vms(host_id: str = Query("local")):
"state": "error",
"error": str(e),
})
return {"vms": vms, "total": len(vms)}
return {"vms": vms, "total": len(vms), "host_id": host_id}
@router.get("/list-all")
async def list_all_vms(include_ip: bool = False):
"""获取所有主机所有虚拟机列表(聚合模式)"""
hosts = list_hosts()
all_vms = []
for host in hosts:
try:
conn = conn_pool.get_conn(host.id)
domains = conn.listAllDomains(0)
for dom in domains:
try:
vm_info = parse_vm_info(dom, host.id, include_ip=include_ip)
vm_info["host_id"] = host.id
vm_info["host_name"] = host.name
all_vms.append(vm_info)
except Exception:
all_vms.append({
"name": dom.name(),
"uuid": dom.UUIDString(),
"state": "error",
"host_id": host.id,
"host_name": host.name,
})
except Exception:
pass # 跳过无法连接的主机
return {"vms": all_vms, "total": len(all_vms), "host_count": len(hosts)}
@router.get("/detail/{name}")
@@ -64,7 +95,7 @@ async def get_vm_detail(name: str, host_id: str = Query("local")):
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
info = parse_vm_info(dom)
info = parse_vm_info(dom, host_id)
# 运行中的虚拟机获取更多动态信息
if info["state"] == "running":
@@ -115,6 +146,120 @@ async def get_vm_detail(name: str, host_id: str = Query("local")):
return info
@router.get("/ip/{name}")
async def get_vm_ip(name: str, host_id: str = Query("local")):
"""获取虚拟机 IP 地址"""
conn = conn_pool.get_conn(host_id)
try:
dom = conn.lookupByName(name)
except libvirt.libvirtError:
raise HTTPException(status_code=404, detail=f"虚拟机 '{name}' 不存在")
if not dom.isActive():
return {"name": name, "ips": [], "message": "虚拟机未运行"}
xml_desc = dom.XMLDesc(0)
tree = etree.fromstring(xml_desc.encode())
interfaces = []
for iface in tree.findall(".//interface"):
source = iface.find("source")
mac_elem = iface.find("mac")
if mac_elem is None:
continue
mac = mac_elem.get("address", "")
network = source.get("network", "") if source is not None else ""
ips = []
# 方式1: QEMU Guest Agent
try:
ifaces = dom.interfaceAddresses(
libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT, 0
)
if ifaces:
for ifname, ifdata in ifaces.items():
if ifdata.get("hwaddr", "").lower() == mac.lower():
addrs = ifdata.get("addrs", [])
for a in addrs:
addr = a.get("addr", "")
if "." in addr:
ips.append({"ip": addr, "type": "ipv4", "source": "guest-agent"})
elif ":" in addr:
ips.append({"ip": addr, "type": "ipv6", "source": "guest-agent"})
except Exception:
pass
# 方式2: DHCP 租约
if not ips:
try:
ifaces = dom.interfaceAddresses(
libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0
)
if ifaces:
for ifname, ifdata in ifaces.items():
hwaddr = ifdata.get("hwaddr", "").lower()
if hwaddr == mac.lower():
addrs = ifdata.get("addrs", [])
for a in addrs:
addr = a.get("addr", "")
if "." in addr:
ips.append({"ip": addr, "type": "ipv4", "source": "dhcp-lease"})
except Exception:
pass
# 方式3: ARP 表查找
if not ips:
ip_list = _arp_lookup(mac, host_id)
for ip in ip_list:
ips.append({"ip": ip, "type": "ipv4", "source": "arp"})
interfaces.append({
"mac": mac,
"network": network,
"ips": ips,
})
return {"name": name, "interfaces": interfaces}
def _arp_lookup(mac: str, host_id: str = "local") -> list:
"""通过 ARP 表查找 MAC 对应的 IP"""
import subprocess
from app.hosts import get_host as get_host_info
host_info = get_host_info(host_id)
is_remote = host_info and host_info.type != "local"
try:
if is_remote and host_info.type == "ssh":
from urllib.parse import urlparse
parsed = urlparse(host_info.uri)
remote_host = parsed.hostname
ssh_args = ["ssh", "-o", "StrictHostKeyChecking=no"]
if host_info.ssh_key_path:
ssh_args.extend(["-i", host_info.ssh_key_path])
ssh_args.extend([remote_host, "ip", "neigh", "show"])
result = subprocess.run(ssh_args, capture_output=True, text=True, timeout=5)
else:
result = subprocess.run(
["ip", "neigh", "show"],
capture_output=True, text=True, timeout=3,
)
if result.returncode == 0:
found = []
for line in result.stdout.strip().split("\n"):
if mac.lower() in line.lower():
parts = line.split()
if parts and "." in parts[0]:
found.append(parts[0])
return found
except Exception:
pass
return []
@router.post("/create")
async def create_vm(vm: VMCreate, host_id: str = Query("local")):
"""创建虚拟机"""