Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f4ea2653f | |||
| 33f61c9f1d | |||
| f4f5475ffb |
@@ -112,32 +112,65 @@ async def monitor_vm(name: str, host_id: str = Query("local")):
|
||||
|
||||
|
||||
def _get_vm_cpu_percent(dom, cache_key: str) -> float:
|
||||
"""计算虚拟机 CPU 使用率"""
|
||||
"""计算虚拟机 CPU 使用率(归一化到 0-100%,基于 vCPU 数量)"""
|
||||
try:
|
||||
# dom.info() 返回: [state, maxMem, memory, nrVirtCpu, cpuTime]
|
||||
info1 = dom.info()
|
||||
cpu_time1 = info1[2]
|
||||
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
|
||||
cpu_time0, t0, prev_vcpu = prev
|
||||
elapsed = t1 - t0
|
||||
cpu_diff = cpu_time1 - cpu_time0
|
||||
cpu_percent = round((cpu_diff / 1e9) / elapsed * 100, 1)
|
||||
cpu_percent = min(cpu_percent, 100.0)
|
||||
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)
|
||||
_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
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
|
||||
from app.libvirt_conn import conn_pool
|
||||
from app.utils import parse_vm_info, generate_vm_xml
|
||||
from app.routers.monitor import get_vm_runtime_stats
|
||||
import libvirt
|
||||
|
||||
router = APIRouter()
|
||||
@@ -45,6 +46,12 @@ async def list_vms(host_id: str = Query("local"), include_ip: bool = False):
|
||||
for dom in domains:
|
||||
try:
|
||||
vm_info = parse_vm_info(dom, host_id, include_ip=include_ip)
|
||||
# 添加运行时统计(CPU使用率、内存使用量)
|
||||
cache_key = f"{host_id}_{vm_info['name']}"
|
||||
runtime = get_vm_runtime_stats(dom, cache_key)
|
||||
vm_info["cpu_percent"] = runtime["cpu_percent"]
|
||||
vm_info["memory_rss_mb"] = runtime["memory"].get("rss_mb", 0)
|
||||
vm_info["memory_usage_percent"] = runtime["memory"].get("usage_percent", 0)
|
||||
vms.append(vm_info)
|
||||
except Exception as e:
|
||||
vms.append({
|
||||
@@ -71,6 +78,12 @@ async def list_all_vms(include_ip: bool = False):
|
||||
vm_info = parse_vm_info(dom, host.id, include_ip=include_ip)
|
||||
vm_info["host_id"] = host.id
|
||||
vm_info["host_name"] = host.name
|
||||
# 添加运行时统计
|
||||
cache_key = f"{host.id}_{vm_info['name']}"
|
||||
runtime = get_vm_runtime_stats(dom, cache_key)
|
||||
vm_info["cpu_percent"] = runtime["cpu_percent"]
|
||||
vm_info["memory_rss_mb"] = runtime["memory"].get("rss_mb", 0)
|
||||
vm_info["memory_usage_percent"] = runtime["memory"].get("usage_percent", 0)
|
||||
all_vms.append(vm_info)
|
||||
except Exception:
|
||||
all_vms.append({
|
||||
@@ -100,8 +113,9 @@ async def get_vm_detail(name: str, host_id: str = Query("local")):
|
||||
# 运行中的虚拟机获取更多动态信息
|
||||
if info["state"] == "running":
|
||||
try:
|
||||
_, _, cpu_time, _ = dom.info()
|
||||
info["cpu_time_ns"] = cpu_time
|
||||
# dom.info(): [state, maxMem, memory, nrVirtCpu, cpuTime]
|
||||
dom_info = dom.info()
|
||||
info["cpu_time_ns"] = dom_info[4] # cpuTime 在索引4
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+5
-3
@@ -99,9 +99,11 @@ from starlette.responses import Response
|
||||
|
||||
routes = [
|
||||
WebSocketRoute("/ws/{path:path}", vnc_websocket, name="vnc"),
|
||||
Route("/api/{path:path}", proxy_api),
|
||||
Route("/api", proxy_api),
|
||||
Mount("/", StaticFiles(directory=DIST_DIR, html=True), name="static"),
|
||||
Route("/api/{path:path}", proxy_api, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]),
|
||||
Route("/api", proxy_api, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]),
|
||||
Mount("/assets", StaticFiles(directory=os.path.join(DIST_DIR, "assets")), name="static-assets"),
|
||||
Route("/{path:path}", serve_spa),
|
||||
Route("/", serve_spa),
|
||||
]
|
||||
|
||||
app = Starlette(routes=routes)
|
||||
|
||||
@@ -63,9 +63,23 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="vcpus" label="CPU" width="80" align="center" />
|
||||
<el-table-column label="内存" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.memory_mb }} MB</template>
|
||||
<el-table-column label="CPU" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.state === 'running' && row.cpu_percent !== undefined">
|
||||
<span :style="{ color: row.cpu_percent > 80 ? '#f56c6c' : row.cpu_percent > 50 ? '#e6a23c' : '#67c23a' }">
|
||||
{{ row.cpu_percent }}%</span> / {{ row.vcpus }}核
|
||||
</span>
|
||||
<span v-else>{{ row.vcpus }}核</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内存" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.state === 'running' && row.memory_rss_mb">
|
||||
<span :style="{ color: row.memory_usage_percent > 80 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ formatMem(row.memory_rss_mb) }}</span> / {{ formatMem(row.memory_mb) }}
|
||||
</span>
|
||||
<span v-else>{{ formatMem(row.memory_mb) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="IP 地址" min-width="130">
|
||||
<template #default="{ row }">
|
||||
@@ -188,6 +202,12 @@ function poolColor(pool) {
|
||||
return '#409eff'
|
||||
}
|
||||
|
||||
function formatMem(mb) {
|
||||
if (!mb) return '-'
|
||||
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB'
|
||||
return mb + ' MB'
|
||||
}
|
||||
|
||||
let refreshTimer = null
|
||||
|
||||
async function loadData() {
|
||||
|
||||
@@ -63,9 +63,23 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="vcpus" label="CPU" width="80" align="center" />
|
||||
<el-table-column label="内存" width="110" align="center">
|
||||
<template #default="{ row }">{{ formatMem(row.memory_mb) }}</template>
|
||||
<el-table-column label="CPU" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.state === 'running' && row.cpu_percent !== undefined">
|
||||
<span :style="{ color: row.cpu_percent > 80 ? '#f56c6c' : row.cpu_percent > 50 ? '#e6a23c' : '#67c23a' }">
|
||||
{{ row.cpu_percent }}%</span> / {{ row.vcpus }}核
|
||||
</span>
|
||||
<span v-else>{{ row.vcpus }}核</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内存" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.state === 'running' && row.memory_rss_mb">
|
||||
<span :style="{ color: row.memory_usage_percent > 80 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ formatMem(row.memory_rss_mb) }}</span> / {{ formatMem(row.memory_mb) }}
|
||||
</span>
|
||||
<span v-else>{{ formatMem(row.memory_mb) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="IP 地址" min-width="140">
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/bin/bash
|
||||
# KVM Manager 一键启动脚本
|
||||
# 用法: ./start.sh [start|stop|restart|status]
|
||||
set -e
|
||||
|
||||
# ── 配置 ──────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BACKEND_DIR="$SCRIPT_DIR/backend"
|
||||
FRONTEND_DIR="$SCRIPT_DIR/frontend"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
PID_DIR="$SCRIPT_DIR/.pid"
|
||||
|
||||
BACKEND_PORT=8004
|
||||
FRONTEND_PORT=8006
|
||||
BACKEND_CMD="$BACKEND_DIR/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port $BACKEND_PORT"
|
||||
FRONTEND_CMD="$BACKEND_DIR/venv/bin/python $FRONTEND_DIR/serve.py"
|
||||
|
||||
# ── 颜色 ──────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $1"; }
|
||||
|
||||
mkdir -p "$LOG_DIR" "$PID_DIR"
|
||||
|
||||
# 检查端口是否被占用,返回 PID
|
||||
port_pid() {
|
||||
lsof -ti:$1 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
# 等待端口可用
|
||||
wait_port() {
|
||||
local port=$1 name=$2 max=10 count=0
|
||||
while [ $count -lt $max ]; do
|
||||
if port_pid $port >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
count=$((count + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── 启动后端 ──────────────────────────────────────────
|
||||
start_backend() {
|
||||
info "启动后端服务 (端口 $BACKEND_PORT)..."
|
||||
|
||||
# 检查是否已运行
|
||||
local existing_pid=$(port_pid $BACKEND_PORT)
|
||||
if [ -n "$existing_pid" ]; then
|
||||
warn "后端已在运行 (PID: $existing_pid)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查 venv
|
||||
if [ ! -d "$BACKEND_DIR/venv" ]; then
|
||||
fail "未找到 Python 虚拟环境,请先运行: cd backend && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查 libvirtd
|
||||
if ! systemctl is-active --quiet libvirtd 2>/dev/null; then
|
||||
warn "libvirtd 未运行,尝试启动..."
|
||||
systemctl start libvirtd 2>/dev/null || warn "libvirtd 启动失败,后端可能无法连接虚拟机"
|
||||
fi
|
||||
|
||||
cd "$BACKEND_DIR"
|
||||
nohup $BACKEND_CMD > "$LOG_DIR/backend.log" 2>&1 &
|
||||
local pid=$!
|
||||
echo $pid > "$PID_DIR/backend.pid"
|
||||
|
||||
sleep 2
|
||||
if port_pid $BACKEND_PORT >/dev/null 2>&1; then
|
||||
ok "后端服务已启动 (PID: $pid, 端口: $BACKEND_PORT)"
|
||||
ok "日志: $LOG_DIR/backend.log"
|
||||
else
|
||||
fail "后端服务启动失败,查看日志:"
|
||||
tail -20 "$LOG_DIR/backend.log"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 启动前端 ──────────────────────────────────────────
|
||||
start_frontend() {
|
||||
info "启动前端服务 (端口 $FRONTEND_PORT)..."
|
||||
|
||||
local existing_pid=$(port_pid $FRONTEND_PORT)
|
||||
if [ -n "$existing_pid" ]; then
|
||||
warn "前端已在运行 (PID: $existing_pid)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查 dist
|
||||
if [ ! -d "$FRONTEND_DIR/dist" ]; then
|
||||
warn "未找到前端构建产物,正在构建..."
|
||||
cd "$FRONTEND_DIR"
|
||||
npm install --silent && npm run build
|
||||
if [ ! -d "$FRONTEND_DIR/dist" ]; then
|
||||
fail "前端构建失败"
|
||||
return 1
|
||||
fi
|
||||
ok "前端构建完成"
|
||||
fi
|
||||
|
||||
cd "$FRONTEND_DIR"
|
||||
nohup $FRONTEND_CMD > "$LOG_DIR/frontend.log" 2>&1 &
|
||||
local pid=$!
|
||||
echo $pid > "$PID_DIR/frontend.pid"
|
||||
|
||||
sleep 2
|
||||
if port_pid $FRONTEND_PORT >/dev/null 2>&1; then
|
||||
ok "前端服务已启动 (PID: $pid, 端口: $FRONTEND_PORT)"
|
||||
ok "日志: $LOG_DIR/frontend.log"
|
||||
else
|
||||
fail "前端服务启动失败,查看日志:"
|
||||
tail -20 "$LOG_DIR/frontend.log"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 停止服务 ──────────────────────────────────────────
|
||||
stop_service() {
|
||||
local name=$1 port=$2 pidfile=$PID_DIR/$3.pid
|
||||
info "停止 $name (端口 $port)..."
|
||||
|
||||
# 先尝试 PID 文件
|
||||
if [ -f "$pidfile" ]; then
|
||||
local pid=$(cat "$pidfile")
|
||||
if ps -p "$pid" >/dev/null 2>&1; then
|
||||
kill "$pid" 2>/dev/null && ok "$name 已停止 (PID: $pid)" || warn "$name 停止失败"
|
||||
fi
|
||||
rm -f "$pidfile"
|
||||
fi
|
||||
|
||||
# 再检查端口残留
|
||||
local residual=$(port_pid $port)
|
||||
if [ -n "$residual" ]; then
|
||||
kill "$residual" 2>/dev/null && ok "$name 残留进程已清理 (PID: $residual)" || true
|
||||
fi
|
||||
}
|
||||
|
||||
stop_all() {
|
||||
info "停止所有服务..."
|
||||
stop_service "后端" $BACKEND_PORT "backend"
|
||||
stop_service "前端" $FRONTEND_PORT "frontend"
|
||||
ok "所有服务已停止"
|
||||
}
|
||||
|
||||
# ── 状态 ──────────────────────────────────────────────
|
||||
show_status() {
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} KVM Manager 服务状态${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
|
||||
# 后端
|
||||
local bpid=$(port_pid $BACKEND_PORT)
|
||||
if [ -n "$bpid" ]; then
|
||||
echo -e " 后端: ${GREEN}● 运行中${NC} PID: $bpid 端口: $BACKEND_PORT"
|
||||
else
|
||||
echo -e " 后端: ${RED}○ 未运行${NC}"
|
||||
fi
|
||||
|
||||
# 前端
|
||||
local fpid=$(port_pid $FRONTEND_PORT)
|
||||
if [ -n "$fpid" ]; then
|
||||
echo -e " 前端: ${GREEN}● 运行中${NC} PID: $fpid 端口: $FRONTEND_PORT"
|
||||
else
|
||||
echo -e " 前端: ${RED}○ 未运行${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " 访问地址: ${YELLOW}http://localhost:$FRONTEND_PORT${NC}"
|
||||
echo -e " API文档: ${YELLOW}http://localhost:$BACKEND_PORT/docs${NC}"
|
||||
echo -e " 日志目录: ${YELLOW}$LOG_DIR${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── 查看日志 ──────────────────────────────────────────
|
||||
show_logs() {
|
||||
local target=${1:-all}
|
||||
case $target in
|
||||
backend|b) tail -f "$LOG_DIR/backend.log" 2>/dev/null || fail "后端日志不存在" ;;
|
||||
frontend|f) tail -f "$LOG_DIR/frontend.log" 2>/dev/null || fail "前端日志不存在" ;;
|
||||
*) tail -f "$LOG_DIR/backend.log" "$LOG_DIR/frontend.log" 2>/dev/null || fail "日志不存在" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── 主入口 ────────────────────────────────────────────
|
||||
usage() {
|
||||
echo ""
|
||||
echo -e "${CYAN}KVM Manager 一键启动脚本${NC}"
|
||||
echo ""
|
||||
echo "用法: $0 <命令>"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " start 启动所有服务 (默认)"
|
||||
echo " stop 停止所有服务"
|
||||
echo " restart 重启所有服务"
|
||||
echo " status 查看服务状态"
|
||||
echo " logs 查看日志 (logs backend/frontend)"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 # 启动所有"
|
||||
echo " $0 start # 启动所有"
|
||||
echo " $0 stop # 停止所有"
|
||||
echo " $0 restart # 重启所有"
|
||||
echo " $0 status # 查看状态"
|
||||
echo " $0 logs backend # 查看后端日志"
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start)
|
||||
start_backend
|
||||
start_frontend
|
||||
show_status
|
||||
;;
|
||||
stop)
|
||||
stop_all
|
||||
;;
|
||||
restart)
|
||||
stop_all
|
||||
sleep 1
|
||||
start_backend
|
||||
start_frontend
|
||||
show_status
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
logs)
|
||||
show_logs "${2:-all}"
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user