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
+118 -31
View File
@@ -96,8 +96,14 @@ def generate_vm_xml(
return xml_parts
def parse_vm_info(dom) -> dict:
"""从 libvirt domain 对象提取虚拟机信息"""
def parse_vm_info(dom, host_id: str = "local", include_ip: bool = False) -> dict:
"""从 libvirt domain 对象提取虚拟机信息
Args:
dom: libvirt domain 对象
host_id: 主机ID
include_ip: 是否获取IP地址(开启会明显变慢,默认关闭)
"""
from app.libvirt_conn import libvirt_conn
# 基本信息
@@ -161,43 +167,37 @@ def parse_vm_info(dom) -> dict:
for iface in tree.findall(".//interface"):
source = iface.find("source")
model = iface.find("model")
target = iface.find("target")
iface_info = {
"type": iface.get("type", ""),
"network": source.get("network", "") if source is not None else "",
"bridge": source.get("bridge", "") if source is not None else "",
"model": model.get("type", "") if model is not None else "",
"dev": target.get("dev", "") if target is not None else "",
}
# 如果运行中,获取MAC和IP
if info["state"] == "running":
mac = iface.find("mac")
if mac is not None:
iface_info["mac"] = mac.get("address", "")
# 尝试获取IP地址
try:
ifaces = dom.interfaceAddresses(
libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT, 0
)
if ifaces:
for ifname, ifdata in ifaces.items():
if mac is not None and ifdata.get("hwaddr", "") == iface_info.get("mac", ""):
addrs = ifdata.get("addrs", [])
if addrs:
iface_info["ip"] = addrs[0].get("addr", "")
except Exception:
try:
ifaces = dom.interfaceAddresses(
libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0
)
if ifaces:
for ifname, ifdata in ifaces.items():
addrs = ifdata.get("addrs", [])
if addrs:
iface_info["ip"] = addrs[0].get("addr", "")
break
except Exception:
pass
mac_elem = iface.find("mac")
if mac_elem is not None:
iface_info["mac"] = mac_elem.get("address", "")
interfaces.append(iface_info)
info["interfaces"] = interfaces
# 获取 IP 地址(仅在需要时)
if include_ip and info["state"] == "running":
arp_cache = _get_arp_table(host_id)
for iface in interfaces:
if "mac" in iface:
# 先尝试 Guest Agent 和 DHCP 租约
iface["ip"] = _get_vm_ip(dom, iface["mac"], iface.get("network", ""), host_id, None)
# 如果没找到再用 ARP 表
if not iface.get("ip") and arp_cache:
mac_lower = iface["mac"].lower()
if mac_lower in arp_cache:
iface["ip"] = arp_cache[mac_lower]
else:
# 不获取 IP 时,初始化 ip 字段为空
for iface in interfaces:
iface["ip"] = ""
# VNC
graphics = tree.find(".//graphics[@type='vnc']")
if graphics is not None:
@@ -214,6 +214,93 @@ def parse_vm_info(dom) -> dict:
return info
def _get_arp_table(host_id: str = "local") -> dict:
"""获取 ARP 表(MAC -> IP 映射),返回 {mac_lower: 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:
arp = {}
for line in result.stdout.strip().split("\n"):
parts = line.split()
if len(parts) >= 5:
ip = parts[0]
# lladdr 行格式: IP dev IFACE lladdr MAC ...
for i, p in enumerate(parts):
if p == "lladdr" and i + 1 < len(parts) and "." in ip:
arp[parts[i + 1].lower()] = ip
return arp
except Exception:
pass
return {}
def _get_vm_ip(dom, mac: str, network: str = "", host_id: str = "local", arp_cache: dict = None) -> str:
"""多种方式尝试获取虚拟机 IP 地址"""
# 方式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", "")
# 优先返回 IPv4
if "." in addr:
return addr
if addrs:
return addrs[0].get("addr", "")
except Exception:
pass
# 方式2: DHCP 租约(通过 libvirt 网络)
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:
return addr
if addrs:
return addrs[0].get("addr", "")
except Exception:
pass
# 方式3: ARP 表查找(通过 MAC 地址查 IP)
if arp_cache is not None and mac.lower() in arp_cache:
return arp_cache[mac.lower()]
return ""
def _get_state(dom) -> str:
"""获取虚拟机运行状态"""
raw = dom.info()