Files

226 baris
7.0 KiB
Python

"""资源监控路由"""
from fastapi import APIRouter, HTTPException, Query
from app.libvirt_conn import conn_pool
import libvirt
import time
import threading
router = APIRouter()
# 简易内存缓存
_stats_cache = {}
_cache_lock = threading.Lock()
@router.get("/overview")
async def monitor_overview(host_id: str = Query("local")):
"""宿主机总览监控"""
conn = conn_pool.get_conn(host_id)
host_info = conn.getInfo()
hostname = conn.getHostname()
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 = {}
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, host_id: str = Query("local")):
"""获取虚拟机实时监控数据"""
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, "state": "stopped", "cpu_percent": 0, "memory": {}}
cache_key = f"{host_id}_{name}"
cpu_percent = _get_vm_cpu_percent(dom, cache_key)
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
disk_stats = _get_vm_disk_stats(dom)
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, cache_key: str) -> float:
"""计算虚拟机 CPU 使用率(归一化到 0-100%,基于 vCPU 数量)"""
try:
# dom.info() 返回: [state, maxMem, memory, nrVirtCpu, cpuTime]
info1 = dom.info()
cpu_time1 = info1[4] # cpuTime 在索引4(之前错误地用了索引2=memory)
nr_vcpu = info1[3] or 1 # vCPU 数量
t1 = time.time()
with _cache_lock:
prev = _stats_cache.get(cache_key)
if prev:
cpu_time0, t0, prev_vcpu = prev
elapsed = t1 - t0
if elapsed > 0:
cpu_diff = cpu_time1 - cpu_time0
# 除以 vCPU 数量归一化,使满载时 = 100%
cpu_percent = round((cpu_diff / 1e9) / elapsed / nr_vcpu * 100, 1)
cpu_percent = max(0.0, min(cpu_percent, 100.0))
else:
cpu_percent = 0.0
else:
cpu_percent = 0.0
with _cache_lock:
_stats_cache[cache_key] = (cpu_time1, t1, nr_vcpu)
return cpu_percent
except Exception:
return 0.0
def get_vm_runtime_stats(dom, cache_key: str) -> dict:
"""获取虚拟机运行时统计(供其他模块调用,如 VM 列表页)
Returns:
dict: { cpu_percent, memory: { rss_mb, actual_mb, usage_percent } }
"""
if not dom.isActive():
return {"cpu_percent": 0, "memory": {}}
cpu_percent = _get_vm_cpu_percent(dom, cache_key)
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,
"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
return {"cpu_percent": cpu_percent, "memory": mem_stats}
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