提交 c0200115 authored 作者: 陈泽健's avatar 陈泽健

feat(service-monitor): 新增安全合规/中间件检测模块并修复状态等级误判

检测项丰富化移植(阶段二):
- 新增 39_middleware_conn.sh 中间件连接检测(Redis/MySQL/EMQX/FastDFS)
- 新增 46_security_deep.sh 安全合规深度检测(9类:弱密码/基线/可疑文件等)
- check_modules.py 注册 2 个新模块
- display_names.py 追加 36 个 KEY 映射
- thresholds.py 追加 6 个安全阈值
- config.sh.template 追加安全白名单 + EMQX 端口配置

状态等级误判修复:
- 39/46 脚本 LEVEL KEY 对齐为 <值KEY>_LEVEL 格式(与 _resolve_status 约定一致)
- 23_mysql_depth / 25_redis_depth 重构为 output_result 格式
- parser.py _VALUE_STATUS_MAP 补充 11 个异常状态词映射(兜底)

中间件端口检测逻辑重构:
- 改用服务自带工具 ping 优先(redis-cli/mysqladmin/curl API)
- 避免容器内无 ss/netstat 导致误判"端口未监听"

服务管理模块(附带):
- routes/service_manage.py 新增授权/升级/信息页面路由
- templates/service_manage/ 新增 4 个模板

文档:更新 HANDOFF_服务监测.md 记录修复全过程 + 检测项丰富化 PRD
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 3f48af4c
# 需求文档:检测项丰富化(参考目录移植)
## 文档信息
- 创建时间:2026-07-22
- 需求来源:用户需求
- 关联模块:service_monitor
- 参考目录:`E:\GithubData\ubains-module-test\platform-auto-test\临时目录\服务监测\`
---
## 1. 背景与目标
### 1.1 背景
当前服务监测模块有 30 个检测脚本(13 系统 + 17 服务),display_names.py 收录 539 个检测项 KEY。用户希望对比参考目录(老版 PowerShell + Shell 巡检工具集)继续丰富检测项。
参考目录是平台自动化测试项目中的旧版巡检工具,采用 PowerShell 模块 + Shell 脚本混合架构,检测维度比当前项目更广。
### 1.2 目标
1. **补全已有脚本输出但未收录的 KEY**:当前部分脚本输出的 KEY 未在 display_names.py 中定义
2. **重构 depth 脚本**:`23_mysql_depth.sh` 和 `25_redis_depth.sh` 使用 `echo` 而非 `output_result`,需重构以适配标准解析器
3. **移植参考目录特有的全新检测类别**:18 类新检测项
---
## 2. 当前项目已有检测类别(30 个脚本)
### 系统类(13)
01_system_basic, 02_cpu_check, 03_memory_check, 04_disk_check, 05_oom_check, 06_process_check, 07_network_check, 11_scheduled_tasks, 12_port_check, 40_comprehensive_diagnosis, 43_security_compliance, 44_system_logs, 45_time_sync
### 服务类(17)
20_docker_basic, 21_docker_deep, 22_mysql_basic, 23_mysql_depth, 24_redis_basic, 25_redis_depth, 26_emqx_basic, 27_emqx_deep, 28_java_check, 29_python_check, 30_nginx_check, 31_nacos_check, 33_app_logs, 34_nginx_deep, 35_nacos_deep, 37_ustorage_check, 38_utracker_check
---
## 3. 阶段一:补全已有脚本缺失的 KEY
### 3.1 display_names.py 缺失项
| 模块 | 缺失 KEY | 中文名 |
|------|---------|--------|
| 03_memory | VM_PGMajFAULT | 主缺页错误数 |
| 21_docker_deep | DOCKER_DAEMON_CPU | Docker守护进程CPU |
| 21_docker_deep | DOCKER_DAEMON_MEM | Docker守护进程内存 |
| 34_nginx_deep | NGINX_CONTAINER_LEVEL | Nginx容器级别 |
| 35_nacos_deep | NACOS_CONTAINER_LEVEL | Nacos容器级别 |
### 3.2 depth 脚本重构
**23_mysql_depth.sh** 当前用 `echo "KEY:value"`,需改为 `output_result "KEY" "value"`,KEY 加 `MYSQL_DEPTH_` 前缀避免冲突:
| 当前 KEY | 重构后 KEY | 中文名 |
|---------|-----------|--------|
| hit_rate | MYSQL_DEPTH_BUFFER_POOL_HIT_RATE | 缓冲池命中率 |
| slow_enabled | MYSQL_DEPTH_SLOW_QUERY_ENABLED | 慢查询日志开启 |
| slow_count | MYSQL_DEPTH_SLOW_QUERY_COUNT | 慢查询数量 |
| conn_rate | MYSQL_DEPTH_CONNECTION_USAGE | 连接使用率 |
| io_running | MYSQL_DEPTH_SLAVE_IO_RUNNING | 从库IO线程 |
| sql_running | MYSQL_DEPTH_SLAVE_SQL_RUNNING | 从库SQL线程 |
| seconds_behind | MYSQL_DEPTH_SLAVE_DELAY | 主从延迟秒数 |
| table_count | MYSQL_DEPTH_TABLE_COUNT | 表数量 |
| qps | MYSQL_DEPTH_QPS | 每秒查询数 |
| uptime | MYSQL_DEPTH_UPTIME_DAYS | 运行天数 |
| ... | ... | ... |
**25_redis_depth.sh** 同理,KEY 加 `REDIS_DEPTH_` 前缀。
---
## 4. 阶段二:移植全新检测类别(18 类)
### P1 优先级(深度检测 + 安全 + 中间件连接)
| 编号 | 新模块 | 检测项 | 参考来源 |
|------|--------|--------|---------|
| 46 | MySQL深度检测(重构) | 缓冲池命中率、慢查询、连接使用率、主从复制、TOP大表、QPS基线 | check_mysql_deep.sh, MiddlewareCheck.psm1 |
| 47 | Redis深度检测(重构) | RDB/AOF持久化、内存碎片率、缓存命中率、主从复制、键空间统计 | check_redis.sh, MiddlewareCheck.psm1 |
| 48 | 安全合规深度检测 | MySQL Root访问范围、空密码用户、Redis密码、EMQX默认密码、SUID文件、异常crontab、SSH暴力破解、异常端口 | SecurityCheck.psm1 |
| 49 | 中间件连接检测 | MQTT订阅、MQTT推送、Redis连接、MySQL连接、FastDFS上传下载 | middleware_check.sh, MiddlewareCheck.psm1 |
### P2 优先级(平台服务检测)
| 编号 | 新模块 | 检测项 | 参考来源 |
|------|--------|--------|---------|
| 50 | 系统类型识别 | ujava/upython/upython_voice 变体识别 | ServerProfile.psm1 |
| 51 | 平台服务检测 | auth/gateway/system/meeting/mqtt/quartz/message/extapi 服务状态 | ServiceCheck.psm1 |
| 52 | 配置IP检测 | 硬编码IP扫描、配置文件IP检查 | ConfigIPCheck.psm1, config_check.sh |
| 53 | Console配置检测 | true→false 配置检查 | check_server_health.sh |
### P3 优先级(辅助功能)
| 编号 | 新模块 | 检测项 | 参考来源 |
|------|--------|--------|---------|
| 54 | 文件权限检测 | 关键文件权限检查 | FilePermission.psm1 |
| 55 | 定时任务预期检测 | 预期任务缺失检测 | check_server_health.sh |
| 56 | FastDFS功能验证 | 上传/下载/完整性测试 | check_fdfs_arm.sh |
| 57 | Android设备检测 | adb连接、日志导出 | AndroidCheck.psm1 |
| 58 | 数据备份检测 | MySQL dump、打包、下载 | DataBackup.psm1 |
| 59 | 配置日志导出 | 远程配置/日志拉取 | LogExport.psm1 |
| 60 | 修复能力检测 | DNS/NTP/端口/容器修复链 | issue_handler.sh |
---
## 5. 技术方案
### 5.1 每个新模块的完整实现
每个新检测类别需要:
1. **bash 脚本**:`assets/system/XX_name.sh` 或 `assets/service/XX_name.sh`
- 遵循现有脚本格式:加载 config.sh + common.sh,使用 `output_result "KEY" "value"`
- 容器名用 `CONTAINERS[name]` 模糊匹配模式
2. **模块注册**:`check_modules.py` 的 `ALL_MODULES` 列表新增 `CheckModule` 条目
3. **显示名**:`display_names.py` 新增 KEY→中文名映射
4. **阈值**(如适用):`thresholds.py` 新增阈值规则
5. **config 模板**(如需凭据):`config.sh.template` 新增容器/凭据配置
6. **上传清单**:`deploy/upload_to_server.py` 确认脚本会被上传(assets 目录已整体上传)
### 5.2 移植注意事项
- **参考目录是 PowerShell**:需翻译为 bash,保留检测逻辑
- **参考目录是 Shell 老格式**:需适配 `output_result` 标准输出
- **容器名匹配**:参考目录用精确名,本项目用 `grep -iE` 模糊匹配
- **凭据注入**:参考目录硬编码,本项目用 `__占位符__` + executor 渲染
- **阈值**:参考目录内嵌,本项目用 `thresholds.py` 集中管理
### 5.3 测试策略
每个新模块:
1. 本地语法检查:`bash -n script.sh`
2. 单元测试:`check_modules.test_get_suite` 等已有测试通过
3. 部署后在目标机实际运行验证
4. 统计页面确认新检测项出现在选择器中
---
## 6. 验收标准
| 标准 | 验证方法 |
|------|---------|
| display_names.py 缺失项补全 | 脚本输出 KEY 都有中文名 |
| depth 脚本重构完成 | 用 output_result 格式,统计页面可识别 |
| 新模块 bash 脚本创建 | 语法检查通过 |
| check_modules.py 注册 | get_suite('full') 包含新模块 |
| display_names.py 新增映射 | 新 KEY 有中文名 |
| 统计页面显示新检测项 | 选择器中出现新模块和检测项 |
| 单元测试通过 | pytest 全绿 |
---
## 7. 风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 工作量大,单会话无法完成 | 进度缓慢 | 分阶段实现,每阶段独立可部署 |
| bash 脚本在目标机环境差异 | 检测失败 | 容器名模糊匹配 + 命令存在性检查 |
| 参考目录逻辑复杂 | 移植出错 | 保留核心检测逻辑,简化边缘功能 |
| 新增 KEY 命名冲突 | 解析混乱 | 加模块前缀(MYSQL_DEPTH_ 等) |
---
## 8. 执行路线图
- **阶段一**(本次):补全 display_names 缺失项 + 重构 2 个 depth 脚本
- **阶段二**:P1 优先级 4 个新模块(MySQL深度/Redis深度/安全合规深度/中间件连接)
- **阶段三**:P2 优先级 4 个新模块(系统类型/平台服务/配置IP/Console配置)
- **阶段四**:P3 优先级 7 个新模块(文件权限/定时任务预期/FastDFS/Android/数据备份/日志导出/修复能力)
# HANDOFF_服务监测 — 服务监测模块:检测项丰富化移植(阶段二)
> 最后更新:2026-07-22(状态等级误判 + 端口检测逻辑修复,5.44 实际运行验证通过)| 分支:troubleshoot-ai-assistant | 负责人:czj
> 专属交接文档:聚焦服务监测模块(service_monitor)的全部历史与当前状态
---
## 1. 我们在做什么
服务监测模块(service_monitor)是运行维护平台的子模块,提供目标服务器巡检、报告管理、定时任务、通知推送等功能。
**已完成轮次**
- **阶段一**:补全缺失项 + 重构 MySQL/Redis 深度检测脚本(见 2.5 节)
- **阶段二(本次)**:移植安全合规深度检测 + 中间件连接检测模块
本轮会话完成了两个新检测模块的移植:
1. **安全合规深度检测**(46_security_deep.sh):检测弱密码、安全基线违规、可疑文件、异常配置。包含 9 类安全检测:
- MySQL Root 访问范围、空密码用户
- Redis 密码状态
- EMQX 默认密码检测
- Linux 空密码账户
- SUID 文件安全检查
- Crontab 异常任务
- SSH 暴力破解检测
- 异常端口监听
2. **中间件连接检测**(39_middleware_conn.sh):验证 Redis/MySQL/EMQX/FastDFS 连接状态和运行健康度。包含 4 类连接检测:
- Redis 连接(容器存在 + 端口监听 + ping 测试 + 版本/内存/连接数)
- MySQL 连接(容器存在 + 端口监听 + mysqladmin ping + 版本)
- EMQX 连接(MQTT 端口 + Dashboard 端口 + API 状态)
- FastDFS 连接(容器存在 + 进程检查)
---
## 2. 已经完成了什么
### 2.1 报告免登访问与操作鉴权 ✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_报告免登访问与操作鉴权.md`
**核心实现**
- `report_service.py`:报告生成时自动生成 `access_token`(格式 `rpt_<32位随机hex>`)和 `access_token_expires_at`(7天有效期)
- `runner_service.py`:通知链接拼接 `?token=rpt_xxx` 参数
- `routes.py``page_report()` 支持 token 验证,验证通过后渲染报告页并标记 `token_auth=True`(访客模式);`api_export_report()` 支持 token 导出
- `auth.py`:GET/POST `/login` 支持 `next` 参数,登录成功后跳转回原始页面
- `decorators.py``page_login_required` 重定向时携带 `next` 参数
- `login.html`:表单提交携带 `next` 参数,登录成功后跳转
- `report.html`:访客模式显示提示条、隐藏管理操作、导出链接携带 token
- `base.html`:访客模式下导航菜单点击跳转登录页,顶部显示"访客 + 登录按钮"
**验证结果**
- 未登录点击通知链接 → 直接查看报告 ✅
- 访客模式点击其他模块 → 弹窗提示并跳转登录 ✅
- 登录后回跳到原始页面 ✅
- 218 个单元测试全绿 ✅
### 2.2 监测统计模块 ✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_监测统计模块.md`
**核心实现**
- `statistics_service.py`(新增):统计聚合服务,包含 5 个公开函数:
- `get_overview()`:概览摘要(总报告数、异常率、最常异常项等)
- `get_module_stats()`:模块健康统计
- `get_item_trend()`:检测项趋势数据(时间序列 + 状态分布)
- `get_abnormal_items()`:高频异常检测项 TOP 10
- `get_item_keys_for_target()`:检测项列表(按模块分组,含 `has_numeric` 标记)
- `routes.py`:新增页面路由 `/service-monitor/statistics` + 5 个 API 端点
- `base.html`:侧边栏添加"📈 监测统计"菜单项
- `statistics.html`(新增):完整统计页面,使用 ECharts 渲染图表
- 筛选栏:目标选择器 + 日期范围(自定义)
- 概览卡片:4 项摘要指标
- 模块健康概览:堆叠条形图
- 检测项趋势分析:模块/检测项两级联动选择器 + 折线图 + 饼图
- 高频异常检测项:水平堆叠条形图
**验证结果**
- 统计页面正常加载 ✅
- 各 API 返回正确数据结构 ✅
- 图表正常渲染 ✅
- 218 个单元测试全绿 ✅
### 2.3 检测项趋势图表显示问题修复 ✅
**问题文档**`Docs/需求文档/服务监测/PRD_问题处理_检测项趋势图表显示问题.md`
**问题根因**
- 部分检测项的值是状态文本("正常"/"异常")、yes/no、路径等,`_extract_numeric()` 无法提取数值
- 前端收到 `value_num=null` 后折线图无数据点,但未显示任何提示
**修复内容**
- `statistics_service.py``get_item_keys_for_target()` 增加 `has_numeric` 字段,标记检测项是否有数值数据
- `statistics.html``fetchItemTrend()` 增加三种情况处理:
- 无数据点:显示"暂无数据"
- 有数据点但无数值:显示友好提示"该检测项为状态/文本类型,无法绘制数值趋势图,请查看右侧状态分布",饼图正常渲染
- 有数值数据:正常渲染折线图 + 饼图
- 检测项选择器标注:无数值能力的检测项标注 "(状态)" 后缀
**验证结果**
- 选择 `CPU_USAGE` 等数值项,折线图正常 ✅
- 选择 `CPU_STATUS` 等状态项,显示友好提示 + 饼图正常 ✅
- 选择器中状态项标注 "(状态)" ✅
### 2.4 JavaScript 代码残留问题修复 ✅
**问题文档**`Docs/需求文档/服务监测/PRD_问题处理_统计页面JS错误.md`
**问题根因**
- 编辑 `fetchItemKeys` 函数时,旧代码尾部 4 行未被删除,残留在新函数之后
- 导致 `onModuleChange``fetchItemTrend``fetchAllData` 等函数被错误嵌套,全局作用域无法访问 `fetchAllData`
**修复内容**
- 删除第 330-333 行、第 492-494 行的残留代码
- 补回被误删的 `onModuleChange` 函数体
**验证结果**
- 花括号平衡(124 对)✅
- 9 个关键函数全部存在 ✅
- 查询按钮点击正常工作 ✅
### 2.5 检测项丰富化(参考目录移植 - 阶段一)✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_检测项丰富化移植.md`
**参考目录**`platform-auto-test/临时目录/服务监测/`(老版 PowerShell + Shell 巡检工具)
**已完成内容**
**1. display_names.py 补全缺失 KEY**
- 新增 `VM_PGMajFAULT`(主缺页错误数)
- 原有 `DOCKER_DAEMON_CPU/MEM``NGINX_CONTAINER_LEVEL``NACOS_CONTAINER_LEVEL` 已存在
**2. 重构 23_mysql_depth.sh**
-`echo "KEY:value"` 改为 `output_result "KEY" "value"` 标准格式
- KEY 加 `MYSQL_DEPTH_` 前缀,display_names.py 新增 30 个 KEY
- 去掉硬编码密码,使用 `MYSQL_PASSWORD` 环境变量
- 容器名使用 `CONTAINERS[mysql]` 模糊匹配
- thresholds.py 新增 7 个深度阈值(连接使用率/慢查询/缓冲池命中率/主从延迟/死锁/锁等待/碎片表)
**3. 重构 25_redis_depth.sh**
-`echo "KEY:value"` 改为 `output_result "KEY" "value"` 标准格式
- KEY 加 `REDIS_DEPTH_` 前缀,display_names.py 新增 22 个 KEY
- 去掉硬编码密码,使用 `REDIS_PASSWORD` 环境变量
- 容器名使用 `CONTAINERS[redis]` 模糊匹配
- thresholds.py 新增 5 个深度阈值(内存碎片/缓存命中/阻塞客户端/拒绝连接/慢日志)
**验证结果**:218 个测试全绿 ✅ 已部署到 5.60 ✅
### 2.6 参考目录分析完成
参考目录有 18 类全新检测项,当前项目未实现:
- **P1**:MySQL深度/Redis深度/安全合规深度/中间件连接(4类)
- **P2**:系统类型识别/平台服务检测/配置IP检测/Console配置(4类)
- **P3**:文件权限/定时任务预期/FastDFS/Android/数据备份/日志导出/修复能力(7类)
**深度检测重构已完成(阶段一),P1 移植进行中(阶段二已完成 2/4)**
### 2.7 安全合规深度检测模块 ✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_检测项丰富化移植.md`
**参考来源**`platform-auto-test/临时目录/服务监测/modules/SecurityCheck.psm1`
**核心实现**
- `46_security_deep.sh`(新增):安全合规深度检测脚本,9 类安全检测
- MySQL Root 访问范围:查询 `mysql.user` 表 root 用户的 `host` 字段,含 `%` 为严重
- MySQL 空密码用户:查询 `authentication_string` 为空的用户
- Redis 密码状态:检查 `requirepass` 配置,空或弱密码为严重
- EMQX 默认密码:用 `admin:public` 测试 Dashboard API
- Linux 空密码账户:解析 `/etc/shadow` 第 2 字段
- SUID 文件:扫描系统 SUID 文件与白名单对比
- Crontab:收集所有用户/系统级 crontab 与安全模式对比
- SSH 暴力破解:分析 SSH 日志失败登录统计 IP 次数(>=50 次)
- 异常端口:获取监听端口与平台端口白名单对比
- `display_names.py`:新增 20 个 `SECURITY_DEEP_*` KEY 映射
- `thresholds.py`:新增 6 个安全检测阈值
- `config.sh.template`:新增安全白名单配置(SUID 白名单、端口白名单、EMQX Dashboard 端口)
**验证结果**:218 个测试全绿 ✅ 语法检查通过 ✅ **已部署到 5.60**
### 2.8 中间件连接检测模块 ✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_检测项丰富化移植.md`
**参考来源**`platform-auto-test/临时目录/服务监测/lib/shell/middleware_check.sh` + `MiddlewareCheck.psm1`
**核心实现**
- `39_middleware_conn.sh`(新增):中间件连接检测脚本,4 类连接检测
- Redis 连接:容器存在 + 端口监听 + `redis-cli ping` + 版本/内存/连接数
- MySQL 连接:容器存在 + 端口监听 + `mysqladmin ping` + 版本
- EMQX 连接:MQTT 端口监听 + Dashboard 端口 + API 状态 + 进程检查
- FastDFS 连接:容器存在 + 网络模式 + trackerd/storaged 进程检查
- `display_names.py`:新增 16 个 `MW_CONN_*` KEY 映射
- `config.sh.template`:新增 EMQX 端口配置(MQTT 端口、Dashboard 端口)
**验证结果**:218 个测试全绿 ✅ 语法检查通过 ✅ **已部署到 5.60**
### 2.9 状态等级误判问题修复 ✅
**问题文档**:用户报告 Redis/MySQL/EMQX 连接状态"端口未监听"却显示"正常"
**问题根因**
- LEVEL KEY 命名不一致:脚本输出 `MW_CONN_REDIS_LEVEL`,但 `_resolve_status` 查找 `MW_CONN_REDIS_STATUS_LEVEL`
- 状态词映射缺失:`"端口未监听"``"连接失败"` 等异常状态词不在 `_VALUE_STATUS_MAP`
- 优先级 1(LEVEL 字段)失败 → 优先级 3(文本映射)也失败 → 默认"正常"
**修复内容**
- `39_middleware_conn.sh`:4 类连接的 LEVEL KEY 改为 `<STATUS_KEY>_LEVEL` 格式(8 处)
- `46_security_deep.sh`:7 类安全检测的 LEVEL KEY 改为 `<值KEY>_LEVEL` 格式(7 处)
- `display_names.py`:同步 11 个 LEVEL KEY 的显示名映射
- `parser.py``_VALUE_STATUS_MAP` 补充 11 个异常状态词(端口未监听/MQTT 端口未监听/连接失败/进程异常/未检测到容器/容器未运行/无密码/弱密码/使用默认密码/查询失败/无法读取)
**验证结果**:218 个测试全绿 ✅ 语法检查通过 ✅ **已部署到 5.60**
### 2.10 中间件端口检测逻辑重构 ✅
**问题文档**:部署后 5.44 检测仍报"端口未监听"
**问题根因**
- 脚本在**容器内**`ss -tlnp | grep ':6379'` 检查端口
- 5.44 的容器镜像(精简版)**没有 `ss` 和 `netstat` 命令**
- `ss` 命令失败 stderr 被 `2>/dev/null` 吞掉,`port_listening` 变空 → 误判"端口未监听"
**修复内容**
- `39_middleware_conn.sh` 重构检测逻辑:
- Redis:`redis-cli ping` 优先(成功=运行中,NOAUTH=需密码认证,其他=连接失败)
- MySQL:`mysqladmin ping` 优先(在容器内执行,连内部端口)
- EMQX:宿主机 `ss` 检 MQTT 端口 + 容器内进程检查(官方镜像有 `ps`
- FastDFS:进程检查(不变,此前已正确处理 host 网络模式)
**验证结果**
- Redis:运行中,版本 8.2.2,内存 2.23M,连接数 33 ✅
- MySQL:运行中,版本 8.0.28 ✅
- EMQX:运行中,MQTT 端口 1883 正常 ✅
- FastDFS:运行中,host 网络模式 ✅
### 2.11 5.44 目标凭据配置 ✅
**问题**:Redis/MySQL ping 连接失败(NOAUTH / Access denied)
**根因**:5.44 目标的 `credential_overrides` 为空,检测时无密码可用
**修复方式**
- 通过 API 注入加密密码:`mysql_password` / `redis_password` = `dNrprU&2S`
- 密码使用 Fernet 加密存储,运行时解密注入 config.sh
**验证结果**
- 凭据加密存储正确 ✅
- 解密后注入正确 ✅
- full 套件检测 442 项,4 正常 / 4 警告 / 11 严重 ✅
**额外发现的安全问题**(因密码配置后才检测出来):
- MySQL Root 访问范围:`%,localhost`**严重**(允许任意 IP)
- EMQX 默认密码:使用默认密码 → **严重**(admin:public)
- 异常端口数:57 个 → **严重**(大量非白名单端口)
---
## 3. 当前卡在哪
**无卡点**。阶段一(补全缺失项 + 重构 depth 脚本)和阶段二(安全合规深度 + 中间件连接)已完成,状态等级误判和端口检测逻辑已修复,5.44 实际运行验证通过。
**当前状态**
- 服务健康检查:status: ok, scheduler: running ✅
- 报告免登访问正常工作 ✅
- 统计模块正常工作 ✅
- 检测项趋势图表友好提示正常 ✅
- display_names.py 已补全缺失 KEY(539+52+36=627 条)✅
- 23_mysql_depth.sh / 25_redis_depth.sh 已重构为 output_result 格式 ✅
- 46_security_deep.sh 安全合规深度检测模块 ✅
- 39_middleware_conn.sh 中间件连接检测模块 ✅
- full 套件 32 个模块(原 30 + 新 2)✅
- 218 个单元测试全绿 ✅
- **已部署到 5.60** ✅(服务器端语法检查通过,文件完整)
- **5.44 实际运行验证通过**(full 套件,中间件全部"运行中",安全模块发现真实问题)✅
- 状态等级误判已修复(LEVEL KEY 对齐 + 状态词映射兜底)✅
- 中间件端口检测逻辑已修复(服务 ping 优先,不依赖容器内 ss)✅
- 5.44 目标凭据已配置(MySQL/Redis 密码加密注入)✅
---
## 4. 下一步计划
| 优先级 | 任务 | 说明 |
|--------|------|------|
| ~~P1~~ | ~~Web 界面验证实际运行~~ | ✅ 已完成(5.44 full 套件检测通过,发现问题项正确标记严重) |
| ~~P1~~ | ~~统计页面验证~~ | ✅ 已完成(新检测项出现在统计页面选择器中) |
| P2 | 继续移植 P2 优先级模块 | 系统类型识别、平台服务检测、配置IP检测、Console配置 |
| P3 | 继续移植 P3 优先级模块 | 文件权限、定时任务预期、FastDFS功能验证等 |
| 持续 | 更新 HANDOFF_服务监测文档 | 每次会话结束前更新进度 |
---
## 5. 踩过的坑(绝对不要再踩)
### 坑 1:JavaScript 编辑时代码残留
**坑**:使用 Edit 工具替换函数时,`old_string` 未精确匹配完整范围,只替换了函数开头,尾部代码残留。导致后续函数定义在错误的闭包内,全局作用域无法访问。
**避免方法**
- 编辑前完整阅读原代码,确保 `old_string` 匹配完整范围
- 编辑后检查花括号平衡
- 本地测试后再部署
### 坑 2:状态类检测项折线图空白无提示
**坑**`_extract_numeric()` 对非数值("正常"/"异常"/"yes"/"no")返回 `(None, value)`,前端收到 `value_num=null` 后折线图无数据点,但未显示任何提示。用户以为"图表坏了"。
**避免方法**
- 前端处理数据前检查数值情况,无数值时显示友好提示
- 饼图使用 `status` 字段,不依赖数值
### 坑 3:检测项选择器选项过多难找
**坑**:419 个检测项平铺在一个下拉框,用户难以找到目标项。
**避免方法**
- 用两级联动:第一级选模块,第二级选检测项
- 检测项选择器按模块分组(optgroup)
- 标注无趋势能力的检测项
### 坑 4:部署后健康检查误报
**坑**`upload_to_server.py` 结尾的 `[FAIL] Service may not be running` 是误报。服务启动需要加载索引(357 条),sleep 3 秒不够。
**避免方法**
-`curl http://192.168.5.60:8088/api/health` 权威验证
- 不要信赖部署脚本的自动检查
### 坑 5:Shell 脚本硬编码密码
**坑**:参考目录的 `23_mysql_depth.sh``25_redis_depth.sh` 硬编码了密码(`MYSQL_PASSWORD="dNrprU&2S"`),直接复制到项目会泄露凭据。
**避免方法**
- 使用 `MYSQL_PASSWORD` 环境变量(通过 config.sh 注入)
- 容器名使用 `CONTAINERS[name]` 模糊匹配而非硬编码
- 所有输出使用 `output_result "KEY" "value"` 标准函数
### 坑 6:PowerShell 移植到 Bash 的差异
**坑**:参考目录是 PowerShell 模块,直接翻译到 Bash 时需注意:
- PowerShell 的 `@()` 数组 → Bash 用空格分隔字符串 + `for` 循环
- PowerShell 的 `$Config.Key` → Bash 用 `${CONTAINERS[key]}` 关联数组
- PowerShell 的 `Invoke-WebRequest` → Bash 用 `curl`
- PowerShell 的 `Select-Object` → Bash 用 `awk`/`cut`
- PowerShell 的 `-match` → Bash 用 `grep -E`
**避免方法**
- 移植时只保留核心检测逻辑,简化边缘功能
- 容器名用 `grep -iE` 模糊匹配替代精确名
- 凭据用 `__占位符__` + executor 渲染替代硬编码
### 坑 7:部署路径与验证方式
**坑**:部署后想验证新模块,发现:
- `/tmp/check_modules/lib/` 目录为空 —— 这是**检测运行时**临时工作目录,脚本只在运行巡检时才上传到这里
- `/data/web/service_monitor/assets/` 不存在 —— 实际部署路径是 `/opt/troubleshoot/web/service_monitor/`
- 统计 API `item-keys` 返回空 —— 因为统计页面基于**历史报告数据**聚合,新模块没运行过就没有数据
- 登录接口 `/login` 返回 415 —— 后端期望 JSON 格式(`Content-Type: application/json`),不是 form-urlencoded
**避免方法**
- 部署路径:`/opt/troubleshoot/web/service_monitor/`(不是 /data/web)
- 验证模块注册:SSH 检查 `check_modules.py` 包含新模块 ID
- 验证脚本:SSH 检查 `assets/system/``assets/service/` 下文件存在 + `bash -n` 语法检查
- 触发实际运行:必须通过 Web 界面 SSE 流式接口 `/api/service-monitor/run/stream`(需管理员权限)
- Windows 没有 `sshpass`,用 paramiko(项目已依赖)执行 SSH 命令
### 坑 8:LEVEL KEY 命名不一致导致状态误判
**坑**:检测脚本输出 `MW_CONN_REDIS_LEVEL`,但 `_resolve_status` 查找 `MW_CONN_REDIS_STATUS_LEVEL`。LEVEL 字段对不上 → 文本映射也缺失 → 默认"正常"。明明"端口未监听"却显示"正常"。
**避免方法**
- LEVEL KEY 必须严格遵循 `<值KEY>_LEVEL` 格式(`f"{key}_LEVEL"` 精确匹配)
- 文本状态词映射作为双重保险,补充常见异常词
- 新模块开发时先确认已有模块的命名约定
### 坑 9:容器内无 ss/netstat 导致端口检测误判
**坑**:脚本在容器内用 `ss -tlnp | grep ':6379'` 检查端口。但精简镜像(Alpine/slim)没装 `iproute2``ss` 命令失败 stderr 被 `2>/dev/null` 吞掉,结果变量为空 ≠ "yes" → 误判"端口未监听"。
**避免方法**
- 端口检测**不要依赖容器内的 ss/netstat**
- 优先用**服务自带工具**(redis-cli ping / mysqladmin ping / curl API)
- 端口检测用**宿主机 ss**(适用于有端口映射的服务)
- 进程检查用 `ps aux`(几乎所有镜像都有)
### 坑 10:目标凭据未配置导致安全检测失效
**坑**:46_security_deep.sh 检测 Redis 密码状态时,因 `REDIS_PASSWORD` 未配置,`redis-cli INFO``NOAUTH`,脚本误判"未检测到容器"或"查询失败"。实际 Redis 有密码保护,但密码没配进系统,安全漏洞检测不出来。
**避免方法**
- 添加远程目标时必须配置数据库密码(credential_overrides)
- 密码通过 API 加密存储,config.sh 模板用 `__REDIS_PASSWORD__` 占位符
- 部署后验证:跑一次 full 套件,确认安全模块能输出"已设置"而非"查询失败"
---
## 6. 关键文件与命令速查
### 本次修改文件(阶段一:检测项丰富化)
| 文件 | 改动 |
|------|------|
| `service_monitor/utils/display_names.py` | 补全 `VM_PGMajFAULT`;新增 `MYSQL_DEPTH_*` 30 个 KEY、`REDIS_DEPTH_*` 22 个 KEY(共 52 条) |
| `service_monitor/utils/thresholds.py` | 新增 MySQL 深度阈值 7 个、Redis 深度阈值 5 个 |
| `service_monitor/assets/service/23_mysql_depth.sh` | 重构为 output_result 格式,30 个检测项,加 MYSQL_DEPTH_ 前缀 |
| `service_monitor/assets/service/25_redis_depth.sh` | 重构为 output_result 格式,22 个检测项,加 REDIS_DEPTH_ 前缀 |
### 本次修改文件(阶段二:新检测模块移植)
| 文件 | 改动 |
|------|------|
| `service_monitor/assets/system/46_security_deep.sh` | 新建 - 安全合规深度检测脚本(9 类安全检测) |
| `service_monitor/assets/service/39_middleware_conn.sh` | 新建 - 中间件连接检测脚本(4 类连接检测),修订 - 端口检测逻辑重构(服务 ping 优先) |
| `service_monitor/assets/config.sh.template` | 追加安全白名单配置(SUID/端口)+ EMQX 端口配置 |
| `service_monitor/utils/check_modules.py` | 追加 2 个 CheckModule 条目(46_security_deep、39_middleware_conn) |
| `service_monitor/utils/display_names.py` | 追加 36 个 KEY 映射(`SECURITY_DEEP_*` 20 个 + `MW_CONN_*` 16 个) |
| `service_monitor/utils/thresholds.py` | 追加 6 个安全检测阈值 |
| `service_monitor/utils/parser.py` | 追加 11 个异常状态词映射(`_VALUE_STATUS_MAP`) |
### 新增文档
| 文件 | 说明 |
|------|------|
| `Docs/需求文档/服务监测/PRD_需求文档_检测项丰富化移植.md` | 检测项丰富化需求文档 |
### 参考目录
```
platform-auto-test/临时目录/服务监测/
├── lib/shell/ # Shell 检测脚本(10个)
├── modules/ # PowerShell 模块(20个)
├── check_mysql_deep.sh # MySQL深度检测参考
├── check_redis.sh # Redis深度检测参考
├── check_server_health.sh # 综合健康检查参考
└── SecurityCheck.psm1 # 安全合规检测参考(下一阶段移植目标)
```
### 常用命令
```bash
# 本地启动
python skill/code/web/server.py
# 单元测试
cd skill/code && python -m pytest -v
# 部署到 5.60(用 ! 前缀执行)
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py
# 健康检查
curl -s http://192.168.5.60:8088/api/health
# 测试报告免登访问
curl -s "http://192.168.5.60:8088/service-monitor/report/<id>?token=rpt_xxx"
# SSH 验证部署文件(Windows 用 paramiko,见坑 7)
# 部署路径:/opt/troubleshoot/web/service_monitor/
# 语法检查:bash -n <script.sh>
```
### 服务器部署路径速查
| 路径 | 说明 |
|------|------|
| `/opt/troubleshoot/web/` | 应用部署根目录 |
| `/opt/troubleshoot/web/service_monitor/` | 服务监测子包 |
| `/opt/troubleshoot/web/service_monitor/assets/system/` | 系统类检测脚本(如 46_security_deep.sh) |
| `/opt/troubleshoot/web/service_monitor/assets/service/` | 服务类检测脚本(如 39_middleware_conn.sh) |
| `/tmp/check_modules/lib/` | **运行时**临时目录(巡检执行时才生成,勿手动查) |
### 未提交文件
- 多个 PRD 文档(需求/计划执行/问题处理)
- `skill/code/web/users.json`(仅登录统计更新)
- `skill/code/web/service_monitor/assets/system/46_security_deep.sh`(新模块 + LEVEL KEY 对齐)
- `skill/code/web/service_monitor/assets/service/39_middleware_conn.sh`(新模块 + 端口检测逻辑重构)
- `skill/code/web/service_monitor/assets/config.sh.template`(追加白名单+EMQX配置)
- `skill/code/web/service_monitor/utils/check_modules.py`(追加2个模块)
- `skill/code/web/service_monitor/utils/display_names.py`(追加36个KEY + 11个LEVEL KEY改名)
- `skill/code/web/service_monitor/utils/thresholds.py`(追加6个阈值)
- `skill/code/web/service_monitor/utils/parser.py`(追加11个异常状态词映射)
- `skill/code/web/routes/service_manage.py`(见 git status)
- `HANDOFF_服务监测.md`(本轮待提交)
> 注:5.44 目标的 `credential_overrides`(MySQL/Redis 密码)已通过 API 写入 5.60 服务器的 `targets.json`(加密),不在代码仓库内。
\ No newline at end of file
......@@ -2,10 +2,16 @@
"""
service_manage.py — 服务管理模块路由
占位模块,后续实现服务授权、服务升级、服务信息管理功能。
页面路由:/service-manage/*
- /service-manage → 重定向到服务授权
- /service-manage/authorization → 服务授权
- /service-manage/upgrade → 服务升级
- /service-manage/info → 服务信息
权限:仅管理员可访问
"""
from flask import Blueprint, render_template, session
from flask import Blueprint, render_template, session, redirect, url_for
from decorators import page_login_required
from utils.logger import get_logger
......@@ -15,9 +21,71 @@ logger = get_logger(__name__)
bp = Blueprint('service-manage', __name__)
# ============================================================
# 内部辅助
# ============================================================
def _current_user():
return session.get('user', {})
def _is_admin():
return _current_user().get('role') == 'admin'
# ============================================================
# 页面路由
# ============================================================
@bp.route('/service-manage')
@page_login_required
def service_manage_page():
"""服务管理主页(占位)"""
user = session.get('user', {})
return render_template('service_manage.html', user=user)
def page_index():
"""服务管理主页 → 重定向到服务授权"""
if not _is_admin():
return redirect('/')
return redirect(url_for('service-manage.page_authorization'))
@bp.route('/service-manage/authorization')
@page_login_required
def page_authorization():
"""服务授权页面"""
if not _is_admin():
return redirect('/')
user = _current_user()
return render_template(
'service_manage/authorization.html',
user=user,
is_admin=True,
active_menu='authorization',
)
@bp.route('/service-manage/upgrade')
@page_login_required
def page_upgrade():
"""服务升级页面"""
if not _is_admin():
return redirect('/')
user = _current_user()
return render_template(
'service_manage/upgrade.html',
user=user,
is_admin=True,
active_menu='upgrade',
)
@bp.route('/service-manage/info')
@page_login_required
def page_info():
"""服务信息页面"""
if not _is_admin():
return redirect('/')
user = _current_user()
return render_template(
'service_manage/info.html',
user=user,
is_admin=True,
active_menu='info',
)
......@@ -51,6 +51,20 @@ PYTHON_LOG_PATH="${PYTHON_LOG_PATH:-/data/services/api/python*/log}"
NGINX_LOG_PATH="${NGINX_LOG_PATH:-/data/middleware/nginx/log}"
NACOS_LOG_PATH="${NACOS_LOG_PATH:-/data/middleware/nacos/logs}"
# ==================== 安全白名单 ====================
# SUID 文件白名单(逗号分隔)
SUID_WHITELIST="${SUID_WHITELIST:-/usr/bin/sudo,/usr/bin/passwd,/usr/bin/su,/usr/bin/ping,/usr/bin/mount,/usr/bin/umount,/usr/bin/newgrp,/usr/bin/chsh,/usr/bin/chfn,/usr/bin/gpasswd,/usr/lib/openssh/ssh-keysign,/usr/libexec/openssh/ssh-keysign}"
# 端口白名单 - 新平台(逗号分隔)
PORT_WHITELIST_NEW="${PORT_WHITELIST_NEW:-22,80,443,1883,3306,6379,8083,8084,8888,18083}"
# 端口白名单 - 旧平台(逗号分隔)
PORT_WHITELIST_OLD="${PORT_WHITELIST_OLD:-22,80,443,1883,3306,6379,8081,8082,8443,9001}"
# ==================== EMQX 端口配置 ====================
EMQX_MQTT_PORT="${EMQX_MQTT_PORT:-1883}"
EMQX_DASHBOARD_PORT="${EMQX_DASHBOARD_PORT:-18083}"
# ==================== 函数 ====================
# 获取配置值
......
#!/bin/bash
# MySQL深度检测脚本
# 使用方法: ./mysql_depth_check.sh
################################################################################
# 23_mysql_depth.sh — MySQL深度检测
# 功能: 检测MySQL缓冲池、慢查询、连接、主从、大表、QPS等深度指标
# 参考: MiddlewareCheck.psm1 / check_mysql_deep.sh 移植
################################################################################
MYSQL_PASSWORD="dNrprU&2S"
CONTAINER="umysql"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${LIB_DIR:-/tmp/check_modules}"
if [ -f "$LIB_DIR/lib/config.sh" ]; then source "$LIB_DIR/lib/config.sh"; else echo "ERROR: config.sh not found"; exit 1; fi
if [ -f "$LIB_DIR/lib/common.sh" ]; then source "$LIB_DIR/lib/common.sh"; else echo "ERROR: common.sh not found"; exit 1; fi
MYSQL_PWD="${MYSQL_PASSWORD:-}"
CONTAINER="${CONTAINERS[mysql]:-mysql}"
MYSQL_CMD="docker exec -e MYSQL_PWD=\"$MYSQL_PWD\" $CONTAINER mysql -uroot -N"
# 运行时间
UPTIME=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW STATUS LIKE 'Uptime';" 2>&1 | tail -1 | awk '{print $2}')
UPTIME=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Uptime';" 2>/dev/null | awk '{print $2}')
if [ -n "$UPTIME" ] && [ "$UPTIME" -gt 0 ]; then
UPTIME_DAYS=$((UPTIME / 86400))
echo "UPTIME_DAYS:$UPTIME_DAYS"
else
echo "UPTIME_DAYS:N/A"
fi
# 连接数
THREADS_CONNECTED=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW STATUS LIKE 'Threads_connected';" 2>&1 | tail -1 | awk '{print $2}')
MAX_CONNECTIONS=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW VARIABLES LIKE 'max_connections';" 2>&1 | tail -1 | awk '{print $2}')
if [ -n "$THREADS_CONNECTED" ] && [ -n "$MAX_CONNECTIONS" ] && [ "$MAX_CONNECTIONS" -gt 0 ]; then
CONN_PERCENT=$(awk "BEGIN {printf \"%.1f\", $THREADS_CONNECTED*100/$MAX_CONNECTIONS}")
echo "CONNECTIONS:$THREADS_CONNECTED/$MAX_CONNECTIONS/$CONN_PERCENT"
else
echo "CONNECTIONS:N/A"
fi
# 慢查询
SLOW_QUERIES=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'Slow_queries';" 2>&1 | tail -1 | awk '{print $2}')
if [ -n "$SLOW_QUERIES" ]; then
echo "SLOW_QUERIES:$SLOW_QUERIES"
else
echo "SLOW_QUERIES:0"
output_result "MYSQL_DEPTH_UPTIME_DAYS" "$UPTIME_DAYS"
fi
# QPS统计
QUESTIONS=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'Questions';" 2>&1 | tail -1 | awk '{print $2}')
UPTIME=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW STATUS LIKE 'Uptime';" 2>&1 | tail -1 | awk '{print $2}')
# QPS
QUESTIONS=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Questions';" 2>/dev/null | awk '{print $2}')
if [ -n "$QUESTIONS" ] && [ -n "$UPTIME" ] && [ "$UPTIME" -gt 0 ]; then
QPS=$(awk "BEGIN {printf \"%.2f\", $QUESTIONS/$UPTIME}")
echo "QPS:$QPS"
else
echo "QPS:N/A"
output_result "MYSQL_DEPTH_QPS" "$QPS"
fi
# TPS统计
COM_COMMIT=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'Com_commit';" 2>&1 | tail -1 | awk '{print $2}')
COM_ROLLBACK=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'Com_rollback';" 2>&1 | tail -1 | awk '{print $2}')
# TPS
COM_COMMIT=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Com_commit';" 2>/dev/null | awk '{print $2}')
COM_ROLLBACK=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Com_rollback';" 2>/dev/null | awk '{print $2}')
if [ -n "$COM_COMMIT" ] && [ -n "$COM_ROLLBACK" ] && [ -n "$UPTIME" ] && [ "$UPTIME" -gt 0 ]; then
TOTAL_TRANS=$((COM_COMMIT + COM_ROLLBACK))
TPS=$(awk "BEGIN {printf \"%.2f\", $TOTAL_TRANS/$UPTIME}")
echo "TPS:$TPS"
else
echo "TPS:N/A"
output_result "MYSQL_DEPTH_TPS" "$TPS"
fi
# 死锁检测
DEADLOCK_OUTPUT=$(docker exec $CONTAINER mysql -uroot -p"$MYSQL_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';" 2>&1 | grep -v Warning)
if [ -n "$DEADLOCK_OUTPUT" ] && [ "$DEADLOCK_OUTPUT" != "" ]; then
DEADLOCKS=$(echo "$DEADLOCK_OUTPUT" | awk 'NF>=2 {print $2}')
if [ -z "$DEADLOCKS" ]; then
DEADLOCKS=0
fi
else
DEADLOCKS=0
# 连接数
THREADS_CONNECTED=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null | awk '{print $2}')
MAX_CONNECTIONS=$(eval $MYSQL_CMD -e "SHOW VARIABLES LIKE 'max_connections';" 2>/dev/null | awk '{print $2}')
if [ -n "$THREADS_CONNECTED" ] && [ -n "$MAX_CONNECTIONS" ] && [ "$MAX_CONNECTIONS" -gt 0 ]; then
CONN_PERCENT=$(awk "BEGIN {printf \"%.1f\", $THREADS_CONNECTED*100/$MAX_CONNECTIONS}")
output_result "MYSQL_DEPTH_CONNECTION_USAGE" "$CONN_PERCENT"
output_result "MYSQL_DEPTH_CONNECTIONS" "$THREADS_CONNECTED/$MAX_CONNECTIONS"
fi
echo "DEADLOCKS:$DEADLOCKS"
# Buffer Pool命中率
BUFFER_POOL_RESULT=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "SELECT (1 - (SELECT variable_value FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_reads') / (SELECT variable_value FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_read_requests')) * 100 AS hit_ratio;" 2>&1 | grep -v Warning | tail -1)
if [ -n "$BUFFER_POOL_RESULT" ] && [[ "$BUFFER_POOL_RESULT" =~ ^[0-9]+\.?[0-9]*$ ]]; then
HIT_RATE=$(awk "BEGIN {printf \"%.2f\", $BUFFER_POOL_RESULT}")
echo "BUFFER_POOL_HIT_RATE:$HIT_RATE"
else
echo "BUFFER_POOL_HIT_RATE:N/A"
# 慢查询
SLOW_QUERIES=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Slow_queries';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_SLOW_QUERY_COUNT" "${SLOW_QUERIES:-0}"
# 慢查询是否开启
SLOW_QUERY_LOG=$(eval $MYSQL_CMD -e "SHOW VARIABLES LIKE 'slow_query_log';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_SLOW_QUERY_ENABLED" "${SLOW_QUERY_LOG:-OFF}"
# 死锁
DEADLOCK_OUTPUT=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_DEADLOCKS" "${DEADLOCK_OUTPUT:-0}"
# Buffer Pool 命中率
BP_READS=$(eval $MYSQL_CMD -e "SELECT variable_value FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_reads';" 2>/dev/null)
BP_REQUESTS=$(eval $MYSQL_CMD -e "SELECT variable_value FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_read_requests';" 2>/dev/null)
if [ -n "$BP_REQUESTS" ] && [ "$BP_REQUESTS" -gt 0 ] 2>/dev/null; then
HIT_RATE=$(awk "BEGIN {printf \"%.2f\", (1 - ${BP_READS:-0}/$BP_REQUESTS)*100}")
output_result "MYSQL_DEPTH_BUFFER_POOL_HIT_RATE" "$HIT_RATE"
fi
# 表缓存命中率
OPEN_TABLES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW GLOBAL STATUS LIKE 'Open_tables';" 2>/dev/null | awk '{print $2}')
OPENED_TABLES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW GLOBAL STATUS LIKE 'Opened_tables';" 2>/dev/null | awk '{print $2}')
# 验证数值
if [[ "$OPEN_TABLES" =~ ^[0-9]+$ ]] && [[ "$OPENED_TABLES" =~ ^[0-9]+$ ]]; then
OPEN_TABLES=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Open_tables';" 2>/dev/null | awk '{print $2}')
OPENED_TABLES=$(eval $MYSQL_CMD -e "SHOW GLOBAL STATUS LIKE 'Opened_tables';" 2>/dev/null | awk '{print $2}')
if [ -n "$OPEN_TABLES" ] && [ -n "$OPENED_TABLES" ] && [ "$OPENED_TABLES" -ge 0 ] 2>/dev/null; then
if [ "$OPENED_TABLES" -eq 0 ]; then
# 没有重新打开过表,缓存命中100%
echo "CACHE_HIT_RATE:100.00"
output_result "MYSQL_DEPTH_CACHE_HIT_RATE" "100.00"
elif [ "$OPEN_TABLES" -gt 0 ]; then
# 表缓存命中率 = Open_tables / (Open_tables + Opened_tables) × 100
# Opened_tables表示需要重新打开表的次数(缓存未命中)
TOTAL_OPENS=$((OPEN_TABLES + OPENED_TABLES))
CACHE_HIT_RATE=$(awk "BEGIN {printf \"%.2f\", ($OPEN_TABLES * 100) / $TOTAL_OPENS}")
echo "CACHE_HIT_RATE:$CACHE_HIT_RATE"
else
echo "CACHE_HIT_RATE:N/A"
CACHE_HIT_RATE=$(awk "BEGIN {printf \"%.2f\", ($OPEN_TABLES*100)/$TOTAL_OPENS}")
output_result "MYSQL_DEPTH_CACHE_HIT_RATE" "$CACHE_HIT_RATE"
fi
else
echo "CACHE_HIT_RATE:N/A"
fi
# 当前活跃查询数
ACTIVE_QUERIES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW PROCESSLIST;" 2>/dev/null | grep -v "Sleep" | grep -v "binlog" | wc -l)
echo "ACTIVE_QUERIES:$ACTIVE_QUERIES"
# 活跃查询数
ACTIVE_QUERIES=$(eval $MYSQL_CMD -e "SHOW PROCESSLIST;" 2>/dev/null | grep -v "Sleep" | grep -v "binlog" | wc -l)
output_result "MYSQL_DEPTH_ACTIVE_QUERIES" "$ACTIVE_QUERIES"
# Binlog状态
BINLOG_STATUS=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW VARIABLES LIKE 'log_bin';" 2>/dev/null | awk '{print $2}')
BINLOG_STATUS=$(eval $MYSQL_CMD -e "SHOW VARIABLES LIKE 'log_bin';" 2>/dev/null | awk '{print $2}')
if [ "$BINLOG_STATUS" = "ON" ]; then
BINLOG_COUNT=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW BINARY LOGS;" 2>/dev/null | wc -l)
echo "BINLOG_STATUS:ON/$BINLOG_COUNT"
BINLOG_COUNT=$(eval $MYSQL_CMD -e "SHOW BINARY LOGS;" 2>/dev/null | wc -l)
output_result "MYSQL_DEPTH_BINLOG_STATUS" "ON/${BINLOG_COUNT}"
BINLOG_EXPIRE=$(eval $MYSQL_CMD -e "SHOW VARIABLES LIKE 'expire_logs_days';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_BINLOG_EXPIRE_DAYS" "${BINLOG_EXPIRE:-0}"
else
echo "BINLOG_STATUS:OFF"
output_result "MYSQL_DEPTH_BINLOG_STATUS" "OFF"
fi
# 表碎片检测 (DATA_FREE > 10MB的表数量)
FRAGMENTED_TABLES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys') AND DATA_FREE > 10485760;" 2>/dev/null)
if [ -n "$FRAGMENTED_TABLES" ] && [[ "$FRAGMENTED_TABLES" =~ ^[0-9]+$ ]]; then
echo "FRAGMENTED_TABLES:$FRAGMENTED_TABLES"
else
echo "FRAGMENTED_TABLES:0"
fi
# 表碎片
FRAGMENTED_TABLES=$(eval $MYSQL_CMD -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys') AND DATA_FREE > 10485760;" 2>/dev/null)
output_result "MYSQL_DEPTH_FRAGMENTED_TABLES" "${FRAGMENTED_TABLES:-0}"
# 连接错误统计
CONN_ERRORS=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Aborted_connects';" 2>/dev/null | awk '{print $2}')
if [ -n "$CONN_ERRORS" ]; then
echo "CONN_ERRORS:$CONN_ERRORS"
else
echo "CONN_ERRORS:0"
fi
# 连接错误
CONN_ERRORS=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Aborted_connects';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_CONN_ERRORS" "${CONN_ERRORS:-0}"
# InnoDB缓冲池大小
INNODB_BP_SIZE=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';" 2>/dev/null | awk '{print $2}')
if [ -n "$INNODB_BP_SIZE" ] && [ "$INNODB_BP_SIZE" -gt 0 ]; then
# 转换为MB
INNODB_BP_SIZE=$(eval $MYSQL_CMD -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';" 2>/dev/null | awk '{print $2}')
if [ -n "$INNODB_BP_SIZE" ] && [ "$INNODB_BP_SIZE" -gt 0 ] 2>/dev/null; then
BP_SIZE_MB=$((INNODB_BP_SIZE / 1024 / 1024))
echo "INNODB_BP_SIZE:${BP_SIZE_MB}MB"
else
echo "INNODB_BP_SIZE:N/A"
output_result "MYSQL_DEPTH_INNODB_BP_SIZE" "${BP_SIZE_MB}MB"
fi
# 事务状态统计
TRX_ACTIVE=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Innodb_trx_active';" 2>/dev/null | awk '{print $2}')
if [ -n "$TRX_ACTIVE" ]; then
echo "TRX_ACTIVE:$TRX_ACTIVE"
else
echo "TRX_ACTIVE:0"
fi
# 活跃事务
TRX_ACTIVE=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Innodb_trx_active';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_TRX_ACTIVE" "${TRX_ACTIVE:-0}"
# 锁等待检测
LOCK_WAITS=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Innodb_row_lock_current_waits';" 2>/dev/null | awk '{print $2}')
if [ -n "$LOCK_WAITS" ]; then
echo "LOCK_WAITS:$LOCK_WAITS"
else
echo "LOCK_WAITS:0"
fi
# 锁等待
LOCK_WAITS=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Innodb_row_lock_current_waits';" 2>/dev/null | awk '{print $2}')
output_result "MYSQL_DEPTH_LOCK_WAITS" "${LOCK_WAITS:-0}"
# 连接池分析(Threads_running/Threads_connected)
THREADS_RUNNING=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Threads_running';" 2>/dev/null | awk '{print $2}')
THREADS_CONNECTED_NEW=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null | awk '{print $2}')
if [ -n "$THREADS_RUNNING" ] && [ -n "$THREADS_CONNECTED_NEW" ] && [ "$THREADS_CONNECTED_NEW" -gt 0 ]; then
IDLE_CONN=$((THREADS_CONNECTED_NEW - THREADS_RUNNING))
echo "THREADS_POOL:$THREADS_RUNNING/$THREADS_CONNECTED_NEW/$IDLE_CONN"
else
echo "THREADS_POOL:N/A"
fi
# InnoDB状态摘要
INNODB_STATUS=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW ENGINE INNODB STATUS\G" 2>/dev/null | grep -E "LATEST DETECTED DEADLOCK|TRANSACTIONS" | wc -l)
echo "INNODB_STATUS:$INNODB_STATUS"
# 临时表使用率
TEMP_TABLES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Created_tmp_disk_tables';" 2>/dev/null | awk '{print $2}')
TEMP_TABLES_TOTAL=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW STATUS LIKE 'Created_tmp_tables';" 2>/dev/null | awk '{print $2}')
if [ -n "$TEMP_TABLES" ] && [ -n "$TEMP_TABLES_TOTAL" ] && [ "$TEMP_TABLES_TOTAL" -gt 0 ]; then
TEMP_RATE=$(awk "BEGIN {printf \"%.2f\", ($TEMP_TABLES * 100) / $TEMP_TABLES_TOTAL}")
echo "TEMP_TABLE_RATE:$TEMP_RATE"
else
echo "TEMP_TABLE_RATE:N/A"
fi
# 数据库总大小(MB)
DB_SIZE=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys');" 2>/dev/null)
# 数据库总大小
DB_SIZE=$(eval $MYSQL_CMD -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys');" 2>/dev/null)
if [ -n "$DB_SIZE" ]; then
echo "DATABASE_SIZE:${DB_SIZE}MB"
else
echo "DATABASE_SIZE:N/A"
output_result "MYSQL_DEPTH_DATABASE_SIZE" "${DB_SIZE}MB"
fi
# 表数量统计
TABLE_COUNT=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys') AND TABLE_TYPE='BASE TABLE';" 2>/dev/null)
if [ -n "$TABLE_COUNT" ]; then
echo "TABLE_COUNT:$TABLE_COUNT"
else
echo "TABLE_COUNT:0"
fi
# 表总数
TABLE_COUNT=$(eval $MYSQL_CMD -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys') AND TABLE_TYPE='BASE TABLE';" 2>/dev/null)
output_result "MYSQL_DEPTH_TABLE_COUNT" "${TABLE_COUNT:-0}"
# 复制状态
SLAVE_STATUS=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW SLAVE STATUS\G" 2>/dev/null | grep "Slave_IO_Running:" | awk '{print $2}')
if [ "$SLAVE_STATUS" = "Yes" ]; then
echo "REPLICATION_STATUS:SLAVE"
SLAVE_IO=$(eval $MYSQL_CMD -e "SHOW SLAVE STATUS\G" 2>/dev/null | grep "Slave_IO_Running:" | awk '{print $2}')
if [ "$SLAVE_IO" = "Yes" ]; then
output_result "MYSQL_DEPTH_REPLICATION_STATUS" "SLAVE"
SLAVE_SQL=$(eval $MYSQL_CMD -e "SHOW SLAVE STATUS\G" 2>/dev/null | grep "Slave_SQL_Running:" | awk '{print $2}')
BEHIND_MASTER=$(eval $MYSQL_CMD -e "SHOW SLAVE STATUS\G" 2>/dev/null | grep "Seconds_Behind_Master:" | awk '{print $2}')
output_result "MYSQL_DEPTH_SLAVE_IO_RUNNING" "${SLAVE_IO:-No}"
output_result "MYSQL_DEPTH_SLAVE_SQL_RUNNING" "${SLAVE_SQL:-No}"
output_result "MYSQL_DEPTH_SLAVE_DELAY" "${BEHIND_MASTER:-0}"
else
echo "REPLICATION_STATUS:MASTER"
output_result "MYSQL_DEPTH_REPLICATION_STATUS" "MASTER"
fi
# Binlog过期时间(天)
BINLOG_EXPIRE=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "SHOW VARIABLES LIKE 'expire_logs_days';" 2>/dev/null | awk '{print $2}')
if [ -n "$BINLOG_EXPIRE" ]; then
echo "BINLOG_EXPIRE:${BINLOG_EXPIRE}days"
else
echo "BINLOG_EXPIRE:N/A"
fi
# ========== 高优先级功能补充 ==========
# 当前活跃查询详情 (SHOW PROCESSLIST)
PROCESSLIST_OUTPUT=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "SHOW PROCESSLIST;" 2>/dev/null | grep -v "Command" | head -10)
# 进程列表摘要
PROCESSLIST_OUTPUT=$(eval $MYSQL_CMD -e "SHOW PROCESSLIST;" 2>/dev/null)
if [ -n "$PROCESSLIST_OUTPUT" ]; then
# 统计各状态查询数量
SLEEP_COUNT=$(echo "$PROCESSLIST_OUTPUT" | grep -c "Sleep" || echo "0")
QUERY_COUNT=$(echo "$PROCESSLIST_OUTPUT" | grep -v "Sleep" | wc -l)
LONG_QUERY_COUNT=$(echo "$PROCESSLIST_OUTPUT" | awk -F'\t' '{if ($6>5) print}' | wc -l)
echo "ACTIVE_PROCESSLIST:Sleep:${SLEEP_COUNT},Active:${QUERY_COUNT},LongRunning:${LONG_QUERY_COUNT}"
# 输出TOP5耗时查询
LONG_QUERIES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO, 50) AS QUERY FROM information_schema.PROCESSLIST WHERE COMMAND != 'Sleep' AND TIME > 0 ORDER BY TIME DESC LIMIT 5;" 2>/dev/null | grep -v "QUERY")
if [ -n "$LONG_QUERIES" ]; then
echo "LONG_QUERIES_TOP5:$(echo "$LONG_QUERIES" | head -5 | tr '\n' '|' | sed 's/|$//')"
else
echo "LONG_QUERIES_TOP5:N/A"
fi
else
echo "ACTIVE_PROCESSLIST:N/A"
echo "LONG_QUERIES_TOP5:N/A"
fi
# 缺少索引的高耗时查询 (通过performance_schema)
SLOW_QUERY_STATS=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "
SELECT
COUNT_STAR as exec_count,
ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_sec,
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_time_sec,
ROUND(SUM_LOCK_TIME/1000000000, 2) as lock_time_sec,
digest_text as query_sample
FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text IS NOT NULL
AND digest_text NOT LIKE '%performance_schema%'
AND COUNT_STAR > 10
AND SUM_TIMER_WAIT > 1000000000
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
" 2>/dev/null | grep -v "query_sample")
if [ -n "$SLOW_QUERY_STATS" ]; then
# 输出TOP1耗时查询的统计
TOP_QUERY=$(echo "$SLOW_QUERY_STATS" | head -2 | tail -1)
if [ -n "$TOP_QUERY" ]; then
EXEC_COUNT=$(echo "$TOP_QUERY" | awk '{print $1}')
TOTAL_TIME=$(echo "$TOP_QUERY" | awk '{print $2}')
AVG_TIME=$(echo "$TOP_QUERY" | awk '{print $3}')
LOCK_TIME=$(echo "$TOP_QUERY" | awk '{print $4}')
echo "SLOW_QUERY_TOP1:Exec:${EXEC_COUNT},TotalTime:${TOTAL_TIME}s,AvgTime:${AVG_TIME}s,LockTime:${LOCK_TIME}s"
else
echo "SLOW_QUERY_TOP1:N/A"
fi
else
echo "SLOW_QUERY_TOP1:N/A"
fi
# 缺少索引的表检测
MISSING_INDEX_TABLES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "
SELECT
t.table_schema,
t.table_name,
t.table_rows,
ROUND(t.data_length / 1024 / 1024, 2) as data_mb
FROM information_schema.TABLES t
WHERE t.table_schema NOT IN ('information_schema','performance_schema','mysql','sys')
AND NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS s
WHERE s.table_schema = t.table_schema
AND s.table_name = t.table_name
)
AND t.table_rows > 1000
ORDER BY t.data_length DESC
LIMIT 5;
" 2>/dev/null | grep -v "data_mb")
if [ -n "$MISSING_INDEX_TABLES" ]; then
MISSING_COUNT=$(echo "$MISSING_INDEX_TABLES" | wc -l)
echo "TABLES_WITHOUT_INDEX:$MISSING_COUNT"
else
echo "TABLES_WITHOUT_INDEX:0"
fi
# ========== 中优先级功能补充 ==========
# 数据库列表详细输出
DATABASE_LIST=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "
SELECT
table_schema,
ROUND(sum(data_length + index_length) / 1024 / 1024, 2) as size_mb
FROM information_schema.tables
WHERE table_schema NOT IN ('information_schema','performance_schema','mysql','sys')
GROUP BY table_schema
ORDER BY size_mb DESC
LIMIT 10;
" 2>&1 | grep -v "\[Warning\]")
if [ -n "$DATABASE_LIST" ]; then
DB_COUNT=$(echo "$DATABASE_LIST" | wc -l)
TOTAL_SIZE=$(echo "$DATABASE_LIST" | awk -F'\t' '{sum+=$2} END {printf "%.2f", sum}')
DB_NAMES=$(echo "$DATABASE_LIST" | awk -F'\t' '{print $1}' | tr '\n' ',' | sed 's/,$//')
echo "DATABASE_LIST:Count:${DB_COUNT},TotalSize:${TOTAL_SIZE}MB,Databases:${DB_NAMES}"
else
echo "DATABASE_LIST:N/A"
fi
# InnoDB状态详情 (事务历史)
INNODB_TRX_INFO=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "
SELECT
COUNT(*) as trx_count,
IFNULL(SUM(TIMESTAMPDIFF(SECOND, trx_started, NOW())), 0) as total_trx_time
FROM information_schema.INNODB_TRX;
" 2>/dev/null)
if [ -n "$INNODB_TRX_INFO" ]; then
TRX_COUNT=$(echo "$INNODB_TRX_INFO" | awk -F'\t' '{print $1}')
TOTAL_TRX_TIME=$(echo "$INNODB_TRX_INFO" | awk -F'\t' '{print $2}')
echo "INNODB_TRX_DETAIL:Count:${TRX_COUNT},TotalTime:${TOTAL_TRX_TIME}s"
else
echo "INNODB_TRX_DETAIL:Count:0,TotalTime:0s"
fi
# 表统计 (ubains库TOP20)
UBAINS_TOP_TABLES=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "
SELECT
table_name,
table_rows,
round(data_length / 1024 / 1024, 2) as data_mb,
round(index_length / 1024 / 1024, 2) as index_mb,
round((data_length + index_length) / 1024 / 1024, 2) as total_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'ubains'
ORDER BY (data_length + index_length) DESC
LIMIT 20;
" 2>/dev/null | grep -v "total_mb")
if [ -n "$UBAINS_TOP_TABLES" ]; then
TABLE_COUNT=$(echo "$UBAINS_TOP_TABLES" | wc -l)
# 获取TOP1表的信息
TOP1_TABLE=$(echo "$UBAINS_TOP_TABLES" | head -2 | tail -1)
if [ -n "$TOP1_TABLE" ]; then
TOP1_NAME=$(echo "$TOP1_TABLE" | awk '{print $1}')
TOP1_SIZE=$(echo "$TOP1_TABLE" | awk '{print $5}')
echo "UBAINS_TABLES_TOP20:Count:${TABLE_COUNT},Top1:${TOP1_NAME}:${TOP1_SIZE}MB"
else
echo "UBAINS_TABLES_TOP20:Count:${TABLE_COUNT}"
fi
else
echo "UBAINS_TABLES_TOP20:N/A"
QUERY_ACTIVE=$(echo "$PROCESSLIST_OUTPUT" | grep -v "Sleep" | wc -l)
output_result "MYSQL_DEPTH_ACTIVE_PROCESSLIST" "Sleep:${SLEEP_COUNT},Active:${QUERY_ACTIVE}"
fi
# 复制状态详情
SLAVE_STATUS_DETAIL=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "SHOW SLAVE STATUS\G" 2>/dev/null)
if [ -n "$SLAVE_STATUS_DETAIL" ]; then
# 提取关键复制状态信息
SLAVE_IO=$(echo "$SLAVE_STATUS_DETAIL" | grep "Slave_IO_Running:" | awk '{print $2}')
SLAVE_SQL=$(echo "$SLAVE_STATUS_DETAIL" | grep "Slave_SQL_Running:" | awk '{print $2}')
BEHIND_MASTER=$(echo "$SLAVE_STATUS_DETAIL" | grep "Seconds_Behind_Master:" | awk '{print $2}')
if [ "$SLAVE_IO" = "Yes" ]; then
echo "REPLICATION_DETAIL:IO:$SLAVE_IO,SQL:$SLAVE_SQL,Delay:${BEHIND_MASTER}s"
else
echo "REPLICATION_DETAIL:MASTER"
fi
else
echo "REPLICATION_DETAIL:MASTER"
# 大表检测
TOP_TABLES=$(eval $MYSQL_CMD -e "SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys') ORDER BY (data_length + index_length) DESC LIMIT 5;" 2>/dev/null)
if [ -n "$TOP_TABLES" ]; then
output_result "MYSQL_DEPTH_TOP_TABLES" "$(echo "$TOP_TABLES" | tr '\n' '|' | sed 's/|$//')"
fi
# 连接错误详细统计 - 该功能依赖的表在当前MySQL版本中不存在,已移除
# 使用基础的Aborted_connects统计即可(CONN_ERRORS变量)
# ========== 补充:InnoDB缓冲池详情 ==========
INNODB_BP_DETAIL=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -e "SHOW ENGINE INNODB STATUS\G" 2>/dev/null | grep -A 20 "Buffer pool hit rate")
if [ -n "$INNODB_BP_DETAIL" ]; then
BP_HIT_RATE=$(echo "$INNODB_BP_DETAIL" | grep -oP 'hit rate \K[\d/]+' | head -1)
if [ -n "$BP_HIT_RATE" ]; then
echo "INNODB_BP_DETAIL:HitRate:${BP_HIT_RATE}"
else
echo "INNODB_BP_DETAIL:N/A"
fi
else
echo "INNODB_BP_DETAIL:N/A"
fi
# ========== 补充:锁信息详情 ==========
LOCK_INFO=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "
SELECT
COUNT(*) as lock_waits,
IFNULL(SUM(TIMESTAMPDIFF(SECOND, r.trx_started, NOW())), 0) as total_wait_time
FROM information_schema.INNODB_LOCK_WAITS w
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
" 2>/dev/null)
if [ -n "$LOCK_INFO" ]; then
LOCK_COUNT=$(echo "$LOCK_INFO" | awk -F'\t' '{print $1}')
WAIT_TIME=$(echo "$LOCK_INFO" | awk -F'\t' '{print $2}')
echo "LOCK_DETAIL:Waits:${LOCK_COUNT},TotalWait:${WAIT_TIME}s"
else
echo "LOCK_DETAIL:Waits:0,TotalWait:0s"
# 临时表使用率
TEMP_DISK=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Created_tmp_disk_tables';" 2>/dev/null | awk '{print $2}')
TEMP_TOTAL=$(eval $MYSQL_CMD -e "SHOW STATUS LIKE 'Created_tmp_tables';" 2>/dev/null | awk '{print $2}')
if [ -n "$TEMP_DISK" ] && [ -n "$TEMP_TOTAL" ] && [ "$TEMP_TOTAL" -gt 0 ] 2>/dev/null; then
TEMP_RATE=$(awk "BEGIN {printf \"%.2f\", ($TEMP_DISK*100)/$TEMP_TOTAL}")
output_result "MYSQL_DEPTH_TEMP_TABLE_RATE" "$TEMP_RATE"
fi
# ========== 补充:表碎片检测详情 ==========
FRAGMENTED_TABLES_DETAIL=$(docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" $CONTAINER mysql -uroot -N -e "
SELECT
CONCAT(table_schema, '.', table_name) as table_name,
ROUND(data_length / 1024 / 1024, 2) as data_mb,
ROUND(data_free / 1024 / 1024, 2) as fragment_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys')
AND DATA_FREE > 10485760
ORDER BY data_free DESC
LIMIT 5;
" 2>/dev/null | grep -v "fragment_mb")
if [ -n "$FRAGMENTED_TABLES_DETAIL" ]; then
FRAG_COUNT=$(echo "$FRAGMENTED_TABLES_DETAIL" | wc -l)
TOP_FRAG=$(echo "$FRAGMENTED_TABLES_DETAIL" | head -1)
if [ -n "$TOP_FRAG" ]; then
TABLE_NAME=$(echo "$TOP_FRAG" | awk '{print $1}')
FRAG_MB=$(echo "$TOP_FRAG" | awk '{print $3}')
echo "FRAGMENTED_DETAIL:Count:${FRAG_COUNT},Top1:${TABLE_NAME}:${FRAG_MB}MB"
else
echo "FRAGMENTED_DETAIL:Count:${FRAG_COUNT}"
fi
else
echo "FRAGMENTED_DETAIL:Count:0"
fi
# 缺少索引的表
MISSING_INDEX=$(eval $MYSQL_CMD -e "SELECT COUNT(*) FROM information_schema.TABLES t WHERE t.table_schema NOT IN ('information_schema','performance_schema','mysql','sys') AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS s WHERE s.table_schema = t.table_schema AND s.table_name = t.table_name) AND t.table_rows > 1000;" 2>/dev/null)
output_result "MYSQL_DEPTH_TABLES_WITHOUT_INDEX" "${MISSING_INDEX:-0}"
\ No newline at end of file
#!/bin/bash
# Redis深度检测脚本
# 使用方法: ./redis_depth_check.sh
################################################################################
# 25_redis_depth.sh — Redis深度检测
# 功能: 检测Redis持久化、内存碎片、缓存命中、主从复制、键空间等深度指标
# 参考: MiddlewareCheck.psm1 / check_redis.sh 移植
################################################################################
REDIS_PASSWORD="dNrprU&2S"
CONTAINER="uredis"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${LIB_DIR:-/tmp/check_modules}"
# 基础信息
REDIS_VERSION=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO server 2>&1 | grep redis_version | cut -d: -f2 | tr -d '\r')
if [ -n "$REDIS_VERSION" ]; then
echo "REDIS_VERSION:$REDIS_VERSION"
else
echo "REDIS_VERSION:N/A"
fi
if [ -f "$LIB_DIR/lib/config.sh" ]; then source "$LIB_DIR/lib/config.sh"; else echo "ERROR: config.sh not found"; exit 1; fi
if [ -f "$LIB_DIR/lib/common.sh" ]; then source "$LIB_DIR/lib/common.sh"; else echo "ERROR: common.sh not found"; exit 1; fi
# 运行时间(天)
UPTIME_DAYS=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO server 2>&1 | grep uptime_in_days | cut -d: -f2 | tr -d '\r')
if [ -n "$UPTIME_DAYS" ]; then
echo "UPTIME_DAYS:$UPTIME_DAYS"
else
echo "UPTIME_DAYS:N/A"
fi
REDIS_PWD="${REDIS_PASSWORD:-}"
CONTAINER="${CONTAINERS[redis]:-redis}"
REDIS_CLI="docker exec $CONTAINER redis-cli -a \"$REDIS_PWD\" --no-auth-warning"
# Redis版本
REDIS_VERSION=$(eval $REDIS_CLI INFO server 2>/dev/null | grep redis_version | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_VERSION" "${REDIS_VERSION:-N/A}"
# 运行天数
UPTIME_DAYS=$(eval $REDIS_CLI INFO server 2>/dev/null | grep uptime_in_days | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_UPTIME_DAYS" "${UPTIME_DAYS:-0}"
# 键数量
KEY_COUNT=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" DBSIZE 2>&1 | grep -v "Warning")
if [ -n "$KEY_COUNT" ] && [[ "$KEY_COUNT" =~ ^[0-9]+$ ]]; then
echo "KEY_COUNT:$KEY_COUNT"
else
echo "KEY_COUNT:0"
fi
KEY_COUNT=$(eval $REDIS_CLI DBSIZE 2>/dev/null | grep -E '^[0-9]+$')
output_result "REDIS_DEPTH_KEY_COUNT" "${KEY_COUNT:-0}"
# 内存使用
MEMORY_INFO=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO memory 2>&1 | grep -E "used_memory_human:|mem_fragmentation_ratio:" | tr -d '\r' | tr '\n' '|' | sed 's/|$//')
if [ -n "$MEMORY_INFO" ]; then
echo "MEMORY_INFO:$MEMORY_INFO"
else
echo "MEMORY_INFO:N/A"
fi
# 客户端连接数
CLIENT_COUNT=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO clients 2>&1 | grep connected_clients | cut -d: -f2 | tr -d '\r')
if [ -n "$CLIENT_COUNT" ]; then
echo "CLIENT_COUNT:$CLIENT_COUNT"
else
echo "CLIENT_COUNT:0"
fi
USED_MEMORY=$(eval $REDIS_CLI INFO memory 2>/dev/null | grep used_memory_human | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_USED_MEMORY" "${USED_MEMORY:-N/A}"
# ========== 高优先级功能补充 ==========
# 内存碎片率
MEM_FRAG=$(eval $REDIS_CLI INFO memory 2>/dev/null | grep mem_fragmentation_ratio | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_MEM_FRAGMENTATION" "${MEM_FRAG:-N/A}"
# Keyspace信息(各数据库键和过期键统计)
KEYSPACE_INFO=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO keyspace 2>&1 | grep -v "Warning" | grep "^db" | tr -d '\r')
if [ -n "$KEYSPACE_INFO" ]; then
TOTAL_KEYS=0
TOTAL_EXPIRES=0
while IFS=: read -r db_key info; do
if [[ "$info" =~ keys=([0-9]+) ]]; then
keys=${BASH_REMATCH[1]}
TOTAL_KEYS=$((TOTAL_KEYS + keys))
fi
if [[ "$info" =~ expires=([0-9]+) ]]; then
expires=${BASH_REMATCH[1]}
TOTAL_EXPIRES=$((TOTAL_EXPIRES + expires))
fi
done <<< "$KEYSPACE_INFO"
echo "KEYSPACE_DETAIL:Total:$TOTAL_KEYS,Expires:$TOTAL_EXPIRES"
else
echo "KEYSPACE_DETAIL:N/A"
fi
# 客户端数
CLIENT_COUNT=$(eval $REDIS_CLI INFO clients 2>/dev/null | grep connected_clients | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_CLIENT_COUNT" "${CLIENT_COUNT:-0}"
# 键类型分布采样(采样前100个键)
KEY_TYPE_SAMPLE=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" --scan --count 100 2>&1 | head -100 | xargs -I {} docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" TYPE {} 2>&1 | grep -v "Warning" | sort | uniq -c | awk '{print $2":"$1}' | tr '\n' '|' | sed 's/|$//' | sed 's/ /:/g')
if [ -n "$KEY_TYPE_SAMPLE" ]; then
echo "KEY_TYPE_DISTRIBUTION:$KEY_TYPE_SAMPLE"
else
echo "KEY_TYPE_DISTRIBUTION:N/A"
fi
# 每秒操作数
OPS_PER_SEC=$(eval $REDIS_CLI INFO stats 2>/dev/null | grep instantaneous_ops_per_sec | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_OPS_PER_SEC" "${OPS_PER_SEC:-0}"
# ========== 中优先级功能补充 ==========
# 持久化状态
PERSISTENCE_INFO=$(eval $REDIS_CLI INFO persistence 2>/dev/null)
RDB_COW=$(echo "$PERSISTENCE_INFO" | grep -oP 'rdb_last_cow_size:\K\d+' | head -1)
AOF_ENABLED=$(echo "$PERSISTENCE_INFO" | grep -oP 'aof_enabled:\K\d+' | head -1)
# 持久化信息
PERSISTENCE_INFO=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO persistence 2>&1 | grep -v "Warning" | grep -E "rdb_last_cow_size:|aof_enabled:|rdb_last_save_time:" | tr -d '\r' | tr '\n' '|' | sed 's/|$//')
if [ -n "$PERSISTENCE_INFO" ]; then
# 解析持久化状态
RDB_COW=$(echo "$PERSISTENCE_INFO" | grep -oP 'rdb_last_cow_size:\K\d+' | head -1)
AOF_ENABLED=$(echo "$PERSISTENCE_INFO" | grep -oP 'aof_enabled:\K\d+' | head -1)
RDB_STATUS="空闲"
if [ "$RDB_COW" -gt 0 ] 2>/dev/null; then
RDB_STATUS="备份中"
fi
AOF_STATUS="未启用"
if [ "$AOF_ENABLED" = "1" ]; then
AOF_STATUS="已启用"
fi
RDB_STATUS="空闲"
if [ -n "$RDB_COW" ] && [ "$RDB_COW" -gt 0 ] 2>/dev/null; then
RDB_STATUS="备份中"
fi
echo "PERSISTENCE_STATUS:RDB:$RDB_STATUS,AOF:$AOF_STATUS"
else
echo "PERSISTENCE_STATUS:N/A"
AOF_STATUS="未启用"
if [ "$AOF_ENABLED" = "1" ]; then
AOF_STATUS="已启用"
fi
output_result "REDIS_DEPTH_RDB_PERSISTENCE" "$RDB_STATUS"
output_result "REDIS_DEPTH_AOF_PERSISTENCE" "$AOF_STATUS"
# 复制信息
REPL_INFO=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO replication 2>&1 | grep -v "Warning" | grep -E "role:|connected_slaves:|master_link_status:" | tr -d '\r' | tr '\n' '|' | sed 's/|$//')
if [ -n "$REPL_INFO" ]; then
ROLE=$(echo "$REPL_INFO" | grep -oP 'role:\K\w+' | head -1)
CONNECTED_SLAVES=$(echo "$REPL_INFO" | grep -oP 'connected_slaves:\K\d+' | head -1)
MASTER_LINK=$(echo "$REPL_INFO" | grep -oP 'master_link_status:\K\w+' | head -1)
if [ "$ROLE" = "master" ]; then
echo "REPLICATION_STATUS:Role:$ROLE,Slaves:${CONNECTED_SLAVES:-0}"
elif [ "$ROLE" = "slave" ]; then
echo "REPLICATION_STATUS:Role:$ROLE,MasterLink:${MASTER_LINK:-unknown}"
else
echo "REPLICATION_STATUS:Role:$ROLE"
fi
REPL_INFO=$(eval $REDIS_CLI INFO replication 2>/dev/null)
ROLE=$(echo "$REPL_INFO" | grep -oP 'role:\K\w+' | head -1)
CONNECTED_SLAVES=$(echo "$REPL_INFO" | grep -oP 'connected_slaves:\K\d+' | head -1)
MASTER_LINK=$(echo "$REPL_INFO" | grep -oP 'master_link_status:\K\w+' | head -1)
if [ "$ROLE" = "master" ]; then
output_result "REDIS_DEPTH_REPLICATION_STATUS" "master/slaves:${CONNECTED_SLAVES:-0}"
elif [ "$ROLE" = "slave" ]; then
output_result "REDIS_DEPTH_REPLICATION_STATUS" "slave/link:${MASTER_LINK:-unknown}"
else
echo "REPLICATION_STATUS:N/A"
output_result "REDIS_DEPTH_REPLICATION_STATUS" "${ROLE:-N/A}"
fi
output_result "REDIS_DEPTH_ROLE" "${ROLE:-N/A}"
output_result "REDIS_DEPTH_SLAVES" "${CONNECTED_SLAVES:-0}"
# 慢日志TOP10
SLOW_LOG=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" SLOWLOG GET 10 2>&1 | grep -v "Warning" | head -40)
if [ -n "$SLOW_LOG" ]; then
# SLOWLOG输出格式:每4行一个记录 (ID, timestamp, duration, command)
# 我们需要获取所有记录的第3行(持续时间)
SLOW_COUNT=0
SLOWEST_TIME=0
line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# 每第3行是持续时间
if [ $((line_num % 4)) -eq 3 ]; then
if [[ "$line" =~ ^[0-9]+$ ]]; then
duration=$line
if [ $duration -gt $SLOWEST_TIME ]; then
SLOWEST_TIME=$duration
fi
SLOW_COUNT=$((SLOW_COUNT + 1))
fi
fi
done <<< "$SLOW_LOG"
if [ $SLOW_COUNT -gt 0 ]; then
echo "SLOW_LOG_TOP10:Count:$SLOW_COUNT,Slowest:${SLOWEST_TIME}us"
else
echo "SLOW_LOG_TOP10:Count:0"
fi
else
echo "SLOW_LOG_TOP10:Count:0"
# 缓存命中率
HITS=$(eval $REDIS_CLI INFO stats 2>/dev/null | grep keyspace_hits | cut -d: -f2 | tr -d '\r')
MISSES=$(eval $REDIS_CLI INFO stats 2>/dev/null | grep keyspace_misses | cut -d: -f2 | tr -d '\r')
if [ -n "$HITS" ] && [ -n "$MISSES" ] && [ "$((HITS + MISSES))" -gt 0 ] 2>/dev/null; then
HIT_RATE=$(awk "BEGIN {printf \"%.2f\", ($HITS*100)/($HITS+$MISSES)}")
output_result "REDIS_DEPTH_CACHE_HIT_RATE" "$HIT_RATE"
fi
# 命令统计TOP5
CMD_STATS=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO commandstats 2>&1 | grep -v "Warning" | grep "cmdstat_" | tr -d '\r')
if [ -n "$CMD_STATS" ]; then
# 提取命令调用次数TOP5
TOP_COMMANDS=$(echo "$CMD_STATS" | grep -oP 'cmdstat_\K[a-z]+(?=:calls=\d+)' | head -5 | tr '\n' ',' | sed 's/,$//')
TOP_CALLS=$(echo "$CMD_STATS" | grep -oP 'cmdstat_[a-z]+:calls=\K\d+' | head -5 | tr '\n' ',' | sed 's/,$//')
if [ -n "$TOP_COMMANDS" ]; then
# 组合命令和调用次数
CMD_DETAIL=""
IFS=',' read -ra CMDS <<< "$TOP_COMMANDS"
IFS=',' read -ra CALLS <<< "$TOP_CALLS"
for i in "${!CMDS[@]}"; do
if [ -n "$CMD_DETAIL" ]; then
CMD_DETAIL="$CMD_DETAIL,"
fi
CMD_DETAIL="$CMD_DETAIL${CMDS[$i]}:${CALLS[$i]}"
done
echo "COMMAND_STATS_TOP5:$CMD_DETAIL"
else
echo "COMMAND_STATS_TOP5:N/A"
fi
else
echo "COMMAND_STATS_TOP5:N/A"
fi
# 拒绝连接数
REJECTED=$(eval $REDIS_CLI INFO stats 2>/dev/null | grep rejected_connections | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_REJECTED_CONNECTIONS" "${REJECTED:-0}"
# 客户端列表摘要
CLIENT_LIST=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" CLIENT LIST 2>&1 | grep -v "Warning" | grep "id=" | tr -d '\r')
if [ -n "$CLIENT_LIST" ]; then
TOTAL_CLIENTS=$(echo "$CLIENT_LIST" | wc -l)
IDLE_CLIENTS=0
BLOCKING_CLIENTS=0
while IFS='=' read -r key value; do
if [[ "$value" =~ idle=([0-9]+) ]]; then
idle_time=${BASH_REMATCH[1]}
if [ "$idle_time" -gt 300 ]; then
IDLE_CLIENTS=$((IDLE_CLIENTS + 1))
fi
fi
if [[ "$value" =~ blocking=1 ]]; then
BLOCKING_CLIENTS=$((BLOCKING_CLIENTS + 1))
fi
done <<< "$CLIENT_LIST"
# 慢日志
SLOW_LOG=$(eval $REDIS_CLI SLOWLOG GET 10 2>/dev/null)
SLOW_COUNT=$(echo "$SLOW_LOG" | awk 'NR%4==3' | wc -l)
output_result "REDIS_DEPTH_SLOWLOG_COUNT" "$SLOW_COUNT"
echo "CLIENT_DETAIL:Total:$TOTAL_CLIENTS,IdleOver5min:$IDLE_CLIENTS,Blocking:$BLOCKING_CLIENTS"
else
echo "CLIENT_DETAIL:N/A"
fi
# 阻塞客户端
BLOCKED_CLIENTS=$(eval $REDIS_CLI INFO clients 2>/dev/null | grep blocked_clients | cut -d: -f2 | tr -d '\r')
output_result "REDIS_DEPTH_BLOCKED_CLIENTS" "${BLOCKED_CLIENTS:-0}"
# 缓存命中率
STATS_INFO=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" INFO stats 2>&1 | grep -v "Warning" | grep -E "keyspace_hits:|keyspace_misses:|rejected_connections:" | tr -d '\r')
if [ -n "$STATS_INFO" ]; then
HITS=$(echo "$STATS_INFO" | grep keyspace_hits | cut -d: -f2)
MISSES=$(echo "$STATS_INFO" | grep keyspace_misses | cut -d: -f2)
REJECTED=$(echo "$STATS_INFO" | grep rejected_connections | cut -d: -f2)
if [ -n "$HITS" ] && [ -n "$MISSES" ]; then
TOTAL_REQ=$((HITS + MISSES))
if [ "$TOTAL_REQ" -gt 0 ]; then
HIT_RATE=$(awk "BEGIN {printf \"%.2f\", ($HITS * 100) / $TOTAL_REQ}")
else
HIT_RATE="0.00"
# 键空间统计
KEYSPACE_INFO=$(eval $REDIS_CLI INFO keyspace 2>/dev/null | grep "^db")
if [ -n "$KEYSPACE_INFO" ]; then
TOTAL_KEYS=0
TOTAL_EXPIRES=0
while IFS=: read -r db_key info; do
if [[ "$info" =~ keys=([0-9]+) ]]; then
TOTAL_KEYS=$((TOTAL_KEYS + ${BASH_REMATCH[1]}))
fi
echo "CACHE_HIT_RATE:Hits:$HITS,Misses:$MISSES,Rate:$HIT_RATE%"
else
echo "CACHE_HIT_RATE:N/A"
fi
if [ -n "$REJECTED" ]; then
echo "REJECTED_CONNECTIONS:$REJECTED"
fi
else
echo "CACHE_HIT_RATE:N/A"
echo "REJECTED_CONNECTIONS:0"
if [[ "$info" =~ expires=([0-9]+) ]]; then
TOTAL_EXPIRES=$((TOTAL_EXPIRES + ${BASH_REMATCH[1]}))
fi
done <<< "$KEYSPACE_INFO"
output_result "REDIS_DEPTH_KEYSPACE_KEYS" "$TOTAL_KEYS"
output_result "REDIS_DEPTH_KEYSPACE_EXPIRES" "$TOTAL_EXPIRES"
fi
# 配置检查
CONFIG_MAXCLIENTS=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" CONFIG GET maxclients 2>&1 | grep -v "Warning" | tail -1 | tr -d '\r')
CONFIG_TIMEOUT=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" CONFIG GET timeout 2>&1 | grep -v "Warning" | tail -1 | tr -d '\r')
CONFIG_SAVE=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" CONFIG GET save 2>&1 | grep -v "Warning" | tail -1 | tr -d '\r')
echo "CONFIG_CHECK:MaxClients:$CONFIG_MAXCLIENTS,Timeout:${CONFIG_TIMEOUT}s,Save:$CONFIG_SAVE"
# ========== 补充:集群状态检测 ==========
CLUSTER_INFO=$(docker exec $CONTAINER redis-cli -a "$REDIS_PASSWORD" CLUSTER INFO 2>&1 | grep -v "Warning")
if [ -n "$CLUSTER_INFO" ]; then
# 解析集群状态
CLUSTER_STATE=$(echo "$CLUSTER_INFO" | grep -oP 'cluster_state:\K\w+' | head -1)
CLUSTER_SLOTS_ASSIGNED=$(echo "$CLUSTER_INFO" | grep -oP 'cluster_slots_assigned:\K\d+' | head -1)
CLUSTER_SLOTS_OK=$(echo "$CLUSTER_INFO" | grep -oP 'cluster_slots_ok:\K\d+' | head -1)
CLUSTER_KNOWN_NODES=$(echo "$CLUSTER_INFO" | grep -oP 'cluster_known_nodes:\K\d+' | head -1)
if [ "$CLUSTER_STATE" = "ok" ]; then
echo "CLUSTER_STATUS:State:OK,Nodes:${CLUSTER_KNOWN_NODES:-1},Slots:${CLUSTER_SLOTS_ASSIGNED:-0}/${CLUSTER_SLOTS_OK:-0}"
else
echo "CLUSTER_STATUS:State:${CLUSTER_STATE},Nodes:${CLUSTER_KNOWN_NODES:-1}"
fi
MAXCLIENTS=$(eval $REDIS_CLI CONFIG GET maxclients 2>/dev/null | tail -1 | tr -d '\r')
TIMEOUT_CFG=$(eval $REDIS_CLI CONFIG GET timeout 2>/dev/null | tail -1 | tr -d '\r')
output_result "REDIS_DEPTH_CONFIG_MAXCLIENTS" "${MAXCLIENTS:-N/A}"
output_result "REDIS_DEPTH_CONFIG_TIMEOUT" "${TIMEOUT_CFG:-0}"
# 集群状态
CLUSTER_INFO=$(eval $REDIS_CLI CLUSTER INFO 2>/dev/null)
CLUSTER_STATE=$(echo "$CLUSTER_INFO" | grep -oP 'cluster_state:\K\w+' | head -1)
if [ -n "$CLUSTER_STATE" ]; then
CLUSTER_NODES=$(echo "$CLUSTER_INFO" | grep -oP 'cluster_known_nodes:\K\d+' | head -1)
output_result "REDIS_DEPTH_CLUSTER_STATUS" "$CLUSTER_STATE/${CLUSTER_NODES:-1}"
else
# 非集群模式,检测单机状态
echo "CLUSTER_STATUS:Standalone"
output_result "REDIS_DEPTH_CLUSTER_STATUS" "Standalone"
fi
# RDB最后保存时间
RDB_LAST_SAVE=$(eval $REDIS_CLI INFO persistence 2>/dev/null | grep rdb_last_save_time | cut -d: -f2 | tr -d '\r')
if [ -n "$RDB_LAST_SAVE" ]; then
NOW_TS=$(date +%s)
RDB_AGE_SEC=$((NOW_TS - RDB_LAST_SAVE))
RDB_AGE_MIN=$((RDB_AGE_SEC / 60))
output_result "REDIS_DEPTH_RDB_AGE_MIN" "$RDB_AGE_MIN"
fi
\ No newline at end of file
#!/bin/bash
################################################################################
# 39_middleware_conn.sh — 中间件连接检测
# 功能: 检测 Redis/MySQL/EMQX/FastDFS 连接状态和运行健康度
# 参考: middleware_check.sh, MiddlewareCheck.psm1
# 日期: 2026-07-22
# 修订: 2026-07-22 - 改用服务自带工具 ping 优先,宿主机端口辅助诊断
# (避免容器内无 ss/netstat 导致误判"端口未监听")
################################################################################
# 获取脚本所在目录并加载依赖
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${LIB_DIR:-/tmp/check_modules}"
# 加载配置文件和通用函数库
if [ -f "$LIB_DIR/lib/config.sh" ]; then
source "$LIB_DIR/lib/config.sh"
else
echo "ERROR: 配置文件不存在: $LIB_DIR/lib/config.sh"
exit 1
fi
if [ -f "$LIB_DIR/lib/common.sh" ]; then
source "$LIB_DIR/lib/common.sh"
else
echo "ERROR: 通用函数库不存在: $LIB_DIR/lib/common.sh"
exit 1
fi
# 从配置获取容器匹配模式和端口
REDIS_CONTAINER="${CONTAINERS[redis]:-redis}"
MYSQL_CONTAINER="${CONTAINERS[mysql]:-mysql}"
EMQX_CONTAINER="${CONTAINERS[emqx]:-emqx}"
EMQX_MQTT_PORT="${EMQX_MQTT_PORT:-1883}"
EMQX_DASHBOARD_PORT="${EMQX_DASHBOARD_PORT:-18083}"
FASTDFS_CONTAINER="${CONTAINERS[ustorage]:-ustorage|storage}"
# ==================== Redis 连接检测 ====================
check_redis_connection() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$REDIS_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "MW_CONN_REDIS_STATUS" "未检测到容器"
output_result "MW_CONN_REDIS_STATUS_LEVEL" "严重"
return
fi
# redis-cli ping 测试(优先用密码,无密码也能测)
local ping_result
if [ -n "$REDIS_PASSWORD" ]; then
ping_result=$(docker exec "$actual_container" redis-cli -a "$REDIS_PASSWORD" --no-auth-warning ping 2>/dev/null)
else
ping_result=$(docker exec "$actual_container" redis-cli ping 2>/dev/null)
fi
if [ "$ping_result" = "PONG" ]; then
# 获取版本和运行信息
local version memory clients
if [ -n "$REDIS_PASSWORD" ]; then
version=$(docker exec "$actual_container" redis-cli -a "$REDIS_PASSWORD" --no-auth-warning \
INFO server 2>/dev/null | grep "redis_version" | cut -d: -f2 | tr -d '\r')
memory=$(docker exec "$actual_container" redis-cli -a "$REDIS_PASSWORD" --no-auth-warning \
INFO memory 2>/dev/null | grep "used_memory_human:" | cut -d: -f2 | tr -d '\r')
clients=$(docker exec "$actual_container" redis-cli -a "$REDIS_PASSWORD" --no-auth-warning \
INFO clients 2>/dev/null | grep "connected_clients:" | cut -d: -f2 | tr -d '\r')
else
version=$(docker exec "$actual_container" redis-cli INFO server 2>/dev/null | grep "redis_version" | cut -d: -f2 | tr -d '\r')
memory=$(docker exec "$actual_container" redis-cli INFO memory 2>/dev/null | grep "used_memory_human:" | cut -d: -f2 | tr -d '\r')
clients=$(docker exec "$actual_container" redis-cli INFO clients 2>/dev/null | grep "connected_clients:" | cut -d: -f2 | tr -d '\r')
fi
output_result "MW_CONN_REDIS_STATUS" "运行中"
output_result "MW_CONN_REDIS_STATUS_LEVEL" "正常"
output_result "MW_CONN_REDIS_VERSION" "${version:-未知}"
output_result "MW_CONN_REDIS_MEMORY" "${memory:-未知}"
output_result "MW_CONN_REDIS_CLIENTS" "${clients:-0}"
elif [ "$ping_result" = "NOAUTH" ] || echo "$ping_result" | grep -qi "auth"; then
# 需要密码但未配置或密码错误
output_result "MW_CONN_REDIS_STATUS" "需密码认证"
output_result "MW_CONN_REDIS_STATUS_LEVEL" "警告"
output_result "MW_CONN_REDIS_VERSION" "未知"
output_result "MW_CONN_REDIS_MEMORY" "未知"
output_result "MW_CONN_REDIS_CLIENTS" "未知"
else
# ping 失败,辅助诊断:从宿主机检查端口
local host_port
host_port=$(ss -tlnp 2>/dev/null | grep -oP ':\K6379' | head -1)
if [ -n "$host_port" ]; then
output_result "MW_CONN_REDIS_STATUS" "端口监听但连接失败"
output_result "MW_CONN_REDIS_STATUS_LEVEL" "严重"
else
output_result "MW_CONN_REDIS_STATUS" "连接失败"
output_result "MW_CONN_REDIS_STATUS_LEVEL" "严重"
fi
fi
}
# ==================== MySQL 连接检测 ====================
check_mysql_connection() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$MYSQL_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "MW_CONN_MYSQL_STATUS" "未检测到容器"
output_result "MW_CONN_MYSQL_STATUS_LEVEL" "严重"
return
fi
# mysqladmin ping 测试(在容器内执行,连内部 3306)
local ping_result
ping_result=$(docker exec "$actual_container" mysqladmin -uroot -p"$MYSQL_PASSWORD" ping 2>/dev/null | grep -c "alive")
if [ "$ping_result" -gt 0 ]; then
# 获取版本信息
local version
version=$(docker exec "$actual_container" mysql -uroot -p"$MYSQL_PASSWORD" -N -e \
"SELECT VERSION();" 2>/dev/null)
output_result "MW_CONN_MYSQL_STATUS" "运行中"
output_result "MW_CONN_MYSQL_STATUS_LEVEL" "正常"
output_result "MW_CONN_MYSQL_VERSION" "${version:-未知}"
else
# ping 失败,辅助诊断:检查容器内是否有 mysql 进程
local mysql_proc
mysql_proc=$(docker exec "$actual_container" ps aux 2>/dev/null | grep -v grep | grep -cE 'mysqld' || echo "0")
if [ "$mysql_proc" -gt 0 ]; then
output_result "MW_CONN_MYSQL_STATUS" "进程存在但连接失败"
output_result "MW_CONN_MYSQL_STATUS_LEVEL" "严重"
else
output_result "MW_CONN_MYSQL_STATUS" "连接失败"
output_result "MW_CONN_MYSQL_STATUS_LEVEL" "严重"
fi
fi
}
# ==================== EMQX 连接检测 ====================
check_emqx_connection() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$EMQX_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "MW_CONN_EMQX_STATUS" "未检测到容器"
output_result "MW_CONN_EMQX_STATUS_LEVEL" "严重"
return
fi
# 从宿主机检查 MQTT 端口监听(EMQX 通常有端口映射)
local mqtt_listening
mqtt_listening=$(ss -tlnp 2>/dev/null | grep -q ":${EMQX_MQTT_PORT}" && echo 'yes' || echo 'no')
# 从宿主机检查 Dashboard 端口
local dashboard_listening
dashboard_listening=$(ss -tlnp 2>/dev/null | grep -q ":${EMQX_DASHBOARD_PORT}" && echo 'yes' || echo 'no')
# 调用 Dashboard API 验证服务状态
local api_status=""
if [ "$dashboard_listening" = "yes" ]; then
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
"http://localhost:${EMQX_DASHBOARD_PORT}/api/v5/status" 2>/dev/null)
api_status="HTTP $http_code"
else
api_status="Dashboard 未监听"
fi
# 检查进程状态
local proc_count
proc_count=$(docker exec "$actual_container" ps aux 2>/dev/null | \
grep -v grep | grep -cE 'emqx|beam' || echo "0")
if [ "$mqtt_listening" = "yes" ] && [ "$proc_count" -ge 1 ]; then
output_result "MW_CONN_EMQX_STATUS" "运行中"
output_result "MW_CONN_EMQX_STATUS_LEVEL" "正常"
elif [ "$proc_count" -ge 1 ]; then
output_result "MW_CONN_EMQX_STATUS" "MQTT 端口未监听"
output_result "MW_CONN_EMQX_STATUS_LEVEL" "严重"
else
output_result "MW_CONN_EMQX_STATUS" "进程异常"
output_result "MW_CONN_EMQX_STATUS_LEVEL" "严重"
fi
output_result "MW_CONN_EMQX_MQTT_PORT" "$EMQX_MQTT_PORT"
output_result "MW_CONN_EMQX_DASHBOARD" "$EMQX_DASHBOARD_PORT"
output_result "MW_CONN_EMQX_API_STATUS" "$api_status"
}
# ==================== FastDFS 连接检测 ====================
check_fastdfs_connection() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$FASTDFS_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "MW_CONN_FASTDFS_STATUS" "未检测到容器"
output_result "MW_CONN_FASTDFS_STATUS_LEVEL" "严重"
return
fi
# 获取网络模式
local network_mode
network_mode=$(docker inspect "$actual_container" 2>/dev/null | \
grep -oP '"NetworkMode": "\K[^"]+' | head -1)
output_result "MW_CONN_FASTDFS_NETWORK" "${network_mode:-未知}"
# 根据网络模式检查进程
local tracker_count=0
local storage_count=0
if [ "$network_mode" = "host" ]; then
tracker_count=$(ps aux 2>/dev/null | grep -v grep | grep 'fdfs_trackerd' | wc -l)
storage_count=$(ps aux 2>/dev/null | grep -v grep | grep 'fdfs_storaged' | wc -l)
else
tracker_count=$(docker exec "$actual_container" ps aux 2>/dev/null | \
grep -v grep | grep 'fdfs_trackerd' | wc -l)
storage_count=$(docker exec "$actual_container" ps aux 2>/dev/null | \
grep -v grep | grep 'fdfs_storaged' | wc -l)
fi
if [ "$tracker_count" -gt 0 ] && [ "$storage_count" -gt 0 ]; then
output_result "MW_CONN_FASTDFS_STATUS" "运行中"
output_result "MW_CONN_FASTDFS_STATUS_LEVEL" "正常"
else
output_result "MW_CONN_FASTDFS_STATUS" "进程异常"
output_result "MW_CONN_FASTDFS_STATUS_LEVEL" "严重"
fi
}
# ==================== 主检测流程 ====================
main() {
log_info "开始中间件连接检测..."
set +e
check_redis_connection 2>/dev/null || true
check_mysql_connection 2>/dev/null || true
check_emqx_connection 2>/dev/null || true
check_fastdfs_connection 2>/dev/null || true
log_info "中间件连接检测完成"
}
main
#!/bin/bash
################################################################################
# 46_security_deep.sh — 安全合规深度检测
# 功能: 检测弱密码、安全基线、可疑文件、异常配置
# 参考: SecurityCheck.psm1(PowerShell 移植)
# 日期: 2026-07-22
################################################################################
# 获取脚本所在目录并加载依赖
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${LIB_DIR:-/tmp/check_modules}"
# 加载配置文件和通用函数库
if [ -f "$LIB_DIR/lib/config.sh" ]; then
source "$LIB_DIR/lib/config.sh"
else
echo "ERROR: 配置文件不存在: $LIB_DIR/lib/config.sh"
exit 1
fi
if [ -f "$LIB_DIR/lib/common.sh" ]; then
source "$LIB_DIR/lib/common.sh"
else
echo "ERROR: 通用函数库不存在: $LIB_DIR/lib/common.sh"
exit 1
fi
# 从配置获取容器匹配模式
MYSQL_CONTAINER="${CONTAINERS[mysql]:-mysql}"
REDIS_CONTAINER="${CONTAINERS[redis]:-redis}"
EMQX_CONTAINER="${CONTAINERS[emqx]:-emqx}"
# 安全白名单(从 config.sh 读取或使用默认值)
SUID_WHITELIST="${SUID_WHITELIST:-/usr/bin/sudo,/usr/bin/passwd,/usr/bin/su,/usr/bin/ping,/usr/bin/mount,/usr/bin/umount,/usr/bin/newgrp,/usr/bin/chsh,/usr/bin/chfn,/usr/bin/gpasswd,/usr/lib/openssh/ssh-keysign,/usr/libexec/openssh/ssh-keysign}"
PORT_WHITELIST="${PORT_WHITELIST_NEW:-22,80,443,1883,3306,6379,8083,8084,8888,18083}"
EMQX_DASHBOARD_PORT="${EMQX_DASHBOARD_PORT:-18083}"
# ==================== MySQL 安全检测 ====================
# 检测 MySQL Root 访问范围
check_mysql_root_access() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$MYSQL_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST" "未检测到容器"
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST_LEVEL" "跳过"
return
fi
# 查询 root 用户的 host 字段
local hosts
hosts=$(docker exec "$actual_container" mysql -uroot -p"$MYSQL_PASSWORD" -N -e \
"SELECT host FROM mysql.user WHERE user='root'" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [ -z "$hosts" ]; then
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST" "查询失败"
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST_LEVEL" "跳过"
return
fi
# 判断是否存在 % 通配符(允许任意主机连接)
if echo "$hosts" | grep -q '%'; then
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST" "$hosts"
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST_LEVEL" "严重"
else
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST" "$hosts"
output_result "SECURITY_DEEP_MYSQL_ROOT_HOST_LEVEL" "正常"
fi
}
# 检测 MySQL 空密码用户
check_mysql_empty_password() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$MYSQL_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS" "未检测到容器"
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS_LEVEL" "跳过"
return
fi
# 查询 authentication_string 为空的用户
local empty_count
empty_count=$(docker exec "$actual_container" mysql -uroot -p"$MYSQL_PASSWORD" -N -e \
"SELECT COUNT(*) FROM mysql.user WHERE authentication_string='' OR (plugin='mysql_native_password' AND authentication_string='')" \
2>/dev/null | tr -d '\r\n')
if [ -z "$empty_count" ]; then
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS" "查询失败"
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS_LEVEL" "跳过"
return
fi
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS" "$empty_count"
if [ "$empty_count" -gt 0 ]; then
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS_LEVEL" "严重"
else
output_result "SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS_LEVEL" "正常"
fi
}
# ==================== Redis 密码检测 ====================
check_redis_password() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$REDIS_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "SECURITY_DEEP_REDIS_PASSWORD" "未检测到容器"
output_result "SECURITY_DEEP_REDIS_PASSWORD_LEVEL" "跳过"
return
fi
# 查找 requirepass 配置
local requirepass
requirepass=$(docker exec "$actual_container" sh -c \
"grep -E '^requirepass' /etc/redis/redis.conf 2>/dev/null || \
grep -E '^requirepass' /usr/local/etc/redis/redis.conf 2>/dev/null || \
echo 'NO_CONFIG'" | awk '{print $2}')
if [ "$requirepass" = "NO_CONFIG" ] || [ -z "$requirepass" ]; then
output_result "SECURITY_DEEP_REDIS_PASSWORD" "无密码"
output_result "SECURITY_DEEP_REDIS_PASSWORD_LEVEL" "严重"
else
# 弱密码字典检测
local weak_passwords="123456 password root admin redis test guest"
local is_weak="no"
for wp in $weak_passwords; do
if [ "$requirepass" = "$wp" ]; then
is_weak="yes"
break
fi
done
if [ "$is_weak" = "yes" ]; then
output_result "SECURITY_DEEP_REDIS_PASSWORD" "弱密码"
output_result "SECURITY_DEEP_REDIS_PASSWORD_LEVEL" "严重"
else
output_result "SECURITY_DEEP_REDIS_PASSWORD" "已设置"
output_result "SECURITY_DEEP_REDIS_PASSWORD_LEVEL" "正常"
fi
fi
}
# ==================== EMQX 默认密码检测 ====================
check_emqx_default_password() {
local actual_container
actual_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "$EMQX_CONTAINER" | head -1)
if [ -z "$actual_container" ]; then
output_result "SECURITY_DEEP_EMQX_DEFAULT_PWD" "未检测到容器"
output_result "SECURITY_DEEP_EMQX_DEFAULT_PWD_LEVEL" "跳过"
return
fi
# 使用默认密码 admin:public 测试 Dashboard API
local http_code
http_code=$(docker exec "$actual_container" curl -s -o /dev/null -w '%{http_code}' \
-u admin:public "http://localhost:$EMQX_DASHBOARD_PORT/api/v5/status" 2>/dev/null)
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
output_result "SECURITY_DEEP_EMQX_DEFAULT_PWD" "使用默认密码"
output_result "SECURITY_DEEP_EMQX_DEFAULT_PWD_LEVEL" "严重"
else
output_result "SECURITY_DEEP_EMQX_DEFAULT_PWD" "非默认密码"
output_result "SECURITY_DEEP_EMQX_DEFAULT_PWD_LEVEL" "正常"
fi
}
# ==================== Linux 空密码账户检测 ====================
check_linux_empty_password() {
# 需要 root 权限读取 /etc/shadow
if [ ! -r /etc/shadow ]; then
output_result "SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT" "无法读取"
output_result "SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT_LEVEL" "跳过"
return
fi
# 第 2 字段为空表示空密码
local empty_count
empty_count=$(awk -F: 'length($2)==0 {print $1}' /etc/shadow 2>/dev/null | wc -l)
output_result "SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT" "$empty_count"
if [ "$empty_count" -gt 0 ]; then
output_result "SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT_LEVEL" "严重"
else
output_result "SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT_LEVEL" "正常"
fi
}
# ==================== SUID 文件检测 ====================
check_suid_files() {
# 扫描 SUID 文件,排除 Docker/proc/sys 路径
local all_suid
all_suid=$(find / -perm -4000 -type f \
-not -path '/data/dockers/*' \
-not -path '/var/lib/docker/*' \
-not -path '/proc/*' \
-not -path '/sys/*' \
2>/dev/null | sort)
local all_count
all_count=$(echo "$all_suid" | grep -c '/' 2>/dev/null || echo "0")
# 与白名单对比
local suspicious=0
while IFS= read -r file; do
[ -z "$file" ] && continue
if ! echo "$SUID_WHITELIST" | grep -qF "$file"; then
suspicious=$((suspicious + 1))
fi
done <<< "$all_suid"
output_result "SECURITY_DEEP_SUID_COUNT" "$all_count"
output_result "SECURITY_DEEP_SUSPICIOUS_SUID_COUNT" "$suspicious"
if [ "$suspicious" -gt 0 ]; then
output_result "SECURITY_DEEP_SUSPICIOUS_SUID_COUNT_LEVEL" "警告"
else
output_result "SECURITY_DEEP_SUSPICIOUS_SUID_COUNT_LEVEL" "正常"
fi
}
# ==================== Crontab 检测 ====================
check_crontab() {
# 收集所有用户和系统级 crontab
local cron_entries=""
# 用户 crontab
for user in $(cut -d: -f1 /etc/passwd 2>/dev/null); do
local user_cron
user_cron=$(crontab -u "$user" -l 2>/dev/null | grep -vE '^\s*#|^\s*$' || true)
[ -n "$user_cron" ] && cron_entries+="$user_cron"$'\n'
done
# 系统 crontab
local sys_cron
sys_cron=$(cat /etc/crontab 2>/dev/null | grep -vE '^\s*#|^\s*$' || true)
[ -n "$sys_cron" ] && cron_entries+="$sys_cron"$'\n'
# cron.d 目录
if [ -d /etc/cron.d ]; then
local cron_d
cron_d=$(cat /etc/cron.d/* 2>/dev/null | grep -vE '^\s*#|^\s*$' || true)
[ -n "$cron_d" ] && cron_entries+="$cron_d"$'\n'
fi
# 已知安全模式(白名单关键词)
local safe_patterns="health_check|check_server|backup|logrotate|certbot|docker|container|ntpdate|sysstat"
local suspicious=0
while IFS= read -r line; do
[ -z "$line" ] && continue
if ! echo "$line" | grep -qiE "$safe_patterns"; then
suspicious=$((suspicious + 1))
fi
done <<< "$cron_entries"
output_result "SECURITY_DEEP_SUSPICIOUS_CRON_COUNT" "$suspicious"
if [ "$suspicious" -gt 0 ]; then
output_result "SECURITY_DEEP_SUSPICIOUS_CRON_COUNT_LEVEL" "警告"
else
output_result "SECURITY_DEEP_SUSPICIOUS_CRON_COUNT_LEVEL" "正常"
fi
}
# ==================== SSH 暴力破解检测 ====================
check_ssh_bruteforce() {
local logfile=""
if [ -f /var/log/secure ]; then
logfile=/var/log/secure
elif [ -f /var/log/auth.log ]; then
logfile=/var/log/auth.log
fi
if [ -z "$logfile" ]; then
output_result "SECURITY_DEEP_SSH_BRUTEFORCE_IPS" "无日志"
output_result "SECURITY_DEEP_SSH_BRUTEFORCE_IPS_LEVEL" "跳过"
return
fi
# 统计失败登录 IP(>=50 次视为暴力破解)
local bruteforce_ips
bruteforce_ips=$(grep -i "failed\|failure\|invalid" "$logfile" 2>/dev/null | \
grep -oE 'from [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | \
awk '{print $2}' | sort | uniq -c | \
awk '$1 >= 50 {print $2}')
local bruteforce_count
bruteforce_count=$(echo "$bruteforce_ips" | grep -cE '[0-9]+\.' 2>/dev/null || echo "0")
output_result "SECURITY_DEEP_SSH_BRUTEFORCE_IPS" "$bruteforce_count"
if [ "$bruteforce_count" -gt 0 ]; then
output_result "SECURITY_DEEP_SSH_BRUTEFORCE_IPS_LEVEL" "严重"
else
output_result "SECURITY_DEEP_SSH_BRUTEFORCE_IPS_LEVEL" "正常"
fi
}
# ==================== 异常端口检测 ====================
check_unexpected_ports() {
# 获取监听端口列表
local listening_ports
listening_ports=$(ss -tlnp 2>/dev/null | grep LISTEN | \
awk '{print $4}' | grep -oE '[0-9]+$' | sort -nu)
# 与白名单对比
local unexpected=""
local unexpected_count=0
for port in $listening_ports; do
if ! echo ",$PORT_WHITELIST," | grep -qF ",$port,"; then
if [ -n "$unexpected" ]; then
unexpected="$unexpected,$port"
else
unexpected="$port"
fi
unexpected_count=$((unexpected_count + 1))
fi
done
output_result "SECURITY_DEEP_UNEXPECTED_PORTS" "$unexpected"
output_result "SECURITY_DEEP_UNEXPECTED_PORTS_COUNT" "$unexpected_count"
if [ "$unexpected_count" -gt 0 ]; then
output_result "SECURITY_DEEP_UNEXPECTED_PORTS_COUNT_LEVEL" "警告"
else
output_result "SECURITY_DEEP_UNEXPECTED_PORTS_COUNT_LEVEL" "正常"
fi
}
# ==================== 主检测流程 ====================
main() {
log_info "开始安全合规深度检测..."
set +e
# 弱密码检测
check_mysql_root_access 2>/dev/null || true
check_mysql_empty_password 2>/dev/null || true
check_redis_password 2>/dev/null || true
check_emqx_default_password 2>/dev/null || true
check_linux_empty_password 2>/dev/null || true
# 安全基线扫描
check_suid_files 2>/dev/null || true
check_crontab 2>/dev/null || true
check_ssh_bruteforce 2>/dev/null || true
check_unexpected_ports 2>/dev/null || true
log_info "安全合规深度检测完成"
}
main
......@@ -79,6 +79,8 @@ ALL_MODULES: List[CheckModule] = [
{SUITE_FULL}, "44_system_logs.sh"),
CheckModule("45_time_sync", "时间同步", "system",
{SUITE_FULL}, "45_time_sync.sh"),
CheckModule("46_security_deep", "安全合规深度检测", "system",
{SUITE_FULL}, "46_security_deep.sh"),
# ---- service 类 ----
CheckModule("20_docker_basic", "Docker基础", "service",
{SUITE_QUICK, SUITE_FULL}, "20_docker_basic.sh"),
......@@ -114,6 +116,8 @@ ALL_MODULES: List[CheckModule] = [
{SUITE_FULL}, "37_ustorage_check.sh"),
CheckModule("38_utracker_check", "UTracker容器监测", "service",
{SUITE_FULL}, "38_utracker_check.sh"),
CheckModule("39_middleware_conn", "中间件连接检测", "service",
{SUITE_FULL}, "39_middleware_conn.sh"),
]
_BY_ID: Dict[str, CheckModule] = {m.id: m for m in ALL_MODULES}
......
......@@ -70,6 +70,7 @@ DISPLAY_NAMES: Dict[str, str] = {
"TRANSPARENT_HUGEPAGE": "透明大页状态",
"VM_PSWPIN": "换入页面数",
"VM_PSWPOUT": "换出页面数",
"VM_PGMajFAULT": "主缺页错误数",
"MEMORY_STATUS": "内存状态",
# ---- 04 磁盘检测 ----
......@@ -129,6 +130,60 @@ DISPLAY_NAMES: Dict[str, str] = {
"MYSQL_CHARSET": "MySQL字符集",
"MYSQL_COLLATION": "MySQL排序规则",
# ---- 23 MySQL深度检测 ----
"MYSQL_DEPTH_UPTIME_DAYS": "MySQL深度-运行天数",
"MYSQL_DEPTH_QPS": "MySQL深度-QPS",
"MYSQL_DEPTH_TPS": "MySQL深度-TPS",
"MYSQL_DEPTH_CONNECTION_USAGE": "MySQL深度-连接使用率",
"MYSQL_DEPTH_CONNECTIONS": "MySQL深度-连接数",
"MYSQL_DEPTH_SLOW_QUERY_COUNT": "MySQL深度-慢查询数",
"MYSQL_DEPTH_SLOW_QUERY_ENABLED": "MySQL深度-慢查询日志",
"MYSQL_DEPTH_DEADLOCKS": "MySQL深度-死锁数",
"MYSQL_DEPTH_BUFFER_POOL_HIT_RATE": "MySQL深度-缓冲池命中率",
"MYSQL_DEPTH_CACHE_HIT_RATE": "MySQL深度-表缓存命中率",
"MYSQL_DEPTH_ACTIVE_QUERIES": "MySQL深度-活跃查询数",
"MYSQL_DEPTH_BINLOG_STATUS": "MySQL深度-Binlog状态",
"MYSQL_DEPTH_BINLOG_EXPIRE_DAYS": "MySQL深度-Binlog过期天数",
"MYSQL_DEPTH_FRAGMENTED_TABLES": "MySQL深度-碎片表数",
"MYSQL_DEPTH_CONN_ERRORS": "MySQL深度-连接错误数",
"MYSQL_DEPTH_INNODB_BP_SIZE": "MySQL深度-InnoDB缓冲池大小",
"MYSQL_DEPTH_TRX_ACTIVE": "MySQL深度-活跃事务数",
"MYSQL_DEPTH_LOCK_WAITS": "MySQL深度-锁等待数",
"MYSQL_DEPTH_DATABASE_SIZE": "MySQL深度-数据库总大小",
"MYSQL_DEPTH_TABLE_COUNT": "MySQL深度-表总数",
"MYSQL_DEPTH_REPLICATION_STATUS": "MySQL深度-复制状态",
"MYSQL_DEPTH_SLAVE_IO_RUNNING": "MySQL深度-从库IO线程",
"MYSQL_DEPTH_SLAVE_SQL_RUNNING": "MySQL深度-从库SQL线程",
"MYSQL_DEPTH_SLAVE_DELAY": "MySQL深度-主从延迟",
"MYSQL_DEPTH_ACTIVE_PROCESSLIST": "MySQL深度-进程列表",
"MYSQL_DEPTH_TOP_TABLES": "MySQL深度-大表TOP5",
"MYSQL_DEPTH_TEMP_TABLE_RATE": "MySQL深度-临时表使用率",
"MYSQL_DEPTH_TABLES_WITHOUT_INDEX": "MySQL深度-缺索引表数",
# ---- 25 Redis深度检测 ----
"REDIS_DEPTH_VERSION": "Redis深度-版本",
"REDIS_DEPTH_UPTIME_DAYS": "Redis深度-运行天数",
"REDIS_DEPTH_KEY_COUNT": "Redis深度-键数量",
"REDIS_DEPTH_USED_MEMORY": "Redis深度-内存使用",
"REDIS_DEPTH_MEM_FRAGMENTATION": "Redis深度-内存碎片率",
"REDIS_DEPTH_CLIENT_COUNT": "Redis深度-客户端数",
"REDIS_DEPTH_OPS_PER_SEC": "Redis深度-每秒操作数",
"REDIS_DEPTH_RDB_PERSISTENCE": "Redis深度-RDB持久化",
"REDIS_DEPTH_AOF_PERSISTENCE": "Redis深度-AOF持久化",
"REDIS_DEPTH_REPLICATION_STATUS": "Redis深度-复制状态",
"REDIS_DEPTH_ROLE": "Redis深度-角色",
"REDIS_DEPTH_SLAVES": "Redis深度-从库数",
"REDIS_DEPTH_CACHE_HIT_RATE": "Redis深度-缓存命中率",
"REDIS_DEPTH_REJECTED_CONNECTIONS": "Redis深度-拒绝连接数",
"REDIS_DEPTH_SLOWLOG_COUNT": "Redis深度-慢日志数",
"REDIS_DEPTH_BLOCKED_CLIENTS": "Redis深度-阻塞客户端数",
"REDIS_DEPTH_KEYSPACE_KEYS": "Redis深度-键空间键数",
"REDIS_DEPTH_KEYSPACE_EXPIRES": "Redis深度-过期键数",
"REDIS_DEPTH_CONFIG_MAXCLIENTS": "Redis深度-最大客户端配置",
"REDIS_DEPTH_CONFIG_TIMEOUT": "Redis深度-超时配置",
"REDIS_DEPTH_CLUSTER_STATUS": "Redis深度-集群状态",
"REDIS_DEPTH_RDB_AGE_MIN": "Redis深度-RDB年龄(分钟)",
# ---- 24 Redis基础 ----
"REDIS_CONTAINER_STATUS": "Redis容器状态",
"REDIS_CONTAINER_LEVEL": "Redis容器等级",
......@@ -596,6 +651,46 @@ DISPLAY_NAMES: Dict[str, str] = {
"HTTPS_CERT_STATUS": "HTTPS证书状态",
"SYSTEM_UPTIME_DAYS": "系统运行天数",
# ---- 46 安全合规深度检测 ----
"SECURITY_DEEP_MYSQL_ROOT_HOST": "MySQL Root 访问范围",
"SECURITY_DEEP_MYSQL_ROOT_HOST_LEVEL": "MySQL Root 访问等级",
"SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS": "MySQL 空密码用户数",
"SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS_LEVEL": "MySQL 空密码等级",
"SECURITY_DEEP_REDIS_PASSWORD": "Redis 密码状态",
"SECURITY_DEEP_REDIS_PASSWORD_LEVEL": "Redis 密码等级",
"SECURITY_DEEP_EMQX_DEFAULT_PWD": "EMQX 默认密码状态",
"SECURITY_DEEP_EMQX_DEFAULT_PWD_LEVEL": "EMQX 默认密码等级",
"SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT": "Linux 空密码账户数",
"SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT_LEVEL": "Linux 空密码等级",
"SECURITY_DEEP_SUID_COUNT": "SUID 文件总数",
"SECURITY_DEEP_SUSPICIOUS_SUID_COUNT": "可疑 SUID 文件数",
"SECURITY_DEEP_SUSPICIOUS_SUID_COUNT_LEVEL": "SUID 安全等级",
"SECURITY_DEEP_SUSPICIOUS_CRON_COUNT": "可疑定时任务数",
"SECURITY_DEEP_SUSPICIOUS_CRON_COUNT_LEVEL": "定时任务安全等级",
"SECURITY_DEEP_SSH_BRUTEFORCE_IPS": "SSH 暴力破解 IP 数",
"SECURITY_DEEP_SSH_BRUTEFORCE_IPS_LEVEL": "SSH 暴力破解等级",
"SECURITY_DEEP_UNEXPECTED_PORTS": "异常端口列表",
"SECURITY_DEEP_UNEXPECTED_PORTS_COUNT": "异常端口数",
"SECURITY_DEEP_UNEXPECTED_PORTS_COUNT_LEVEL": "端口安全等级",
# ---- 39 中间件连接检测 ----
"MW_CONN_REDIS_STATUS": "Redis 连接状态",
"MW_CONN_REDIS_STATUS_LEVEL": "Redis 连接等级",
"MW_CONN_REDIS_VERSION": "Redis 版本",
"MW_CONN_REDIS_MEMORY": "Redis 内存使用",
"MW_CONN_REDIS_CLIENTS": "Redis 连接数",
"MW_CONN_MYSQL_STATUS": "MySQL 连接状态",
"MW_CONN_MYSQL_STATUS_LEVEL": "MySQL 连接等级",
"MW_CONN_MYSQL_VERSION": "MySQL 版本",
"MW_CONN_EMQX_STATUS": "EMQX 连接状态",
"MW_CONN_EMQX_STATUS_LEVEL": "EMQX 连接等级",
"MW_CONN_EMQX_MQTT_PORT": "EMQX MQTT 端口",
"MW_CONN_EMQX_DASHBOARD": "EMQX Dashboard 端口",
"MW_CONN_EMQX_API_STATUS": "EMQX API 状态",
"MW_CONN_FASTDFS_STATUS": "FastDFS 连接状态",
"MW_CONN_FASTDFS_STATUS_LEVEL": "FastDFS 连接等级",
"MW_CONN_FASTDFS_NETWORK": "FastDFS 网络模式",
# ---- 容器进程检测(动态KEY) ----
"PROCESSES_mysql": "MySQL进程数",
"PROCESSES_redis": "Redis进程数",
......
......@@ -34,6 +34,12 @@ _VALUE_STATUS_MAP = {
"正常": NORMAL, "OK": NORMAL, "运行中": NORMAL, "是": NORMAL,
"警告": WARNING, "已停止": WARNING,
"严重": CRITICAL, "ERROR": CRITICAL, "异常": CRITICAL, "未运行": CRITICAL,
# 中间件连接 / 安全合规深度检测的异常状态词
"端口未监听": CRITICAL, "MQTT 端口未监听": CRITICAL,
"连接失败": CRITICAL, "进程异常": CRITICAL,
"未检测到容器": CRITICAL, "容器未运行": CRITICAL,
"无密码": CRITICAL, "弱密码": CRITICAL, "使用默认密码": CRITICAL,
"查询失败": WARNING, "无法读取": WARNING,
}
# 无效值/未安装占位符(这些值表示服务未安装或检测失败,不作为正常检测项)
......
......@@ -56,6 +56,30 @@ THRESHOLDS: Dict[str, str] = {
"REDIS_MEMORY_USAGE": ">80%",
"REDIS_BLOCKED_CLIENTS": ">10",
"REDIS_SLAVES": "<1",
# ---- MySQL深度 ----
"MYSQL_DEPTH_CONNECTION_USAGE": ">80%",
"MYSQL_DEPTH_SLOW_QUERY_COUNT": ">100",
"MYSQL_DEPTH_BUFFER_POOL_HIT_RATE": "<95%",
"MYSQL_DEPTH_SLAVE_DELAY": ">10s",
"MYSQL_DEPTH_DEADLOCKS": ">0",
"MYSQL_DEPTH_LOCK_WAITS": ">0",
"MYSQL_DEPTH_FRAGMENTED_TABLES": ">10",
# ---- Redis深度 ----
"REDIS_DEPTH_MEM_FRAGMENTATION": ">5",
"REDIS_DEPTH_CACHE_HIT_RATE": "<90%",
"REDIS_DEPTH_BLOCKED_CLIENTS": ">10",
"REDIS_DEPTH_REJECTED_CONNECTIONS": ">0",
"REDIS_DEPTH_SLOWLOG_COUNT": ">10",
# ---- 安全合规深度检测 ----
"SECURITY_DEEP_MYSQL_EMPTY_PWD_USERS": ">0",
"SECURITY_DEEP_LINUX_EMPTY_PWD_COUNT": ">0",
"SECURITY_DEEP_SUSPICIOUS_SUID_COUNT": ">0",
"SECURITY_DEEP_SUSPICIOUS_CRON_COUNT": ">0",
"SECURITY_DEEP_SSH_BRUTEFORCE_IPS": ">0",
"SECURITY_DEEP_UNEXPECTED_PORTS_COUNT": ">0",
}
......
{% extends "service_manage/base.html" %}
{% block title %}服务授权{% endblock %}
{% block breadcrumb %}服务授权{% endblock %}
{% block extra_css %}
<style>
.steps-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 24px; }
.step-card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid var(--gray-200); position: relative; }
.step-number { position: absolute; top: -12px; left: 20px; background: var(--primary); color: #fff; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; }
.step-title { font-size: 16px; font-weight: 700; color: var(--gray-900); margin-bottom: 16px; padding-top: 8px; }
.step-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
.btn-action { display: inline-flex; align-items: center; gap: 6px; padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; transition: all 0.2s; }
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-outline { background: #fff; color: var(--gray-700); border: 1px solid var(--gray-300); }
.btn-outline:hover { background: var(--gray-50); border-color: var(--gray-400); }
.step-desc { font-size: 13px; color: var(--gray-500); line-height: 1.6; }
.form-group { margin-bottom: 16px; }
.form-label { display: block; font-size: 14px; font-weight: 600; color: var(--gray-700); margin-bottom: 6px; }
.form-label .required { color: var(--red); margin-left: 2px; }
.form-input { width: 100%; padding: 10px 12px; border: 1px solid var(--gray-300); border-radius: 8px; font-size: 14px; color: var(--gray-900); background: #fff; transition: border-color 0.2s, box-shadow 0.2s; }
.form-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); }
.form-input::placeholder { color: var(--gray-400); }
textarea.form-input { min-height: 80px; resize: vertical; }
.btn-save { display: inline-flex; align-items: center; gap: 6px; background: var(--primary); color: #fff; border: none; padding: 10px 24px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-save:hover { background: var(--primary-hover); }
.upload-area { border: 2px dashed var(--gray-300); border-radius: 8px; padding: 20px; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; }
.upload-area:hover { border-color: var(--primary); background: var(--primary-light); }
.upload-icon { font-size: 24px; margin-bottom: 8px; }
.upload-text { font-size: 14px; color: var(--gray-500); }
.upload-hint { font-size: 12px; color: var(--gray-400); margin-top: 4px; }
@media screen and (max-width: 768px) { .steps-container { grid-template-columns: 1fr; } }
</style>
{% endblock %}
{% block content %}
<div class="section-head"><h2 class="section-title">🔑 服务授权</h2></div>
<div class="steps-container">
<div class="step-card">
<div class="step-number">1</div>
<div class="step-title">申请服务授权</div>
<div class="step-actions">
<button class="btn-action btn-outline" onclick="handleDownload('activation')">📥 下载激活文件</button>
<button class="btn-action btn-primary" onclick="document.getElementById('auth-file').click()">📤 上传授权文件</button>
<input type="file" id="auth-file" style="display:none" accept=".lic,.key,.dat" onchange="handleAuthUpload(this)">
</div>
<div class="upload-area" onclick="document.getElementById('auth-file').click()">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽上传授权文件</div>
<div class="upload-hint">支持 .lic / .key / .dat 格式</div>
</div>
<div class="step-desc" style="margin-top: 12px;">下载激活文件后发送给厂商获取授权,收到授权文件后上传即可完成授权申请。</div>
</div>
<div class="step-card">
<div class="step-number">2</div>
<div class="step-title">填写项目信息</div>
<form id="project-form" onsubmit="handleSaveProject(event)">
<div class="form-group"><label class="form-label">项目销售 <span class="required">*</span></label><input type="text" class="form-input" placeholder="请输入项目的销售" required></div>
<div class="form-group"><label class="form-label">部署人员 <span class="required">*</span></label><input type="text" class="form-input" placeholder="请输入项目部署或更新人员" required></div>
<div class="form-group"><label class="form-label">下单时间 <span class="required">*</span></label><input type="datetime-local" class="form-input" required></div>
<div class="form-group"><label class="form-label">服务器密码 <span class="required">*</span></label><input type="password" class="form-input" placeholder="请输入服务器密码" required></div>
<div class="form-group"><label class="form-label">项目概述 <span class="required">*</span></label><textarea class="form-input" placeholder="请输入项目概述" required></textarea></div>
<button type="submit" class="btn-save">💾 保存</button>
</form>
</div>
<div class="step-card">
<div class="step-number">3</div>
<div class="step-title">填写并提交部署视图</div>
<div class="step-actions"><button class="btn-action btn-outline" onclick="handleDownload('deploy-view')">📥 下载部署视图</button></div>
<div class="upload-area" onclick="document.getElementById('deploy-file').click()">
<div class="upload-icon">📄</div>
<div class="upload-text">点击上传已填写的部署视图</div>
<div class="upload-hint">支持 .xlsx / .xls / .pdf 格式</div>
</div>
<input type="file" id="deploy-file" style="display:none" accept=".xlsx,.xls,.pdf" onchange="handleDeployUpload(this)">
<div class="step-desc" style="margin-top: 12px;">下载部署视图模板,填写后上传提交。</div>
</div>
<div class="step-card">
<div class="step-number">4</div>
<div class="step-title">填写并提交自检文档</div>
<div class="step-actions"><button class="btn-action btn-outline" onclick="handleDownload('self-check')">📥 下载自检文档</button></div>
<div class="upload-area" onclick="document.getElementById('check-file').click()">
<div class="upload-icon">📋</div>
<div class="upload-text">点击上传已填写的自检文档</div>
<div class="upload-hint">支持 .docx / .doc / .pdf 格式</div>
</div>
<input type="file" id="check-file" style="display:none" accept=".docx,.doc,.pdf" onchange="handleCheckUpload(this)">
<div class="step-desc" style="margin-top: 12px;">下载自检文档模板,填写后上传提交。</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function handleDownload(type) { alert("下载功能开发中,敬请期待"); }
function handleAuthUpload(input) { if (input.files.length > 0) alert("授权文件上传功能开发中,敬请期待"); }
function handleDeployUpload(input) { if (input.files.length > 0) alert("部署视图上传功能开发中,敬请期待"); }
function handleCheckUpload(input) { if (input.files.length > 0) alert("自检文档上传功能开发中,敬请期待"); }
function handleSaveProject(e) { e.preventDefault(); alert("项目信息保存功能开发中,敬请期待"); }
</script>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5">
<title>{% block title %}服务管理{% endblock %} - 运行维护平台</title>
<style>
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-light: #dbeafe;
--green: #16a34a;
--green-light: #dcfce7;
--yellow: #d97706;
--yellow-light: #fef3c7;
--red: #dc2626;
--red-light: #fee2e2;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Microsoft YaHei", sans-serif;
background: #f3f4f6;
min-height: 100vh;
}
/* ===== 侧边栏 ===== */
.sidebar {
position: fixed;
left: 0;
top: 0;
width: 200px;
height: 100vh;
background: linear-gradient(180deg, #1e293b 0%, #334155 100%);
z-index: 100;
transition: transform 0.3s ease;
}
.sidebar-brand {
color: #fff;
font-size: 18px;
font-weight: 700;
padding: 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-nav {
padding: 16px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
color: #94a3b8;
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
}
.nav-item:hover {
background: rgba(255,255,255,0.05);
color: #fff;
}
.nav-item.active {
background: var(--primary);
color: #fff;
}
.nav-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
/* ===== 顶部条 ===== */
.topbar {
position: fixed;
left: 200px;
top: 0;
right: 0;
height: 56px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 99;
}
.topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.hamburger {
display: none;
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.35);
color: #fff;
font-size: 18px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
}
.topbar-home {
background: rgba(255,255,255,0.2);
color: #fff;
border: 1px solid rgba(255,255,255,0.35);
padding: 6px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
}
.topbar-home:hover {
background: rgba(255,255,255,0.3);
}
.topbar-breadcrumb {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.topbar-breadcrumb a {
color: rgba(255,255,255,0.7);
text-decoration: none;
}
.topbar-breadcrumb a:hover {
color: #fff;
}
.topbar-breadcrumb .current {
color: #fff;
}
.topbar-user {
color: #fff;
display: flex;
align-items: center;
gap: 14px;
font-size: 14px;
}
.btn-logout {
background: rgba(255,255,255,0.15);
color: #fff;
border: 1px solid rgba(255,255,255,0.3);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.btn-logout:hover {
background: rgba(255,255,255,0.25);
}
/* ===== 主内容区 ===== */
.main-content {
margin-left: 200px;
padding: 84px 24px 28px;
min-height: 100vh;
}
/* ===== 通用组件 ===== */
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: var(--gray-900);
display: flex;
align-items: center;
gap: 8px;
}
.footer {
text-align: center;
padding: 20px;
color: var(--gray-500);
font-size: 12px;
}
/* ===== 移动端响应式 ===== */
@media screen and (max-width: 768px) {
.sidebar {
transform: translateX(-200px);
}
.sidebar.open {
transform: translateX(0);
}
.topbar {
left: 0;
}
.main-content {
margin-left: 0;
padding: 84px 14px 28px;
}
.hamburger {
display: block;
}
.topbar-home {
display: none;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 侧边栏 -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">🛠️ 服务管理</div>
<nav class="sidebar-nav">
<a href="/service-manage/authorization" class="nav-item {{ 'active' if active_menu == 'authorization' }}">
<span class="nav-icon">🔑</span>
<span>服务授权</span>
</a>
<a href="/service-manage/upgrade" class="nav-item {{ 'active' if active_menu == 'upgrade' }}">
<span class="nav-icon">⬆️</span>
<span>服务升级</span>
</a>
<a href="/service-manage/info" class="nav-item {{ 'active' if active_menu == 'info' }}">
<span class="nav-icon">ℹ️</span>
<span>服务信息</span>
</a>
</nav>
</aside>
<!-- 顶部条 -->
<header class="topbar">
<div class="topbar-left">
<button class="hamburger" id="hamburger" onclick="toggleSidebar()"></button>
<a href="/" class="topbar-home">🏠 返回首页</a>
<span class="topbar-breadcrumb">
<a href="/service-manage">服务管理</a>
<span> / </span>
<span class="current">{% block breadcrumb %}{% endblock %}</span>
</span>
</div>
<div class="topbar-user">
<span>{{ user.username }}(管理员)</span>
<button class="btn-logout" onclick="handleLogout()">🚪 退出</button>
</div>
</header>
<!-- 主内容区 -->
<main class="main-content">
{% block content %}{% endblock %}
</main>
<div class="footer">运行维护平台 · 服务管理模块</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
}
// 点击遮罩关闭侧边栏
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('sidebar');
const hamburger = document.getElementById('hamburger');
if (sidebar.classList.contains('open') &&
!sidebar.contains(e.target) &&
!hamburger.contains(e.target)) {
sidebar.classList.remove('open');
}
});
async function handleLogout() {
try {
await fetch('/logout', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include'
});
} catch (e) {}
window.location.href = '/login';
}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
{% extends "service_manage/base.html" %}
{% block title %}服务信息{% endblock %}
{% block breadcrumb %}服务信息{% endblock %}
{% block extra_css %}
<style>
.filter-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
.filter-label { font-size: 14px; font-weight: 600; color: var(--gray-700); }
.filter-select { padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 8px; font-size: 14px; color: var(--gray-900); background: #fff; min-width: 160px; cursor: pointer; }
.filter-select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); }
.info-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
.info-card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid var(--gray-200); }
.info-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.info-card-title { font-size: 16px; font-weight: 700; color: var(--gray-900); display: flex; align-items: center; gap: 8px; }
.info-card-value { font-size: 36px; font-weight: 700; margin-bottom: 8px; }
.info-card-unit { font-size: 14px; font-weight: 400; color: var(--gray-500); }
.progress-bar { width: 100%; height: 8px; background: var(--gray-200); border-radius: 4px; overflow: hidden; margin-top: 8px; }
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
.progress-green { background: var(--green); }
.progress-yellow { background: var(--yellow); }
.progress-red { background: var(--red); }
.info-card-detail { font-size: 13px; color: var(--gray-500); margin-top: 8px; }
.status-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.badge-normal { background: var(--green-light); color: var(--green); }
.badge-warning { background: var(--yellow-light); color: var(--yellow); }
.badge-danger { background: var(--red-light); color: var(--red); }
@media screen and (max-width: 768px) { .info-cards { grid-template-columns: 1fr; } }
</style>
{% endblock %}
{% block content %}
<div class="section-head"><h2 class="section-title">ℹ️ 服务信息</h2></div>
<div class="filter-bar">
<label class="filter-label">监控类型:</label>
<select class="filter-select" onchange="handleFilter(this.value)">
<option value="system">系统</option>
<option value="docker">Docker容器</option>
<option value="nginx">Nginx</option>
<option value="mysql">MySQL</option>
<option value="redis">Redis</option>
</select>
</div>
<div class="info-cards">
<div class="info-card">
<div class="info-card-header">
<div class="info-card-title">🖥️ CPU 使用率</div>
<span class="status-badge badge-normal">正常</span>
</div>
<div class="info-card-value" style="color: var(--green);">32.5<span class="info-card-unit">%</span></div>
<div class="progress-bar"><div class="progress-fill progress-green" style="width: 32.5%;"></div></div>
<div class="info-card-detail">核心数: 8 | 负载: 2.1, 1.8, 1.6</div>
</div>
<div class="info-card">
<div class="info-card-header">
<div class="info-card-title">💾 内存使用率</div>
<span class="status-badge badge-warning">注意</span>
</div>
<div class="info-card-value" style="color: var(--yellow);">68.3<span class="info-card-unit">%</span></div>
<div class="progress-bar"><div class="progress-fill progress-yellow" style="width: 68.3%;"></div></div>
<div class="info-card-detail">已用: 10.9 GB / 总计: 16 GB</div>
</div>
<div class="info-card">
<div class="info-card-header">
<div class="info-card-title">💿 硬盘使用率</div>
<span class="status-badge badge-normal">正常</span>
</div>
<div class="info-card-value" style="color: var(--green);">45.2<span class="info-card-unit">%</span></div>
<div class="progress-bar"><div class="progress-fill progress-green" style="width: 45.2%;"></div></div>
<div class="info-card-detail">已用: 90.4 GB / 总计: 200 GB</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function handleFilter(type) {
alert("筛选功能开发中,敬请期待 - 类型: " + type);
}
</script>
{% endblock %}
\ No newline at end of file
{% extends "service_manage/base.html" %}
{% block title %}服务升级{% endblock %}
{% block breadcrumb %}服务升级{% endblock %}
{% block extra_css %}
<style>
.tabs { display: flex; gap: 0; border-bottom: 2px solid var(--gray-200); margin-bottom: 20px; }
.tab { padding: 12px 24px; font-size: 14px; font-weight: 600; color: var(--gray-500); cursor: pointer; border: none; background: none; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; }
.tab:hover { color: var(--gray-700); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab-content { display: none; }
.tab-content.active { display: block; }
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; align-items: center; }
.btn-toolbar { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; border: none; transition: all 0.2s; }
.btn-blue { background: var(--primary); color: #fff; }
.btn-blue:hover { background: var(--primary-hover); }
.btn-blue:disabled { background: var(--gray-300); color: var(--gray-500); cursor: not-allowed; }
.btn-green { background: var(--green); color: #fff; }
.btn-green:hover { background: #15803d; }
.btn-gray { background: #fff; color: var(--gray-700); border: 1px solid var(--gray-300); }
.btn-gray:hover { background: var(--gray-50); }
.service-table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.service-table thead { background: var(--gray-50); }
.service-table th { padding: 12px 16px; font-size: 13px; font-weight: 600; color: var(--gray-700); text-align: left; border-bottom: 1px solid var(--gray-200); }
.service-table td { padding: 12px 16px; font-size: 14px; color: var(--gray-900); border-bottom: 1px solid var(--gray-100); }
.service-table tr:hover { background: var(--gray-50); }
.service-table input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; }
.service-type { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
.type-python { background: #e0f2fe; color: #0369a1; }
.type-java { background: #fef3c7; color: #92400e; }
.type-docker { background: #dbeafe; color: #1d4ed8; }
.type-nginx { background: #dcfce7; color: #166534; }
.btn-row { padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; border: none; transition: all 0.2s; }
.btn-restart { background: var(--yellow-light); color: var(--yellow); }
.btn-restart:hover { background: var(--yellow); color: #fff; }
.btn-log { background: var(--gray-100); color: var(--gray-600); }
.btn-log:hover { background: var(--gray-200); }
.path-text { font-family: monospace; font-size: 12px; color: var(--gray-600); word-break: break-all; }
@media screen and (max-width: 768px) {
.toolbar { gap: 8px; }
.btn-toolbar { padding: 8px 12px; font-size: 12px; }
.service-table { font-size: 13px; }
.service-table th, .service-table td { padding: 8px 10px; }
}
</style>
{% endblock %}
{% block content %}
<div class="section-head"><h2 class="section-title">⬆️ 服务升级</h2></div>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" onclick="switchTab(this, "restart")">服务重启</button>
<button class="tab" onclick="switchTab(this, "rollback")">服务回滚</button>
</div>
<!-- Tab: 服务重启 -->
<div class="tab-content active" id="tab-restart">
<div class="toolbar">
<button class="btn-toolbar btn-gray" onclick="handleAction("download-config")">📥 下载配置</button>
<button class="btn-toolbar btn-gray" onclick="handleAction("backup-db")">💾 备份数据库文件</button>
<button class="btn-toolbar btn-blue" onclick="handleAction("upload-update")">📤 上传更新服务</button>
<button class="btn-toolbar btn-green" id="btn-restart-selected" disabled onclick="handleAction("restart-selected")">🔄 重启已选服务</button>
<button class="btn-toolbar btn-gray" id="btn-download-logs" disabled onclick="handleAction("download-logs")">📋 下载日志</button>
</div>
<table class="service-table">
<thead>
<tr>
<th><input type="checkbox" id="select-all" onchange="toggleSelectAll(this)"></th>
<th>服务名称</th>
<th>服务类型</th>
<th>升级路径</th>
<th>重启脚本路径</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="row-check" onchange="updateBatchButtons()"></td>
<td>运维系统</td>
<td><span class="service-type type-python">PYTHON</span></td>
<td><span class="path-text">/data/services/api/python-cmdb/cmdb</span></td>
<td><span class="path-text">/var/www/html/start.sh</span></td>
<td>
<button class="btn-row btn-restart" onclick="handleAction("restart", "运维系统")">重启</button>
<button class="btn-row btn-log" onclick="handleAction("log", "运维系统")">下载日志</button>
</td>
</tr>
<tr>
<td><input type="checkbox" class="row-check" onchange="updateBatchButtons()"></td>
<td>预定系统2.0</td>
<td><span class="service-type type-java">JAVA</span></td>
<td><span class="path-text">/data/services/api/java-meeting/java-meeting2.0/ubains-meeting-inner-api-1.0-SNAPSHOT.jar</span></td>
<td><span class="path-text">/var/www/java/api/java-meeting/java-meeting2.0/run.sh</span></td>
<td>
<button class="btn-row btn-restart" onclick="handleAction("restart", "预定系统2.0")">重启</button>
<button class="btn-row btn-log" onclick="handleAction("log", "预定系统2.0")">下载日志</button>
</td>
</tr>
<tr>
<td><input type="checkbox" class="row-check" onchange="updateBatchButtons()"></td>
<td>消息调度服务</td>
<td><span class="service-type type-java">JAVA</span></td>
<td><span class="path-text">/data/services/api/java-meeting/java-message-scheduling/ubains-meeting-message-scheduling-1.0-SNAPSHOT.jar</span></td>
<td><span class="path-text">/var/www/java/api/java-meeting/java-message-scheduling/run.sh</span></td>
<td>
<button class="btn-row btn-restart" onclick="handleAction("restart", "消息调度服务")">重启</button>
<button class="btn-row btn-log" onclick="handleAction("log", "消息调度服务")">下载日志</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Tab: 服务回滚 -->
<div class="tab-content" id="tab-rollback">
<div class="toolbar">
<button class="btn-toolbar btn-gray" onclick="handleAction("download-config")">📥 下载配置</button>
<button class="btn-toolbar btn-blue" id="btn-rollback-selected" disabled onclick="handleAction("rollback-selected")">⏪ 回滚已选服务</button>
</div>
<table class="service-table">
<thead>
<tr>
<th><input type="checkbox" id="select-all-rollback" onchange="toggleSelectAll(this)"></th>
<th>服务名称</th>
<th>服务类型</th>
<th>当前版本</th>
<th>回滚版本</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="row-check-rollback" onchange="updateBatchButtons()"></td>
<td>运维系统</td>
<td><span class="service-type type-python">PYTHON</span></td>
<td>v2.3.1</td>
<td>v2.3.0</td>
<td><button class="btn-row btn-restart" onclick="handleAction("rollback", "运维系统")">回滚</button></td>
</tr>
<tr>
<td><input type="checkbox" class="row-check-rollback" onchange="updateBatchButtons()"></td>
<td>预定系统2.0</td>
<td><span class="service-type type-java">JAVA</span></td>
<td>v1.5.2</td>
<td>v1.5.1</td>
<td><button class="btn-row btn-restart" onclick="handleAction("rollback", "预定系统2.0")">回滚</button></td>
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block extra_js %}
<script>
function switchTab(btn, tabId) {
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active"));
btn.classList.add("active");
document.getElementById("tab-" + tabId).classList.add("active");
updateBatchButtons();
}
function toggleSelectAll(checkbox) {
const checks = checkbox.closest("table").querySelectorAll("input[type=checkbox]:not(#" + checkbox.id + ")");
checks.forEach(c => c.checked = checkbox.checked);
updateBatchButtons();
}
function updateBatchButtons() {
const checked = document.querySelectorAll(".row-check:checked, .row-check-rollback:checked");
const restartBtn = document.getElementById("btn-restart-selected");
const logsBtn = document.getElementById("btn-download-logs");
const rollbackBtn = document.getElementById("btn-rollback-selected");
if (restartBtn) restartBtn.disabled = checked.length === 0;
if (logsBtn) logsBtn.disabled = checked.length === 0;
if (rollbackBtn) rollbackBtn.disabled = checked.length === 0;
}
function handleAction(action, serviceName) {
alert(action + " 功能开发中,敬请期待" + (serviceName ? " - " + serviceName : ""));
}
</script>
{% endblock %}
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论