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

feat(skills): 升级集群监控与部署技能,新增钉钉通知功能

- ARM/X86 集群监控:增强中间件检查(Redis/EMQX/Nacos/FastDFS/达梦),优化报告生成器
- X86-QLV10-XTYBS:优化 SSH 辅助工具,更新部署流程文档
- X86-TX-XTYBS:重构完整部署脚本,修复编码问题
- 新增钉钉通知模块,支持集群监控报告自动推送
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 0b20b9cf
......@@ -45,6 +45,10 @@ description: ARM集群夜间监控 - 监测9网段四台ARM架构服务器集群
- 预期产出
- **必须等待用户确认计划后方可开始执行**,不得跳过确认步骤直接操作;
- 计划执行文档保存到 `.claude/skills/ARM-CLUSTER-MONITOR/计划执行_{YYYYMMDD_HHMMSS}.md`
- **定时执行模式例外**:定时任务触发执行时,跳过计划确认步骤,直接执行监控流程。
- **需求文档规范**:每次需求变动或问题处理,需在 `Docs/PRD/新统一平台集群监测/` 目录下输出:
- `_PRD_需求文档.md` — 需求文档
- `_PRD_需求文档_计划执行.md` — 对应的计划执行文档
### 代码实现规范
- **代码存放位置**:如需代码实现(如Python脚本、工具类等),统一存放在 `.claude/skills/ARM-CLUSTER-MONITOR/code/` 目录下;
......@@ -640,6 +644,89 @@ def connect_to_server(server_ip, username, password=None, key_dir=None):
11. **中文输出**:所有回复、代码注释、Git提交信息使用简体中文
12. **计划先行**:需求变更必须输出计划执行文档,确认后方可执行
13. **paramiko连接**:默认使用Python paramiko库,不依赖系统ssh命令
14. **钉钉通知**:仅在08:00-09:00时段发送夜间汇总通知,避免重复发送
## 钉钉通知配置
### 通知策略
**延迟通知机制**
- 定时任务期间(22:00-07:59):执行监控但不发送钉钉通知
- 08:00-09:00时段:执行监控后自动汇总分析并发送一次通知
- 防重复发送:同一日期仅发送一次汇总通知
### 配置文件
`code/dingtalk_config.json` — 钉钉机器人配置
```json
{
"enabled": true,
"webhook": "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN",
"secret": "SECYOUR_SECRET",
"notification_mode": "summary",
"trigger_conditions": {
"always": true,
"on_warning": false,
"on_critical": false
}
}
```
### 配置项说明
| 配置项 | 类型 | 说明 |
|-------|------|------|
| `enabled` | bool | 是否启用钉钉通知 |
| `webhook` | string | 钉钉机器人Webhook地址 |
| `secret` | string | 签名密钥(SEC开头) |
| `notification_mode` | string | 通知模式:`summary`(仅摘要) |
| `trigger_conditions.always` | bool | 每次监控都发送通知 |
| `trigger_conditions.on_warning` | bool | 仅在发生警告时发送 |
| `trigger_conditions.on_critical` | bool | 仅在发生严重告警时发送 |
### 集成位置
- 通知模块:`code/dingtalk_notifier.py`
- 调用位置:`monitor_agent.py``run_full_monitor()` → 第6.5步
- 通知发送失败不影响监控主流程
### 消息格式
```markdown
### ARM集群夜间监控汇总报告
**日期**: 20260710
**时段**: 2026-07-10 22:00 ~ 2026-07-11 08:00
**执行**: 11次
**状态**: [OK] normal
**评分**: 95 -> 92
#### 资源趋势
| 节点 | CPU峰值 | 内存峰值 | 磁盘变化 |
|:---|:---|:---|:---|
| 192.168.9.89 | 52% | 78% | +1% |
| ...
**累计告警**: 3次
#### 建议
- 192.168.9.91 磁盘增长+2%
```
### 相关文件
| 文件 | 说明 |
|------|------|
| `code/dingtalk_notifier.py` | 钉钉通知发送模块 |
| `code/dingtalk_config.json` | 钉钉机器人配置 |
| `code/report_analyzer.py` | 夜间报告汇总分析器 |
| `code/sent_records.json` | 防重复发送记录 |
### 手动触发汇总
```bash
python main.py 汇总 # 手动执行汇总分析并发送通知
```
## 关联文档
......
{
"enabled": true,
"webhook": "https://oapi.dingtalk.com/robot/send?access_token=27071a77f20da381e9a321653ec5f4dcf668bcf058c01162f28e3f1f8633386d",
"secret": "SEC5d85d5735a1805ada1be84929d5b37f5b72a2a832a6bcd9a1ca5615e5799be38",
"notification_mode": "summary",
"trigger_conditions": {
"always": true,
"on_warning": false,
"on_critical": false
}
}
......@@ -6,14 +6,15 @@
1. 命令行参数解析
2. 定时任务启动逻辑
3. 监控执行入口
4. 错误处理
4. 汇总分析与钉钉通知(08:00后执行)
支持命令:
- python main.py # 立即执行一次集群监控
- python main.py 定时 # 启动定时监控(22:00-09:00每2h)
- python main.py 定时状态 # 查看定时任务状态
- python main.py 定时停止 # 停止定时监控任务
- python main.py 配置免密 # 为四台服务器配置SSH免密登录
- python main.py 定时 # 启动定时监控(22:00-08:00每1h)
- python main.py 定时状态 # 查看定时任务状态
- python main.py 定时停止 # 停止定时监控任务
- python main.py 配置免密 # 为四台服务器配置SSH免密登录
- python main.py 汇总 # 执行汇总分析并发送钉钉通知
作者:Claude Code
创建时间:2026-07-09
......@@ -21,25 +22,35 @@
import sys
import os
import json
from datetime import datetime
from pathlib import Path
# 导入监控调度器
from monitor_agent import MonitorAgent
# 导入汇总分析器
try:
from report_analyzer import NightlyReportAnalyzer
from dingtalk_notifier import send_summary_notification
SUMMARY_AVAILABLE = True
except ImportError:
SUMMARY_AVAILABLE = False
print("[警告] 汇总分析模块未找到")
# 防重复发送记录文件
SENT_RECORDS_FILE = Path(__file__).parent / "sent_records.json"
def main():
"""
主入口函数
"""
# 解析命令行参数
args = sys.argv[1:] if len(sys.argv) > 1 else []
# 根据参数执行对应功能
if len(args) == 0:
# 默认:立即执行一次集群监控
run_immediate_monitor()
elif args[0] == '定时':
# 启动定时监控
if len(args) > 1 and args[1] == '状态':
show_schedule_status()
elif len(args) > 1 and args[1] == '停止':
......@@ -47,31 +58,156 @@ def main():
else:
start_schedule()
elif args[0] == '配置免密':
# 配置SSH免密登录
configure_ssh_keys()
elif args[0] == '汇总':
run_summary_and_notify()
else:
# 未知命令
print(f"[错误] 未知命令: {args[0]}")
print_usage()
def should_send_summary() -> bool:
"""
判断是否应该发送汇总通知
规则:
1. 当前时间在 08:00-09:00 之间
2. 今日未发送过
Returns:
是否应该发送
"""
now = datetime.now()
current_hour = now.hour
# 时间判断:08:00-09:00
if current_hour < 8 or current_hour >= 9:
return False
# 检查是否已发送
today = now.strftime('%Y%m%d')
if SENT_RECORDS_FILE.exists():
try:
with open(SENT_RECORDS_FILE, 'r', encoding='utf-8') as f:
records = json.load(f)
if records.get(today) == 'sent':
print(f"[信息] 今日({today})已发送汇总通知,跳过")
return False
except Exception:
pass
return True
def mark_summary_sent():
"""
标记今日已发送汇总通知
"""
today = datetime.now().strftime('%Y%m%d')
records = {}
if SENT_RECORDS_FILE.exists():
try:
with open(SENT_RECORDS_FILE, 'r', encoding='utf-8') as f:
records = json.load(f)
except Exception:
pass
records[today] = 'sent'
try:
with open(SENT_RECORDS_FILE, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"[警告] 记录发送状态失败: {e}")
def run_summary_and_notify():
"""
执行汇总分析并发送钉钉通知
"""
print("=" * 60)
print("ARM集群夜间监控 - 汇总分析模式")
print("=" * 60)
if not SUMMARY_AVAILABLE:
print("[错误] 汇总分析模块不可用")
return False
# 检查是否应该发送
if not should_send_summary():
print("[跳过] 当前时段不需要发送汇总通知")
return False
try:
# 执行汇总分析
analyzer = NightlyReportAnalyzer()
print("\n[步骤1] 分析夜间监控报告...")
if not analyzer.analyze_all_reports():
print("[失败] 汇总分析失败")
return False
# 生成汇总报告
print("\n[步骤2] 生成汇总报告...")
report_path = analyzer.save_summary_report()
if not report_path:
print("[失败] 汇总报告保存失败")
return False
# 生成钉钉摘要
print("\n[步骤3] 发送钉钉通知...")
summary_text = analyzer.get_summary_for_dingtalk()
# 发送通知
result = send_summary_notification(summary_text)
if result:
print("[成功] 钉钉通知发送成功")
# 标记已发送
mark_summary_sent()
print(f"\n汇总报告路径: {report_path}")
return True
else:
print("[失败] 钉钉通知发送失败")
return False
except Exception as e:
print(f"\n[错误] 汇总分析异常: {str(e)}")
handle_error(e)
return False
def run_immediate_monitor():
"""
立即执行一次集群监控
如果当前时间在 08:00-09:00 之间,执行监控后自动触发汇总分析并发送通知
"""
print("=" * 60)
print("ARM集群夜间监控 - 立即执行模式")
print("=" * 60)
print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
now = datetime.now()
print(f"执行时间: {now.strftime('%Y-%m-%d %H:%M:%S')}")
# 判断是否需要发送通知
should_notify = should_send_summary()
if should_notify:
print("[信息] 当前时段为08:00-09:00,监控完成后将发送汇总通知")
try:
# 创建监控调度器
agent = MonitorAgent()
# 执行完整监控流程
summary = agent.run_full_monitor()
# 执行监控(仅在should_notify=True时发送通知)
summary = agent.run_full_monitor(send_notification=should_notify)
# 输出执行结果
if summary.get('status') == 'success':
print("\n" + "=" * 60)
print("监控执行成功")
......@@ -83,6 +219,11 @@ def run_immediate_monitor():
print(f"告警数: {summary.get('alert_count', 0)}")
print(f"报告路径: {summary.get('report_path', '')}")
print("=" * 60)
# 如果在08:00-09:00时段,执行汇总分析
if should_notify and SUMMARY_AVAILABLE:
print("\n[信息] 执行汇总分析...")
run_summary_and_notify()
else:
print("\n" + "=" * 60)
print("监控执行失败")
......
......@@ -37,6 +37,14 @@ from log_analyzer import LogAnalyzer
from cluster_consistency import ClusterConsistency
from report_generator import ReportGenerator
# 导入钉钉通知模块
try:
from dingtalk_notifier import send_cluster_notification
DINGTALK_AVAILABLE = True
except ImportError:
DINGTALK_AVAILABLE = False
print("[警告] 钉钉通知模块未找到,通知功能将不可用")
class MonitorAgent:
"""主监控调度器类"""
......@@ -350,10 +358,13 @@ class MonitorAgent:
return report_path
def run_full_monitor(self) -> Dict:
def run_full_monitor(self, send_notification: bool = False) -> Dict:
"""
执行完整监控流程
Args:
send_notification: 是否发送钉钉通知,默认False(定时任务期间不发送)
Returns:
Dict: 完整的监控结果摘要(压缩格式)
"""
......@@ -392,6 +403,10 @@ class MonitorAgent:
# 6. 生成并保存报告
report_path = self.generate_and_save_report()
# 6.5. 发送钉钉通知(仅当参数允许时)
if send_notification:
self._send_dingtalk_notification()
# 7. 断开所有SSH连接
self.close_all_connections()
......@@ -534,7 +549,7 @@ class MonitorAgent:
name = node_data.get('name', ip)
overall = node_data.get('overall_status', 'unknown')
status_icon = '✅' if overall == 'normal' else ('⚠️' if overall == 'warning' else '❌')
status_icon = '[OK]' if overall == 'normal' else ('[WARN]' if overall == 'warning' else '[ERR]')
print(f"\n[完成] {status_icon} {name} ({ip}): {overall}")
......@@ -563,6 +578,36 @@ class MonitorAgent:
'report_path': '', # 报告生成后填充
}
def _send_dingtalk_notification(self):
"""
发送钉钉通知
如果钉钉通知模块可用且配置已启用,则在报告生成后发送通知。
定时任务执行模式下,通知发送不阻塞主流程。
"""
if not DINGTALK_AVAILABLE:
return
try:
print("\n[信息] 发送钉钉通知...")
# 获取配置中的通知设置
config = self.config_parser.config if hasattr(self.config_parser, 'config') else {}
send_cluster_notification(
monitor_time=self.monitor_time.strftime('%Y-%m-%d %H:%M:%S'),
node_count=len(self.servers),
overall_status=self.cluster_data.get('overall_status', 'normal'),
health_score=self.cluster_data.get('consistency', {}).get('health_score', {}).get('total_score', 0),
alert_count=len(self.cluster_data.get('alerts', [])),
servers_data=self.servers_data,
cluster_data=self.cluster_data
)
except Exception as e:
print(f"[警告] 钉钉通知发送失败: {str(e)}")
print("[信息] 通知发送失败不影响监控流程,继续执行")
def test_monitor_agent():
"""测试主监控调度器功能"""
......
......@@ -760,7 +760,7 @@ class ReportGenerator:
role = server.get('role', '')
overall = server.get('overall_status', 'unknown')
status_icon = '✅' if overall == 'normal' else ('⚠️' if overall == 'warning' else '❌')
status_icon = '[OK]' if overall == 'normal' else ('[WARN]' if overall == 'warning' else '[ERR]')
print(f" {status_icon} {name} ({ip}): {role} - {overall}")
# 告警摘要
......@@ -769,13 +769,13 @@ class ReportGenerator:
if alerts:
for alert in alerts[:10]: # 只显示前10条
level = alert.get('level', 'info')
level_icon = '🔴' if level == 'critical' else ('🟡' if level == 'warning' else '🔵')
level_icon = '[CRIT]' if level == 'critical' else ('[WARN]' if level == 'warning' else '[INFO]')
print(f" {level_icon} [{level}] {alert.get('message', '')}")
if len(alerts) > 10:
print(f" ... 还有 {len(alerts) - 10} 条告警")
else:
print(" 无告警")
print(" [OK] 无告警")
# 健康评分
consistency = cluster_data.get('consistency', {})
......@@ -788,7 +788,7 @@ class ReportGenerator:
# 报告保存路径
if self.report_path:
print(f"\n--- 报告文件 ---")
print(f" 📄 {self.report_path}")
print(f" {self.report_path}")
print("\n" + "=" * 60)
......
......@@ -169,6 +169,9 @@ class SystemMonitor:
return memory_info
# 不监控的挂载点列表(非关键或临时挂载)
EXCLUDED_MOUNTS = ['/cdrom', '/snap', '/mnt/cdrom', '/media', '/run/user']
def check_disk_usage(self) -> Dict:
"""
检查磁盘使用率
......@@ -206,6 +209,10 @@ class SystemMonitor:
usage_percent = int(parts[4].replace('%', ''))
mount_point = parts[5]
# 排除非关键挂载点
if any(excluded in mount_point for excluded in self.EXCLUDED_MOUNTS):
continue
partition_info = {
'filesystem': filesystem,
'size': size,
......@@ -271,6 +278,10 @@ class SystemMonitor:
usage_percent = int(parts[4].replace('%', ''))
mount_point = parts[5]
# 排除非关键挂载点
if any(excluded in mount_point for excluded in self.EXCLUDED_MOUNTS):
continue
partition_info = {
'filesystem': filesystem,
'total_inodes': total_inodes,
......
......@@ -74,6 +74,24 @@ schedule_interval: "2h"
- 计划执行文档需包含:执行目标、执行步骤、预期结果、风险评估
- 用户确认后方可执行
### 0.6 定时任务执行规范(强制要求)
> ⚠️ 定时任务执行模式下,必须遵守以下规范:
1. **跳过计划确认步骤**
- 定时任务触发时,**跳过** `0.4 需求文档处理规范` 中的"输出《计划执行》文档"和"用户确认后方可执行"步骤
- 直接使用 `python main.py` 执行集群监控,无需任何交互确认
2. **判断依据**
- 定时任务由 Cron 触发,prompt 为 `/X86-Cluster-Monitor`
- 此时无需输出计划执行文档,直接运行代码完成监控即可
3. **执行原则**
- 零交互:不调用 `AskUserQuestion`,不等待用户确认
- 快速完成:直接运行 Python 脚本,输出摘要后结束
- 静默运行:减少不必要的输出,仅保留关键摘要信息
- 异常处理:连接失败自动 fallback,凭据从 `config.json` 读取,不会因缺凭据而阻塞
**计划执行文档模板**:
```markdown
# 计划执行文档
......@@ -550,7 +568,98 @@ AuxiliaryTool/ScriptTool/ClusterMonitor/reports/
---
## 9. 异常处理
## 9. 钉钉通知功能
### 9.1 功能说明
监控完成后自动发送**日报汇总**到钉钉群聊,便于及时发现异常。
**重要**:为节约公司资源,钉钉通知**仅在最后一次定时任务(08:00)执行后发送一次**,并附带整夜健康度趋势分析。
### 9.2 发送逻辑
```
定时任务执行时间: 22:00, 00:00, 02:00, 04:00, 06:00, 08:00
┌─────────────────────────────────────────────────────────────┐
│ 钉钉通知发送策略 │
├─────────────────────────────────────────────────────────────┤
│ 22:00 执行监控 → 生成报告 → 不发送钉钉 │
│ 00:00 执行监控 → 生成报告 → 不发送钉钉 │
│ 02:00 执行监控 → 生成报告 → 不发送钉钉 │
│ 04:00 执行监控 → 生成报告 → 不发送钉钉 │
│ 06:00 执行监控 → 生成报告 → 不发送钉钉 │
│ 08:00 执行监控 → 生成报告 → 收集本周期所有报告 │
│ → 生成健康度趋势分析 │
│ → 发送一次汇总通知(带防重发机制) │
└─────────────────────────────────────────────────────────────┘
```
**防重发机制**
- 发送成功后创建标记文件 `.dingtalk_sent_{YYYYMMDD}`
- 当日再次触发时检测到标记文件则跳过发送
- 确保每天只发送一次
### 9.3 配置方法
`config.json` 中配置钉钉参数:
```json
{
"dingtalk": {
"enabled": true,
"webhook": "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN",
"secret": "SECYOUR_SECRET"
}
}
```
**配置项说明**:
| 配置项 | 说明 | 必填 |
|--------|------|------|
| `enabled` | 是否启用钉钉通知 | 是,默认false |
| `webhook` | 钉钉机器人Webhook地址 | 是 |
| `secret` | 签名密钥(加签验证) | 是 |
### 9.4 消息格式(日报汇总)
```
### 🖥️ X86集群监控日报 - 新统一平台X86集群
**日期**: 2026-07-11
**最终状态**: 🟢 正常
**本周期报告数**: 6 次
#### 📊 最终指标
| 指标 | 值 |
|:---|:---|
| 集群健康度 | 98.5% |
| 服务器状态 | 🟢3 🟡1 🔴0 |
#### 📈 健康度趋势
22:00: 98.0% → 00:00: 97.5% → 02:00: 98.2% → 04:00: 98.0% → 06:00: 97.8% → 08:00: 98.5%
📊 **趋势**: 状态稳定
#### 📋 服务器汇总
- 🟢 **192.168.5.41** (master): CPU 15%, 内存 45%
- 🟡 **192.168.5.42** (slave1): CPU 25%, 内存 78%
- 🟢 **192.168.5.43** (slave2): CPU 12%, 内存 52%
- 🟢 **192.168.5.40** (dm8-server): CPU 18%, 内存 48%
---
本通知仅发送一次 · 2026-07-11 08:05:00
```
### 9.5 注意事项
1. **每日仅发送一次**:防重发机制确保不浪费公司资源
2. **趋势分析**:收集整夜6次报告的健康度,分析变化趋势
3. 钉钉通知失败不影响监控主流程
4. 需安装 `requests` 库:`pip install requests`
---
## 10. 异常处理
| 异常情况 | 处理方式 |
|---------|---------|
......@@ -574,4 +683,6 @@ AuxiliaryTool/ScriptTool/ClusterMonitor/reports/
| 日期 | 版本 | 更新内容 |
|------|------|---------|
| 2026-07-09 | v1.0 | 初始版本,创建skill骨架 |
\ No newline at end of file
| 2026-07-09 | v1.0 | 初始版本,创建skill骨架 |
| 2026-07-11 | v1.1 | 新增0.6定时任务执行规范,定时模式下跳过计划确认,零交互自动执行 |
| 2026-07-11 | v1.2 | 接入钉钉通知模块,每日仅发送一次(08:00),附带健康度趋势分析 |
\ No newline at end of file
......@@ -231,9 +231,51 @@ class ClusterMonitor:
if check_results:
print_execution_summary(check_results, report_path)
# 发送钉钉通知(仅在最后一次定时任务执行时发送)
self._send_dingtalk_notification_if_final(check_results, report_path)
logger.info(f'监控完成')
return report_path
def _send_dingtalk_notification_if_final(self, check_results, report_path):
"""
发送钉钉通知(仅在最后一次定时任务执行时)
"""
from datetime import datetime
from dingtalk_notifier import send_daily_report
try:
current_hour = datetime.now().hour
is_final_run = (current_hour == 8)
if not is_final_run:
logger.info(f'当前时间 {current_hour}:00 不是最后一次定时任务,跳过钉钉通知')
return
logger.info('检测到最后一次定时任务执行(08:00),准备发送日报...')
# 获取报告目录
report_dir = Path(report_path).parent if report_path else None
if not report_dir:
logger.warning('报告目录不存在')
return
# 调用钉钉通知模块
success = send_daily_report(
check_results=check_results,
cluster_config=self.cluster_config,
report_dir=str(report_dir),
config_path=str(Path(__file__).parent.parent / 'config.json')
)
if success:
logger.info('钉钉日报发送成功')
else:
logger.info('钉钉日报已跳过或发送失败')
except Exception as e:
logger.warning(f'钉钉通知发送异常(不影响监控主流程): {e}')
def main():
"""主函数"""
......
......@@ -22,12 +22,34 @@
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-master", "redis-sentinel", "uemqx", "unacos", "utracker", "paperless"],
"containers": [
"ujava2",
"utengine",
"umysql",
"redis-master",
"redis-sentinel",
"uemqx",
"unacos",
"utracker",
"ustorage",
"paperless"
],
"services": {
"mysql": {"port": 8306, "type": "master"},
"redis": {"port": 6379, "type": "master"},
"emqx": {"port": 1883, "dashboard_port": 18083},
"nacos": {"port": 8848}
"mysql": {
"port": 8306,
"type": "master"
},
"redis": {
"port": 6379,
"type": "master"
},
"emqx": {
"port": 1883,
"dashboard_port": 18083
},
"nacos": {
"port": 8848
}
},
"note": "主节点,承担核心业务"
},
......@@ -43,9 +65,23 @@
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-slave-1", "redis-sentinel-2", "uemqx", "unacos", "utracker", "paperless"],
"containers": [
"ujava2",
"utengine",
"umysql",
"redis-slave-1",
"redis-sentinel-2",
"uemqx",
"unacos",
"utracker",
"ustorage",
"paperless"
],
"services": {
"redis": {"port": 6379, "type": "slave"}
"redis": {
"port": 6379,
"type": "slave"
}
},
"note": "从节点1"
},
......@@ -61,9 +97,23 @@
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-slave-2", "redis-sentinel-3", "uemqx", "unacos", "utracker", "paperless"],
"containers": [
"ujava2",
"utengine",
"umysql",
"redis-slave-2",
"redis-sentinel-3",
"uemqx",
"unacos",
"utracker",
"ustorage",
"paperless"
],
"services": {
"redis": {"port": 6379, "type": "slave"}
"redis": {
"port": 6379,
"type": "slave"
}
},
"note": "从节点2"
},
......@@ -79,9 +129,14 @@
"cpu_cores": 8,
"memory_gb": 8,
"disk_gb": 60,
"containers": ["dm8-server", "utengine"],
"containers": [
"dm8-server",
"utengine"
],
"services": {
"dm8": {"port": 5236}
"dm8": {
"port": 5236
}
},
"note": "达梦数据库存储使用"
}
......@@ -90,25 +145,48 @@
"redis_cluster": {
"mode": "1主2从3哨兵",
"master": "192.168.5.41",
"slaves": ["192.168.5.42", "192.168.5.43"],
"sentinels": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"slaves": [
"192.168.5.42",
"192.168.5.43"
],
"sentinels": [
"192.168.5.41",
"192.168.5.42",
"192.168.5.43"
],
"password": "Ubains@123"
},
"emqx_cluster": {
"mode": "3节点集群",
"nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"nodes": [
"192.168.5.41",
"192.168.5.42",
"192.168.5.43"
],
"dashboard_username": "admin",
"dashboard_password": "public"
},
"nacos_cluster": {
"mode": "3节点集群",
"nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"nodes": [
"192.168.5.41",
"192.168.5.42",
"192.168.5.43"
],
"console_username": "nacos",
"console_password": "nacos"
},
"fastdfs_cluster": {
"tracker_nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"storage_nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"]
"tracker_nodes": [
"192.168.5.41",
"192.168.5.42",
"192.168.5.43"
],
"storage_nodes": [
"192.168.5.41",
"192.168.5.42",
"192.168.5.43"
]
}
},
"middleware_credentials": {
......@@ -130,6 +208,12 @@
"username": "nacos",
"password": "nacos",
"port": 8848
},
"dm8": {
"username": "SYSDBA",
"password": "Dameng2026Pwd",
"port": 5236,
"expired_date": "2027-04-14"
}
},
"thresholds": {
......@@ -149,11 +233,23 @@
"window": "22:00-09:00",
"interval": "2h",
"cron": "0 22,0,2,4,6,8 * * *",
"execution_times": ["22:00", "00:00", "02:00", "04:00", "06:00", "08:00"]
"execution_times": [
"22:00",
"00:00",
"02:00",
"04:00",
"06:00",
"08:00"
]
},
"report": {
"output_dir": "AuxiliaryTool/ScriptTool/ClusterMonitor/reports",
"filename_format": "cluster_monitor_{YYYYMMDD_HHMMSS}.md",
"history_days": 30
},
"dingtalk": {
"enabled": false,
"webhook": "https://oapi.dingtalk.com/robot/send?access_token=27071a77f20da381e9a321653ec5f4dcf668bcf058c01162f28e3f1f8633386d",
"secret": "SEC5d85d5735a1805ada1be84929d5b37f5b72a2a832a6bcd9a1ca5615e5799be38"
}
}
\ No newline at end of file
---
name: X86-QLV10-XTYBS
description: X86麒麟V10新统一平台远程自动化部署,严格按需求文档执行
description: X86麒麟V10新统一平台远程自动化部署,严格按需求文档执行,全程无交互
---
X86架构-麒麟V10服务器新统一平台自动化部署操作。
......@@ -9,6 +9,44 @@ X86架构-麒麟V10服务器新统一平台自动化部署操作。
/X86-QLV10-XTYBS
## 定时任务
**已设置工作日晚上 22:00 自动执行完整部署流程(全程无交互)**
- 定时任务ID: `84dbe6b5`
- 执行时间: 周一至周五 22:00
- 执行内容: 完整自动化流程(部署→授权→等待→验证→生成报告)
- 脚本路径: `.claude/skills/X86-QLV10-XTYBS/code/scheduled_deploy.py`
- 授权脚本: `AuxiliaryTool/ScriptTool/RemoteDeploy/authorize_x86_kylin.py`(Selenium浏览器自动化)
- 日志路径: `AuxiliaryTool/ScriptTool/RemoteDeploy/reports/scheduled_deploy_YYYYMMDD.log`
- 持久化: 已保存到 `.claude/scheduled_tasks.json`
**自动化流程说明**:
1. **部署阶段**: SSH上传部署包 → 解压 → 执行部署脚本(自动应答)
2. **授权阶段**: Selenium自动打开Chrome浏览器 → 登录维护平台 → 下载激活文件 → 上传license.zip → 重启服务
3. **等待阶段**: 等待10分钟让服务完全启动
4. **验证阶段**: curl接口验证 + 日志检查
5. **报告生成**: 输出部署分析报告
**手动触发方式**:
```bash
# 立即执行完整流程(跳过时间检查)
python .claude/skills/X86-QLV10-XTYBS/code/scheduled_deploy.py --force
# 仅部署,跳过授权阶段
python .claude/skills/X86-QLV10-XTYBS/code/scheduled_deploy.py --force --skip-auth
# 仅检查当前是否满足执行条件
python .claude/skills/X86-QLV10-XTYBS/code/scheduled_deploy.py --check
# 手动执行授权(单独测试)
python AuxiliaryTool/ScriptTool/RemoteDeploy/authorize_x86_kylin.py
```
**管理定时任务**:
- 查看所有定时任务: `/cron`
- 取消定时任务: 使用 `CronDelete` 工具删除 ID `84dbe6b5`
## 基本规则
- 所有回复、代码注释、Git提交信息默认使用中文(简体)描述;
......@@ -26,7 +64,7 @@ X86架构-麒麟V10服务器新统一平台自动化部署操作。
| 超管账号 | superadmin / Ubains@1357 |
| 验证码 | csba |
| 授权文件 | E:\自动化部署\X86-5.69\license.zip |
| 部署包源路径 | Z:\发布版本\03服务器部署\15新统一平台\X86部署包\全量版 |
| 部署包位置 | /data/offline_auto_unifiedPlatform.tar.gz(服务器上已存在,跳过上传) |
| 部署操作指导 | Docs/PRD/远程自动化部署/X86架构_新统一平台自动化部署操作指导.md |
| 需求文档 | Docs/PRD/远程自动化部署/_PRD_X86_麒麟V10远程自动化部署_需求文档.md |
| 部署脚本目录 | AuxiliaryTool/ScriptTool/RemoteDeploy/ |
......@@ -204,11 +242,21 @@ X86架构-麒麟V10服务器新统一平台自动化部署操作。
```
.claude/skills/X86-QLV10-XTYBS/code/
├── ssh_manager.py # SSH连接管理模块(paramiko)
├── deploy_runner.py # 部署执行脚本
├── service_checker.py # 服务状态检查脚本
├── auto_deploy_wrapper.sh # 部署包装脚本(自动应答交互)
├── report_generator.py # 报告生成脚本
└── ssh_keys/ # SSH免密配置目录
└── 192.168.5.69/ # 按服务器IP命名
```
\ No newline at end of file
├── ssh_helper.py # SSH连接管理模块(paramiko,密码已硬编码)
├── verify_x86_status.py # 服务验证脚本(容器+API+日志)
├── auto_deploy_wrapper.sh # 部署包装脚本(自动应答交互)
├── scheduled_deploy.py # 定时部署主控脚本(部署→授权→等待→验证全流程)
└── ssh_keys/ # SSH免密配置目录
└── 192.168.5.69/ # 按服务器IP命名
AuxiliaryTool/ScriptTool/RemoteDeploy/
├── full_deploy.py # 核心部署脚本(SSH上传+解压+执行+验证)
├── authorize_x86_kylin.py # X86麒麟V10自动授权脚本(Selenium浏览器自动化)
└── reports/ # 报告输出目录
```
**全程无交互确认说明**:
- SSH连接:密码硬编码于 `ssh_helper.py` (`Ubains@123`)
- 部署脚本:通过 `printf 'y\ny\ny\ny\ny\ny\ny\nn\n'` 自动应答
- 授权操作:Selenium自动化浏览器,账号密码验证码全部硬编码
- 接口验证:curl命令自动重试5次,无需人工判断
\ No newline at end of file
......@@ -10,6 +10,9 @@ import time
import os
import re
# 设置标准输出编码为UTF-8,避免GBK编码错误
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
# 服务器配置
HOST = "192.168.5.69"
PORT = 22
......@@ -46,10 +49,18 @@ def exec_long_cmd(client, cmd, timeout=3600):
line = stdout.readline()
if not line:
break
line = line.strip()
if line:
print(line)
output_lines.append(line)
try:
line = line.strip()
if line:
# 尝试解码并打印,遇到编码错误跳过
try:
print(line)
except UnicodeEncodeError:
# 打印安全的ASCII表示
print(line.encode('ascii', errors='replace').decode('ascii'))
output_lines.append(line)
except Exception:
pass
exit_code = stdout.channel.recv_exit_status()
print(f"[EXIT CODE] {exit_code}")
......
......@@ -392,4 +392,96 @@ context = browser.new_context(ignore_https_errors=True)
1. 优先使用 `page.expect_file_chooser()` 监听方式
2. 备选:找到全局所有file input,逐个尝试`set_input_files()`
3. 最后手段:用JS将隐藏的file input变为可见后再设置文件
4. 上传成功后检查后端日志确认:`tail -f /data/services/api/java-meeting/java-meeting-extapi/logs/ubains-INFO-AND-ERROR.log | grep "uploadLincenceFile"`
\ No newline at end of file
4. 上传成功后检查后端日志确认:`tail -f /data/services/api/java-meeting/java-meeting-extapi/logs/ubains-INFO-AND-ERROR.log | grep "uploadLincenceFile"`
---
## 定时任务自动化部署(Scheduled Deploy)
> **核心原则:定时任务执行时无需任何人工交互,全程自动化!**
### 定时任务配置
| 项目 | 配置 |
|------|------|
| **执行时间** | 工作日(周一至周五)22:00 |
| **触发方式** | Claude Code CronJob(持久化到 `.claude/scheduled_tasks.json`) |
| **主控脚本** | `code/scheduled_deploy.py` |
| **超时时间** | 2小时(7200秒) |
### 自动化执行流程
```
22:00 定时触发
├─ [自动] 检查工作日+时间条件(非工作日跳过)
├─ [自动] 检查前置条件:
│ ├─ SSH免密配置 (~/.ssh/192.168.5.70/id_rsa)
│ ├─ 部署包 (Z:\发布版本\...\offline_auto_unifiedPlatform.tar.gz)
│ └─ 授权文件 (E:\自动化部署\X86-5.70\license.zip)
├─ [自动] 阶段1-3:部署+验收验证
│ ├─ 上传部署包到服务器
│ ├─ 解压并执行 new_auto.sh --all
│ └─ API接口验证(4个接口,各重试5次间隔30秒)
├─ [自动] 等待10分钟(服务完全启动)
├─ [自动] 阶段4:系统授权(Playwright无头模式)
│ ├─ 登录维护平台(superadmin)
│ ├─ 下载激活文件(绑定硬件指纹)
│ ├─ 上传授权文件(自动处理身份校验对话框)
│ └─ 重启服务(勾选运维系统+预定系统)
└─ [自动] 生成报告到 reports/X86_TX_5.70_定时部署报告_YYYYMMDD_HHMMSS.md
```
### 命令速查
```bash
# 强制立即执行(忽略时间检查)
python .claude/skills/X86-TX-XTYBS/code/scheduled_deploy.py --force
# 仅检查前置条件(不实际部署)
python .claude/skills/X86-TX-XTYBS/code/scheduled_deploy.py --check
# 查看定时任务状态
cat .claude/scheduled_tasks.json
```
### 日志和报告
| 类型 | 路径 |
|------|------|
| **执行日志** | `AuxiliaryTool/ScriptTool/RemoteDeploy/reports/scheduled_deploy_tx_YYYYMMDD.log` |
| **部署报告** | `AuxiliaryTool/ScriptTool/RemoteDeploy/reports/X86_TX_5.70_定时部署报告_YYYYMMDD_HHMMSS.md` |
| **授权截图** | `~/deploy_logs/screenshots/` |
### 定时任务处理原则(给Claude的指令)
> **当定时任务触发执行时,严格遵循以下原则,不要询问任何问题!**
1.**直接执行**:检测到定时任务触发 → 直接运行 `scheduled_deploy.py --force` → 等待完成 → 汇报结果
2.**不确认**:不询问"是否继续"、"是否上传"、"是否确认"
3.**不中断**:即使遇到非致命错误也继续执行下一阶段
4.**自动授权**:阶段4使用 Playwright `headless=True` 无头浏览器,不弹出窗口
5.**异常处理**:授权失败记录错误到日志,不影响整体报告生成
6.**禁止交互**:整个流程中不调用 `AskUserQuestion`、不等待用户输入
### 前置条件(部署前自动检查,无需人工确认)
这些条件在每次部署前由 `scheduled_deploy.py` 自动检查:
1. **SSH免密** - `~/.ssh/192.168.5.70/id_rsa`(已配置,永久有效)
2. **部署包** - `Z:\发布版本\03服务器部署\15新统一平台\X86部署包\全量版\offline_auto_unifiedPlatform.tar.gz`(8.67 GB)
3. **授权文件** - `E:\自动化部署\X86-5.70\license.zip`
如果前置条件不满足,定时任务会记录错误并跳过执行,不会阻塞。
### 修改记录
- **2026-07-11**: 创建定时任务自动化执行方案
- 创建 `code/scheduled_deploy.py` 主控脚本
- 重写 `code/phase4_authorize.py` 为完全无交互版本(headless模式)
- 设置 Claude Code CronJob:工作日 22:00
- 修复 GBK 编码乱码问题
- 明确"禁止交互"原则写入 SKILL.md
\ No newline at end of file
......@@ -76,8 +76,12 @@ class X86Deployer:
self.ssh.close()
self.ssh = None
def phase1_pre_check_and_upload(self):
"""阶段1:部署前置检查与部署包上传"""
def phase1_pre_check_and_upload(self, skip_upload=False):
"""阶段1:部署前置检查与部署包上传
Args:
skip_upload: 跳过上传步骤,直接使用服务器上已有的部署包
"""
print("\n" + "=" * 60)
print("阶段1:部署前置检查与部署包上传")
print("=" * 60)
......@@ -108,54 +112,74 @@ class X86Deployer:
'output': stdout
})
# 2. 清理旧部署文件
print("\n[步骤2] 清理旧部署文件...")
clean_cmd = f"cd /data && rm -rf {self.config['deploy']['deploy_dir']} && rm -f {self.config['deploy']['deploy_package']} && rm -f {self.config['deploy']['deploy_package_md5']}"
stdout, stderr = self.ssh.execute_command(clean_cmd)
print(f"[INFO] 清理完成")
if skip_upload:
print("\n[步骤2] 跳过清理和上传步骤(--skip-upload模式)")
print("[INFO] 检查服务器上是否存在部署包...")
check_cmd = f"ls -lh /data/{self.config['deploy']['deploy_package']}"
stdout, stderr = self.ssh.execute_command(check_cmd)
print(stdout)
# 检查md5文件是否存在,如果不存在则尝试创建
md5_check_cmd = f"ls -lh /data/{self.config['deploy']['deploy_package_md5']}"
md5_stdout, md5_stderr = self.ssh.execute_command(md5_check_cmd)
if 'No such file' in md5_stdout or 'No such file' in md5_stderr or '没有那个文件' in md5_stdout or '没有那个文件' in md5_stderr:
print("[INFO] MD5文件不存在,尝试创建...")
create_md5_cmd = f"cd /data && md5sum {self.config['deploy']['deploy_package']} > {self.config['deploy']['deploy_package_md5']}"
self.ssh.execute_command(create_md5_cmd)
print("[INFO] MD5文件已创建")
phase_result['steps'].append({
'name': '清理旧部署文件',
'status': 'success'
})
phase_result['steps'].append({
'name': '跳过上传(使用服务器已有部署包)',
'status': 'success'
})
else:
# 2. 清理旧部署文件
print("\n[步骤2] 清理旧部署文件...")
clean_cmd = f"cd /data && rm -rf {self.config['deploy']['deploy_dir']} && rm -f {self.config['deploy']['deploy_package']} && rm -f {self.config['deploy']['deploy_package_md5']}"
stdout, stderr = self.ssh.execute_command(clean_cmd)
print(f"[INFO] 清理完成")
# 3. 上传部署包(需要检查本地文件是否存在)
print("\n[步骤3] 上传部署包...")
source_dir = self.config['source']['deploy_package_dir']
deploy_package = self.config['deploy']['deploy_package']
deploy_package_md5 = self.config['deploy']['deploy_package_md5']
phase_result['steps'].append({
'name': '清理旧部署文件',
'status': 'success'
})
local_package = os.path.join(source_dir, deploy_package)
local_md5 = os.path.join(source_dir, deploy_package_md5)
# 3. 上传部署包
print("\n[步骤3] 上传部署包...")
source_dir = self.config['source']['deploy_package_dir']
deploy_package = self.config['deploy']['deploy_package']
deploy_package_md5 = self.config['deploy']['deploy_package_md5']
local_package = os.path.join(source_dir, deploy_package)
local_md5 = os.path.join(source_dir, deploy_package_md5)
if not os.path.exists(local_package):
error_msg = f"部署包不存在: {local_package}"
print(f"[ERROR] {error_msg}")
phase_result['steps'].append({
'name': '上传部署包',
'status': 'failed',
'error': error_msg
})
self.deploy_report['errors'].append(error_msg)
return False
self.ssh.upload_file(local_package, f"/data/{deploy_package}")
self.ssh.upload_file(local_md5, f"/data/{deploy_package_md5}")
# 检查本地文件是否存在
if not os.path.exists(local_package):
error_msg = f"部署包不存在: {local_package}"
print(f"[ERROR] {error_msg}")
phase_result['steps'].append({
'name': '上传部署包',
'status': 'failed',
'error': error_msg
'status': 'success',
'files': [deploy_package, deploy_package_md5]
})
self.deploy_report['errors'].append(error_msg)
return False
# 使用 SFTP 上传
self.ssh.upload_file(local_package, f"/data/{deploy_package}")
self.ssh.upload_file(local_md5, f"/data/{deploy_package_md5}")
phase_result['steps'].append({
'name': '上传部署包',
'status': 'success',
'files': [deploy_package, deploy_package_md5]
})
# 4. 校验部署包完整性
# 4. 校验部署包完整性(无论是否跳过上传都执行)
print("\n[步骤4] 校验部署包完整性...")
verify_cmd = f"cd /data && md5sum -c {deploy_package_md5}"
verify_cmd = f"cd /data && md5sum -c {self.config['deploy']['deploy_package_md5']}"
exit_status, stdout, stderr = self.ssh.execute_command_with_status(verify_cmd)
if exit_status == 0 and 'OK' in stdout:
# md5sum -c 输出可能是中文"成功"或英文"OK"
if exit_status == 0 and ('OK' in stdout or '成功' in stdout):
print(f"[SUCCESS] 部署包完整性校验通过")
phase_result['steps'].append({
'name': '部署包完整性校验',
......@@ -459,8 +483,12 @@ class X86Deployer:
print(f"[SUCCESS] 报告已保存: {report_file}")
return str(report_file)
def run_deploy(self):
"""执行完整部署流程"""
def run_deploy(self, skip_upload=False):
"""执行完整部署流程
Args:
skip_upload: 跳过上传步骤,使用服务器上已有的部署包
"""
self.deploy_report['start_time'] = datetime.now().isoformat()
try:
......@@ -468,7 +496,7 @@ class X86Deployer:
self.connect()
# 阶段1
if not self.phase1_pre_check_and_upload():
if not self.phase1_pre_check_and_upload(skip_upload=skip_upload):
print("[ERROR] 阶段1失败,终止部署")
return False
......@@ -506,14 +534,18 @@ class X86Deployer:
finally:
self.disconnect()
def run_full(self):
"""执行完整流程(部署+验收)"""
def run_full(self, skip_upload=False):
"""执行完整流程(部署+验收)
Args:
skip_upload: 跳过上传步骤,使用服务器上已有的部署包
"""
self.deploy_report['start_time'] = datetime.now().isoformat()
try:
self.connect()
if not self.phase1_pre_check_and_upload():
if not self.phase1_pre_check_and_upload(skip_upload=skip_upload):
return False
if not self.phase2_extract_and_deploy():
......@@ -542,17 +574,18 @@ def main():
parser.add_argument('--verify', action='store_true', help='执行验收检查(阶段3)')
parser.add_argument('--full', action='store_true', help='执行完整流程(阶段1+2+3)')
parser.add_argument('--config', default=None, help='配置文件路径')
parser.add_argument('--skip-upload', action='store_true', help='跳过上传步骤,使用服务器上已有的部署包')
args = parser.parse_args()
deployer = X86Deployer(args.config)
if args.deploy:
deployer.run_deploy()
deployer.run_deploy(skip_upload=args.skip_upload)
elif args.verify:
deployer.run_verify()
elif args.full:
deployer.run_full()
deployer.run_full(skip_upload=args.skip_upload)
else:
parser.print_help()
......
......@@ -100,7 +100,7 @@ CONFIGS = {
'deploy_dir': '/data/offline_auto_unifiedPlatform',
'deploy_pkg': 'offline_auto_unifiedPlatform.tar.gz',
'deploy_md5': 'offline_auto_unifiedPlatform.tar.gz.md5',
'nas_dir': r'Z:\发布版本\03服务器部署\15新统一平台\X86部署包\全量版',
'nas_dir': None, # 跳过上传,使用服务器上已有的部署包
'deploy_script': 'new_auto.sh',
'deploy_answers': "y\ny\ny\ny\ny\ny\ny\nn\n",
'deploy_log': '/data/offline_auto_unifiedPlatform/deploy_output.log',
......
......@@ -132,8 +132,8 @@ docker run -d \
-p 9849:9849 \
-p 7848:7848 \
-e MODE=cluster \
-e NACOS_SERVERS="192.168.9.89:8848,192.168.9.90:8848,192.168.9.91:8848" \
-e NACOS_SERVER_IP="$server_ip" \
-e NACOS_SERVERS="192.168.5.41:8848,192.168.5.42:8848,192.168.5.43:8848" \
-e NACOS_SERVER_IP=192.168.5.41 \
-e NACOS_AUTH_ENABLE=true \
-e NACOS_AUTH_USERNAME="nacos" \
-e NACOS_AUTH_PASSWORD="dNrprU&2S" \
......@@ -147,7 +147,7 @@ docker run -d \
-v /data/middleware/nacos:/home/nacos \
-v /etc/localtime:/etc/localtime:ro \
--mac-address="02:42:ac:11:00:10" \
nacos/nacos-server:v2.5.2
nacos-server:v2.5.2
```
**关键环境变量说明:**
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论