Files
kvm-manager/backend/app/routers/monitor.py
T
2026-04-30 15:51:48 +08:00

208 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""资源监控路由"""
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