后端 API (routes_docker.py): - /api/docker/containers - 容器列表(状态+资源stats: CPU/内存/网络I/O/磁盘I/O/进程数) - /api/docker/container/<id>/stats - 单容器实时资源 - /api/docker/images - 镜像列表(大小/时间/悬空标记) - /api/docker/system - 系统信息(版本/磁盘占用) - /api/docker/container/<id>/start|stop|restart - 容器控制 - /api/docker/container/<id>/logs - 容器日志 - /api/docker/prune - 资源清理 前端 Docker 监控 Tab: - 统计卡片: 总容器/运行中/已停止/镜像数/磁盘占用/可回收 - 容器列表: 状态灯+CPU/内存/NET IO/磁盘IO/端口/运行时间 - 操作按钮: 启动/停止/重启/查看日志 - 镜像列表: 有效镜像+悬空镜像分区显示 - 清理: 容器/镜像/全部一键清理
244 lines
9.2 KiB
Python
244 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Docker 监控 API 路由
|
||
"""
|
||
|
||
import subprocess
|
||
import json
|
||
from flask import jsonify, request
|
||
from utils import log_message
|
||
|
||
|
||
def _run_docker(cmd, timeout=10):
|
||
"""执行 docker 命令并返回解析结果"""
|
||
try:
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||
if result.returncode != 0:
|
||
return None, result.stderr.strip()
|
||
return result.stdout.strip(), None
|
||
except subprocess.TimeoutExpired:
|
||
return None, '命令超时'
|
||
except FileNotFoundError:
|
||
return None, 'Docker 未安装'
|
||
except Exception as e:
|
||
return None, str(e)
|
||
|
||
|
||
def register_docker_routes(app):
|
||
"""注册 Docker 监控相关路由"""
|
||
|
||
@app.route('/api/docker/containers')
|
||
def api_docker_containers():
|
||
"""获取所有容器列表(含资源使用)"""
|
||
# 获取容器列表
|
||
fmt = '{{.ID}}|{{.Names}}|{{.Image}}|{{.State}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}|{{.RunningFor}}'
|
||
out, err = _run_docker(['docker', 'ps', '-a', '--format', fmt, '--no-trunc'])
|
||
if out is None:
|
||
return jsonify({'error': err, 'containers': []})
|
||
|
||
containers = []
|
||
for line in out.split('\n'):
|
||
if not line.strip():
|
||
continue
|
||
parts = line.split('|')
|
||
if len(parts) < 8:
|
||
continue
|
||
cid = parts[0][:12] # 缩短ID
|
||
containers.append({
|
||
'id': cid,
|
||
'name': parts[1],
|
||
'image': parts[2],
|
||
'state': parts[3],
|
||
'status': parts[4],
|
||
'ports': parts[5],
|
||
'created': parts[6],
|
||
'runningFor': parts[7]
|
||
})
|
||
|
||
# 批量获取运行中容器的stats
|
||
running = [c for c in containers if c['state'] == 'running']
|
||
if running:
|
||
try:
|
||
stats_out, _ = _run_docker([
|
||
'docker', 'stats', '--no-stream', '--no-trunc',
|
||
'--format', '{{.Name}}|{{.CPUPerc}}|{{.MemPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}'
|
||
], timeout=15)
|
||
if stats_out:
|
||
stats_map = {}
|
||
for line in stats_out.split('\n'):
|
||
if not line.strip():
|
||
continue
|
||
sp = line.split('|')
|
||
if len(sp) >= 7:
|
||
stats_map[sp[0]] = {
|
||
'cpuPercent': sp[1],
|
||
'memPercent': sp[2],
|
||
'memUsage': sp[3],
|
||
'netIO': sp[4],
|
||
'blockIO': sp[5],
|
||
'pids': sp[6]
|
||
}
|
||
for c in containers:
|
||
if c['name'] in stats_map:
|
||
c['stats'] = stats_map[c['name']]
|
||
except:
|
||
pass
|
||
|
||
return jsonify({'containers': containers})
|
||
|
||
@app.route('/api/docker/container/<container_id>/stats')
|
||
def api_docker_container_stats(container_id):
|
||
"""获取单个容器实时资源统计"""
|
||
out, err = _run_docker([
|
||
'docker', 'stats', '--no-stream', '--no-trunc',
|
||
'--format', '{{.CPUPerc}}|{{.MemPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}',
|
||
container_id
|
||
], timeout=10)
|
||
if out is None:
|
||
return jsonify({'error': err}), 500
|
||
if not out:
|
||
return jsonify({'error': '容器不存在或未运行'}), 404
|
||
|
||
parts = out.split('|')
|
||
return jsonify({
|
||
'cpuPercent': parts[0] if len(parts) > 0 else '0%',
|
||
'memPercent': parts[1] if len(parts) > 1 else '0%',
|
||
'memUsage': parts[2] if len(parts) > 2 else '',
|
||
'netIO': parts[3] if len(parts) > 3 else '',
|
||
'blockIO': parts[4] if len(parts) > 4 else '',
|
||
'pids': parts[5] if len(parts) > 5 else '0'
|
||
})
|
||
|
||
@app.route('/api/docker/images')
|
||
def api_docker_images():
|
||
"""获取所有镜像列表"""
|
||
fmt = '{{.Repository}}|{{.Tag}}|{{.ID}}|{{.Size}}|{{.CreatedAt}}|{{.Containers}}'
|
||
out, err = _run_docker(['docker', 'images', '--format', fmt, '--no-trunc'])
|
||
if out is None:
|
||
return jsonify({'error': err, 'images': []})
|
||
|
||
images = []
|
||
for line in out.split('\n'):
|
||
if not line.strip():
|
||
continue
|
||
parts = line.split('|')
|
||
if len(parts) < 6:
|
||
continue
|
||
repo = parts[0] if parts[0] != '<none>' else ''
|
||
tag = parts[1] if parts[1] != '<none>' else ''
|
||
images.append({
|
||
'repository': repo,
|
||
'tag': tag,
|
||
'id': parts[2][:12],
|
||
'size': parts[3],
|
||
'created': parts[4],
|
||
'containers': parts[5],
|
||
'dangling': not repo and not tag
|
||
})
|
||
|
||
return jsonify({'images': images})
|
||
|
||
@app.route('/api/docker/system')
|
||
def api_docker_system():
|
||
"""获取 Docker 系统信息"""
|
||
# 版本信息
|
||
ver_out, _ = _run_docker(['docker', 'version', '--format', '{{.Server.Version}}|{{.Server.Os}}|{{.Server.Arch}}'], timeout=5)
|
||
|
||
# 磁盘使用
|
||
df_out, _ = _run_docker(['docker', 'system', 'df', '--format', '{{.Type}}|{{.TotalCount}}|{{.Active}}|{{.Size}}|{{.Reclaimable}}'], timeout=10)
|
||
|
||
system_info = {
|
||
'version': '',
|
||
'os': '',
|
||
'arch': '',
|
||
'diskUsage': []
|
||
}
|
||
|
||
if ver_out:
|
||
ver_parts = ver_out.split('|')
|
||
if len(ver_parts) >= 3:
|
||
system_info.update({
|
||
'version': ver_parts[0],
|
||
'os': ver_parts[1],
|
||
'arch': ver_parts[2]
|
||
})
|
||
|
||
if df_out:
|
||
for line in df_out.split('\n'):
|
||
if not line.strip():
|
||
continue
|
||
parts = line.split('|')
|
||
if len(parts) >= 5:
|
||
system_info['diskUsage'].append({
|
||
'type': parts[0],
|
||
'total': parts[1],
|
||
'active': parts[2],
|
||
'size': parts[3],
|
||
'reclaimable': parts[4]
|
||
})
|
||
|
||
return jsonify(system_info)
|
||
|
||
@app.route('/api/docker/container/<container_id>/logs')
|
||
def api_docker_container_logs(container_id):
|
||
"""获取容器日志(最近200行)"""
|
||
lines = int(request.args.get('lines', 200))
|
||
lines = max(10, min(1000, lines))
|
||
|
||
out, err = _run_docker(['docker', 'logs', '--tail', str(lines), container_id], timeout=10)
|
||
if out is None:
|
||
return jsonify({'error': err}), 500
|
||
|
||
return jsonify({'logs': out, 'lines': lines})
|
||
|
||
@app.route('/api/docker/container/<container_id>/start', methods=['POST'])
|
||
def api_docker_container_start(container_id):
|
||
"""启动容器"""
|
||
out, err = _run_docker(['docker', 'start', container_id])
|
||
if out is None and err:
|
||
return jsonify({'error': err}), 500
|
||
log_message(f"Docker 容器启动: {container_id}")
|
||
return jsonify({'message': f'容器 {container_id} 已启动'})
|
||
|
||
@app.route('/api/docker/container/<container_id>/stop', methods=['POST'])
|
||
def api_docker_container_stop(container_id):
|
||
"""停止容器"""
|
||
out, err = _run_docker(['docker', 'stop', container_id])
|
||
if out is None and err:
|
||
return jsonify({'error': err}), 500
|
||
log_message(f"Docker 容器停止: {container_id}")
|
||
return jsonify({'message': f'容器 {container_id} 已停止'})
|
||
|
||
@app.route('/api/docker/container/<container_id>/restart', methods=['POST'])
|
||
def api_docker_container_restart(container_id):
|
||
"""重启容器"""
|
||
out, err = _run_docker(['docker', 'restart', container_id])
|
||
if out is None and err:
|
||
return jsonify({'error': err}), 500
|
||
log_message(f"Docker 容器重启: {container_id}")
|
||
return jsonify({'message': f'容器 {container_id} 已重启'})
|
||
|
||
@app.route('/api/docker/prune', methods=['POST'])
|
||
def api_docker_prune():
|
||
"""清理Docker资源"""
|
||
target = request.json.get('target', 'all') if request.json else 'all'
|
||
results = {}
|
||
|
||
# 清理停止的容器
|
||
if target in ('all', 'containers'):
|
||
out, _ = _run_docker(['docker', 'container', 'prune', '-f'], timeout=30)
|
||
results['containers'] = out or '已清理'
|
||
|
||
# 清理悬空镜像
|
||
if target in ('all', 'images'):
|
||
out, _ = _run_docker(['docker', 'image', 'prune', '-f'], timeout=60)
|
||
results['images'] = out or '已清理'
|
||
|
||
# 清理构建缓存
|
||
if target in ('all', 'build'):
|
||
out, _ = _run_docker(['docker', 'builder', 'prune', '-f'], timeout=60)
|
||
results['build'] = out or '已清理'
|
||
|
||
log_message(f"Docker 资源清理完成: {target}")
|
||
return jsonify({'message': '清理完成', 'results': results})
|