243 lines
9.2 KiB
Python
243 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})
|