- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
60
scripts/tools/backup_system.py
Normal file
60
scripts/tools/backup_system.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
import datetime
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
def create_backup():
|
||||
"""
|
||||
建立系統完整備份 (Zip 壓縮檔)
|
||||
檔名格式: momo_pro_system_backup_YYYYMMDD_HHMMSS_V{version}.zip
|
||||
"""
|
||||
# 1. 基礎路徑設定
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
backup_folder = os.path.join(base_dir, 'backups')
|
||||
|
||||
if not os.path.exists(backup_folder):
|
||||
os.makedirs(backup_folder)
|
||||
print(f"📂 已建立備份目錄: {backup_folder}")
|
||||
|
||||
# 2. 嘗試從 app.py 讀取版本號
|
||||
version = "Unknown"
|
||||
app_py_path = os.path.join(base_dir, 'app.py')
|
||||
try:
|
||||
if os.path.exists(app_py_path):
|
||||
with open(app_py_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
# 尋找 SYSTEM_VERSION = "V9.0"
|
||||
match = re.search(r'SYSTEM_VERSION\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 無法讀取版本號: {e}")
|
||||
|
||||
# 3. 產生備份檔名
|
||||
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
base_name = f"momo_pro_system_backup_{timestamp}_{version}.zip"
|
||||
output_path = os.path.join(backup_folder, base_name)
|
||||
|
||||
print(f"📦 正在打包專案目錄: {base_dir}")
|
||||
print(f"🎯 目標檔案: {output_path}")
|
||||
|
||||
# 4. 執行壓縮
|
||||
try:
|
||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
# 排除不需要備份的目錄
|
||||
for ignore in ['backups', '__pycache__', '.git', '.idea', '.vscode', 'bin', 'bin 2']:
|
||||
if ignore in dirs:
|
||||
dirs.remove(ignore)
|
||||
|
||||
for file in files:
|
||||
if file == '.DS_Store' or file.endswith('.pyc'): continue
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, base_dir)
|
||||
zipf.write(file_path, arcname)
|
||||
print(f"✅ 備份完成!")
|
||||
except Exception as e:
|
||||
print(f"❌ 備份失敗: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_backup()
|
||||
127
scripts/tools/database_exporter.py
Executable file
127
scripts/tools/database_exporter.py
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SQLite 資料庫監控腳本
|
||||
收集資料庫狀態、大小、查詢效能等指標
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from flask import Flask
|
||||
from prometheus_client import Gauge, Counter, Histogram, generate_latest, REGISTRY
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 定義 Prometheus 指標
|
||||
db_size_bytes = Gauge('sqlite_database_size_bytes', '資料庫檔案大小(bytes)', ['database'])
|
||||
db_record_count = Gauge('sqlite_record_count', '資料表記錄總數', ['database', 'table'])
|
||||
db_query_duration = Histogram('sqlite_query_duration_seconds', '查詢執行時間', ['query_type'])
|
||||
db_connection_errors = Counter('sqlite_connection_errors_total', '連接錯誤總數')
|
||||
db_slow_queries = Counter('sqlite_slow_queries_total', '慢查詢總數(>1秒)', ['table'])
|
||||
|
||||
# 資料庫路徑
|
||||
DATABASE_PATH = '/home/ogt/momo_pro_system/data/momo_database.db'
|
||||
|
||||
def get_db_size():
|
||||
"""獲取資料庫檔案大小"""
|
||||
try:
|
||||
if os.path.exists(DATABASE_PATH):
|
||||
size = os.path.getsize(DATABASE_PATH)
|
||||
db_size_bytes.labels(database='momo').set(size)
|
||||
return size
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error getting database size: {e}")
|
||||
return 0
|
||||
|
||||
def get_table_counts():
|
||||
"""獲取各資料表的記錄數"""
|
||||
try:
|
||||
conn = sqlite3.connect(DATABASE_PATH, timeout=5)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 獲取所有資料表
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
for (table_name,) in tables:
|
||||
try:
|
||||
start_time = time.time()
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||
count = cursor.fetchone()[0]
|
||||
duration = time.time() - start_time
|
||||
|
||||
# 記錄指標
|
||||
db_record_count.labels(database='momo', table=table_name).set(count)
|
||||
db_query_duration.labels(query_type='count').observe(duration)
|
||||
|
||||
# 檢測慢查詢
|
||||
if duration > 1.0:
|
||||
db_slow_queries.labels(table=table_name).inc()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error counting table {table_name}: {e}")
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database connection error: {e}")
|
||||
db_connection_errors.inc()
|
||||
|
||||
def measure_query_performance():
|
||||
"""測試常見查詢的效能"""
|
||||
try:
|
||||
conn = sqlite3.connect(DATABASE_PATH, timeout=5)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 測試查詢 1:最近銷售數據
|
||||
start_time = time.time()
|
||||
cursor.execute("SELECT * FROM realtime_sales_monthly ORDER BY 日期 DESC LIMIT 100")
|
||||
cursor.fetchall()
|
||||
duration = time.time() - start_time
|
||||
db_query_duration.labels(query_type='recent_sales').observe(duration)
|
||||
|
||||
if duration > 1.0:
|
||||
db_slow_queries.labels(table='realtime_sales_monthly').inc()
|
||||
|
||||
# 測試查詢 2:商品統計
|
||||
start_time = time.time()
|
||||
cursor.execute("SELECT 品牌, COUNT(*) FROM realtime_sales_monthly GROUP BY 品牌 LIMIT 50")
|
||||
cursor.fetchall()
|
||||
duration = time.time() - start_time
|
||||
db_query_duration.labels(query_type='brand_stats').observe(duration)
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Query performance test error: {e}")
|
||||
db_connection_errors.inc()
|
||||
|
||||
@app.route('/metrics')
|
||||
def metrics():
|
||||
"""Prometheus metrics endpoint"""
|
||||
# 收集最新指標
|
||||
get_db_size()
|
||||
get_table_counts()
|
||||
measure_query_performance()
|
||||
|
||||
# 返回 Prometheus 格式的指標
|
||||
return generate_latest(REGISTRY)
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
"""健康檢查端點"""
|
||||
try:
|
||||
if os.path.exists(DATABASE_PATH):
|
||||
conn = sqlite3.connect(DATABASE_PATH, timeout=2)
|
||||
conn.close()
|
||||
return 'OK', 200
|
||||
return 'Database file not found', 503
|
||||
except Exception as e:
|
||||
return f'Error: {str(e)}', 503
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"Starting Database Exporter on http://127.0.0.1:9120/metrics")
|
||||
app.run(host='127.0.0.1', port=9120)
|
||||
198
scripts/tools/database_optimization.py
Normal file
198
scripts/tools/database_optimization.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SQLite 資料庫優化腳本
|
||||
建議在低流量時段執行(晚上 8 點後)
|
||||
|
||||
功能:
|
||||
1. 添加缺失的索引(提升查詢效能)
|
||||
2. 執行 VACUUM 清理(回收空間、優化儲存)
|
||||
3. 分析資料庫統計(更新查詢規劃器統計資訊)
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 資料庫路徑
|
||||
DB_PATH = '/home/ogt/momo_pro_system/data/momo_database.db'
|
||||
|
||||
# 日誌函數
|
||||
def log(message, level="INFO"):
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"[{timestamp}] [{level}] {message}")
|
||||
|
||||
# 檢查資料庫大小
|
||||
def get_db_size():
|
||||
size_bytes = os.path.getsize(DB_PATH)
|
||||
size_mb = size_bytes / (1024 * 1024)
|
||||
return size_bytes, size_mb
|
||||
|
||||
# 檢查索引是否存在
|
||||
def index_exists(cursor, index_name):
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
||||
(index_name,)
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
# 添加索引
|
||||
def add_indexes(conn):
|
||||
log("開始添加索引...")
|
||||
cursor = conn.cursor()
|
||||
|
||||
indexes = [
|
||||
# products 表索引
|
||||
("idx_products_category", "CREATE INDEX IF NOT EXISTS idx_products_category ON products (category)"),
|
||||
("idx_products_status", "CREATE INDEX IF NOT EXISTS idx_products_status ON products (status)"),
|
||||
("idx_products_updated_at", "CREATE INDEX IF NOT EXISTS idx_products_updated_at ON products (updated_at DESC)"),
|
||||
|
||||
# price_records 表索引
|
||||
("idx_price_records_product_id", "CREATE INDEX IF NOT EXISTS idx_price_records_product_id ON price_records (product_id)"),
|
||||
("idx_price_records_product_time", "CREATE INDEX IF NOT EXISTS idx_price_records_product_time ON price_records (product_id, timestamp DESC)"),
|
||||
|
||||
# promo_products 表索引
|
||||
("idx_promo_crawled_at", "CREATE INDEX IF NOT EXISTS idx_promo_crawled_at ON promo_products (crawled_at DESC)"),
|
||||
("idx_promo_batch_id", "CREATE INDEX IF NOT EXISTS idx_promo_batch_id ON promo_products (batch_id)"),
|
||||
("idx_promo_status_change", "CREATE INDEX IF NOT EXISTS idx_promo_status_change ON promo_products (status_change)"),
|
||||
]
|
||||
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for index_name, create_sql in indexes:
|
||||
try:
|
||||
if index_exists(cursor, index_name):
|
||||
log(f" 索引已存在,跳過: {index_name}", "INFO")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
log(f" 創建索引: {index_name}...", "INFO")
|
||||
start_time = time.time()
|
||||
cursor.execute(create_sql)
|
||||
conn.commit()
|
||||
elapsed = time.time() - start_time
|
||||
log(f" ✓ 索引創建成功: {index_name} (耗時 {elapsed:.2f} 秒)", "SUCCESS")
|
||||
added_count += 1
|
||||
|
||||
except Exception as e:
|
||||
log(f" ✗ 索引創建失敗: {index_name} - {e}", "ERROR")
|
||||
conn.rollback()
|
||||
|
||||
log(f"索引添加完成:新增 {added_count} 個,跳過 {skipped_count} 個", "INFO")
|
||||
return added_count
|
||||
|
||||
# 執行 VACUUM
|
||||
def run_vacuum(conn):
|
||||
log("開始執行 VACUUM 清理...", "INFO")
|
||||
log("⚠️ 注意:此操作會鎖定整個資料庫,期間無法讀寫", "WARNING")
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
conn.execute("VACUUM")
|
||||
elapsed = time.time() - start_time
|
||||
log(f"✓ VACUUM 執行成功 (耗時 {elapsed:.2f} 秒)", "SUCCESS")
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"✗ VACUUM 執行失敗: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
# 分析資料庫統計
|
||||
def analyze_database(conn):
|
||||
log("開始分析資料庫統計...", "INFO")
|
||||
try:
|
||||
start_time = time.time()
|
||||
conn.execute("ANALYZE")
|
||||
elapsed = time.time() - start_time
|
||||
log(f"✓ 資料庫分析完成 (耗時 {elapsed:.2f} 秒)", "SUCCESS")
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"✗ 資料庫分析失敗: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
# 顯示優化前後對比
|
||||
def show_statistics(before_size, after_size):
|
||||
log("=" * 60)
|
||||
log("資料庫優化統計報告", "INFO")
|
||||
log("=" * 60)
|
||||
log(f"優化前大小: {before_size:.2f} MB", "INFO")
|
||||
log(f"優化後大小: {after_size:.2f} MB", "INFO")
|
||||
|
||||
if before_size > after_size:
|
||||
saved = before_size - after_size
|
||||
saved_pct = (saved / before_size) * 100
|
||||
log(f"節省空間: {saved:.2f} MB ({saved_pct:.1f}%)", "SUCCESS")
|
||||
else:
|
||||
log("空間無變化(資料庫已經很緊湊)", "INFO")
|
||||
|
||||
log("=" * 60)
|
||||
|
||||
def main():
|
||||
log("=" * 60)
|
||||
log("SQLite 資料庫優化腳本啟動", "INFO")
|
||||
log("=" * 60)
|
||||
|
||||
# 檢查資料庫是否存在
|
||||
if not os.path.exists(DB_PATH):
|
||||
log(f"錯誤:資料庫不存在 - {DB_PATH}", "ERROR")
|
||||
sys.exit(1)
|
||||
|
||||
# 記錄開始時間和大小
|
||||
before_bytes, before_mb = get_db_size()
|
||||
log(f"當前資料庫大小: {before_mb:.2f} MB", "INFO")
|
||||
|
||||
try:
|
||||
# 連接資料庫
|
||||
log("連接到資料庫...", "INFO")
|
||||
conn = sqlite3.connect(DB_PATH, timeout=30.0)
|
||||
|
||||
# 步驟 1: 添加索引
|
||||
log("\n" + "=" * 60)
|
||||
log("步驟 1/3: 添加索引", "INFO")
|
||||
log("=" * 60)
|
||||
added_count = add_indexes(conn)
|
||||
|
||||
# 步驟 2: 執行 VACUUM
|
||||
log("\n" + "=" * 60)
|
||||
log("步驟 2/3: 執行 VACUUM 清理", "INFO")
|
||||
log("=" * 60)
|
||||
vacuum_success = run_vacuum(conn)
|
||||
|
||||
# 步驟 3: 分析資料庫
|
||||
log("\n" + "=" * 60)
|
||||
log("步驟 3/3: 分析資料庫統計", "INFO")
|
||||
log("=" * 60)
|
||||
analyze_success = analyze_database(conn)
|
||||
|
||||
# 關閉連接
|
||||
conn.close()
|
||||
log("資料庫連接已關閉", "INFO")
|
||||
|
||||
# 顯示統計
|
||||
after_bytes, after_mb = get_db_size()
|
||||
log("")
|
||||
show_statistics(before_mb, after_mb)
|
||||
|
||||
# 最終結果
|
||||
log("")
|
||||
log("=" * 60)
|
||||
if added_count > 0 or vacuum_success:
|
||||
log("✓ 資料庫優化完成!", "SUCCESS")
|
||||
else:
|
||||
log("資料庫優化執行完畢(無顯著變化)", "INFO")
|
||||
log("=" * 60)
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
log(f"資料庫操作錯誤: {e}", "ERROR")
|
||||
log("可能原因:資料庫正在被其他程序使用", "ERROR")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
log(f"未預期的錯誤: {e}", "ERROR")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
102
scripts/tools/generate_password_hash.py
Normal file
102
scripts/tools/generate_password_hash.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
密碼雜湊生成工具
|
||||
|
||||
使用方法:
|
||||
1. 直接執行:python generate_password_hash.py
|
||||
2. 輸入新密碼(至少8字元,包含英文和數字)
|
||||
3. 將生成的雜湊值複製到 .env 檔案的 LOGIN_PASSWORD 欄位
|
||||
|
||||
注意:執行後請立即刪除或清空終端機歷史記錄,避免明文密碼外洩
|
||||
"""
|
||||
|
||||
from werkzeug.security import generate_password_hash
|
||||
import sys
|
||||
import getpass
|
||||
|
||||
def validate_password_strength(password):
|
||||
"""
|
||||
驗證密碼強度
|
||||
|
||||
要求:
|
||||
- 至少 8 個字元
|
||||
- 包含英文字母
|
||||
- 包含數字
|
||||
"""
|
||||
if len(password) < 8:
|
||||
return False, "密碼長度至少需要 8 個字元"
|
||||
|
||||
has_letter = any(c.isalpha() for c in password)
|
||||
has_digit = any(c.isdigit() for c in password)
|
||||
|
||||
if not has_letter:
|
||||
return False, "密碼必須包含英文字母"
|
||||
|
||||
if not has_digit:
|
||||
return False, "密碼必須包含數字"
|
||||
|
||||
return True, None
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("🔐 MOMO 監控系統 - 密碼雜湊生成工具")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("密碼要求:")
|
||||
print(" • 至少 8 個字元")
|
||||
print(" • 包含英文字母(大小寫皆可)")
|
||||
print(" • 包含數字")
|
||||
print(" • 建議使用符號增加強度(如 !@#$%^&*)")
|
||||
print()
|
||||
|
||||
try:
|
||||
# 使用 getpass 隱藏輸入(不會在終端顯示)
|
||||
password = getpass.getpass("請輸入新密碼: ")
|
||||
|
||||
if not password:
|
||||
print("\n❌ 密碼不能為空")
|
||||
sys.exit(1)
|
||||
|
||||
# 驗證密碼強度
|
||||
is_valid, error_message = validate_password_strength(password)
|
||||
if not is_valid:
|
||||
print(f"\n❌ {error_message}")
|
||||
sys.exit(1)
|
||||
|
||||
# 再次確認密碼
|
||||
password_confirm = getpass.getpass("請再次輸入密碼: ")
|
||||
|
||||
if password != password_confirm:
|
||||
print("\n❌ 兩次輸入的密碼不一致")
|
||||
sys.exit(1)
|
||||
|
||||
# 生成雜湊
|
||||
password_hash = generate_password_hash(password, method='pbkdf2:sha256', salt_length=16)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 密碼雜湊生成成功!")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("請將以下內容複製到 .env 檔案中:")
|
||||
print()
|
||||
print(f"LOGIN_PASSWORD={password_hash}")
|
||||
print()
|
||||
print("⚠️ 安全提醒:")
|
||||
print(" 1. 請立即將上述雜湊值複製到 .env 檔案")
|
||||
print(" 2. 重新啟動 app.py 使新密碼生效")
|
||||
print(" 3. 清空終端機歷史記錄(history -c)")
|
||||
print(" 4. 不要將 .env 檔案提交到 Git")
|
||||
print(" 5. 定期更換密碼(建議每 90 天)")
|
||||
print()
|
||||
print("=" * 60)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ 操作已取消")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 發生錯誤: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
212
scripts/tools/migrate-to-k8s.sh
Normal file
212
scripts/tools/migrate-to-k8s.sh
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# Docker 服務遷移到 K8s 腳本
|
||||
# 用途:將 n8n 和 Superset 從 Docker 遷移到 K8s
|
||||
# ==========================================================
|
||||
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="/home/wooo/backups/k8s-migration"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
KUBECONFIG="/home/wooo/.kube/config"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo "[ERROR] $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 確認執行
|
||||
confirm() {
|
||||
read -p "$1 (y/N): " response
|
||||
case "$response" in
|
||||
[yY][eE][sS]|[yY]) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 步驟 1:備份資料
|
||||
backup_data() {
|
||||
log "=== 步驟 1:備份資料 ==="
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 備份 n8n
|
||||
log "備份 n8n 資料..."
|
||||
docker run --rm -v n8n_data:/data -v "$BACKUP_DIR":/backup \
|
||||
alpine tar czf "/backup/n8n_data_${DATE}.tar.gz" -C /data .
|
||||
log "✅ n8n 備份完成: $BACKUP_DIR/n8n_data_${DATE}.tar.gz"
|
||||
|
||||
# 備份 Superset PostgreSQL
|
||||
log "備份 Superset PostgreSQL..."
|
||||
docker exec superset-postgres pg_dump -U superset superset > \
|
||||
"$BACKUP_DIR/superset_db_${DATE}.sql"
|
||||
log "✅ Superset DB 備份完成: $BACKUP_DIR/superset_db_${DATE}.sql"
|
||||
|
||||
# 備份 Superset home
|
||||
log "備份 Superset home..."
|
||||
docker run --rm -v superset_superset_home:/data -v "$BACKUP_DIR":/backup \
|
||||
alpine tar czf "/backup/superset_home_${DATE}.tar.gz" -C /data .
|
||||
log "✅ Superset home 備份完成"
|
||||
|
||||
log "=== 備份完成 ==="
|
||||
ls -la "$BACKUP_DIR"
|
||||
}
|
||||
|
||||
# 步驟 2:部署 K8s 服務
|
||||
deploy_k8s() {
|
||||
log "=== 步驟 2:部署 K8s 服務 ==="
|
||||
|
||||
export KUBECONFIG="$KUBECONFIG"
|
||||
|
||||
# 建立 namespace
|
||||
log "建立 tools namespace..."
|
||||
kubectl apply -f /home/wooo/momo_pro_system/k8s/tools/00-namespace.yaml
|
||||
|
||||
# 部署 n8n
|
||||
log "部署 n8n..."
|
||||
kubectl apply -f /home/wooo/momo_pro_system/k8s/tools/01-n8n.yaml
|
||||
|
||||
# 部署 Superset
|
||||
log "部署 Superset..."
|
||||
kubectl apply -f /home/wooo/momo_pro_system/k8s/tools/02-superset.yaml
|
||||
|
||||
# 等待 Pod 就緒
|
||||
log "等待 Pod 就緒..."
|
||||
kubectl wait --for=condition=ready pod -l app=n8n -n tools --timeout=300s || true
|
||||
kubectl wait --for=condition=ready pod -l app=superset -n tools --timeout=300s || true
|
||||
|
||||
log "=== K8s 部署完成 ==="
|
||||
kubectl get pods -n tools
|
||||
}
|
||||
|
||||
# 步驟 3:還原資料到 K8s
|
||||
restore_data() {
|
||||
log "=== 步驟 3:還原資料 ==="
|
||||
|
||||
export KUBECONFIG="$KUBECONFIG"
|
||||
|
||||
# 取得 n8n PVC 路徑
|
||||
N8N_PV=$(kubectl get pvc n8n-data -n tools -o jsonpath='{.spec.volumeName}')
|
||||
N8N_PATH="/var/lib/rancher/k3s/storage/${N8N_PV}"
|
||||
|
||||
# 還原 n8n 資料
|
||||
log "還原 n8n 資料..."
|
||||
LATEST_N8N=$(ls -t "$BACKUP_DIR"/n8n_data_*.tar.gz | head -1)
|
||||
if [ -f "$LATEST_N8N" ]; then
|
||||
sudo tar xzf "$LATEST_N8N" -C "$N8N_PATH"
|
||||
log "✅ n8n 資料已還原"
|
||||
fi
|
||||
|
||||
# 重啟 n8n Pod 以載入資料
|
||||
kubectl rollout restart deployment/n8n -n tools
|
||||
|
||||
log "=== 資料還原完成 ==="
|
||||
}
|
||||
|
||||
# 步驟 4:更新 Nginx
|
||||
update_nginx() {
|
||||
log "=== 步驟 4:更新 Nginx 配置 ==="
|
||||
|
||||
# 取得 K8s Service ClusterIP
|
||||
export KUBECONFIG="$KUBECONFIG"
|
||||
N8N_IP=$(kubectl get svc n8n -n tools -o jsonpath='{.spec.clusterIP}')
|
||||
SUPERSET_IP=$(kubectl get svc superset -n tools -o jsonpath='{.spec.clusterIP}')
|
||||
|
||||
log "n8n ClusterIP: $N8N_IP"
|
||||
log "Superset ClusterIP: $SUPERSET_IP"
|
||||
|
||||
log "請手動更新 /etc/nginx/sites-enabled/monitor:"
|
||||
log " n8n_backend: $N8N_IP:5678"
|
||||
log " superset_backend: $SUPERSET_IP:8088"
|
||||
|
||||
log "=== Nginx 更新提示完成 ==="
|
||||
}
|
||||
|
||||
# 步驟 5:停止 Docker 服務
|
||||
stop_docker() {
|
||||
log "=== 步驟 5:停止 Docker 服務 ==="
|
||||
|
||||
if confirm "確定要停止 Docker 服務嗎?"; then
|
||||
docker stop momo-n8n momo-superset superset-postgres superset-redis 2>/dev/null || true
|
||||
log "✅ Docker 服務已停止"
|
||||
else
|
||||
log "跳過停止 Docker 服務"
|
||||
fi
|
||||
}
|
||||
|
||||
# 顯示狀態
|
||||
show_status() {
|
||||
log "=== 當前狀態 ==="
|
||||
|
||||
echo ""
|
||||
echo "Docker 容器:"
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}" | grep -E "n8n|superset" || echo "無相關容器"
|
||||
|
||||
echo ""
|
||||
echo "K8s Pods:"
|
||||
export KUBECONFIG="$KUBECONFIG"
|
||||
kubectl get pods -n tools 2>/dev/null || echo "tools namespace 不存在"
|
||||
}
|
||||
|
||||
# 主選單
|
||||
main() {
|
||||
echo "========================================"
|
||||
echo " Docker → K8s 遷移工具"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "1) 備份資料"
|
||||
echo "2) 部署 K8s 服務"
|
||||
echo "3) 還原資料到 K8s"
|
||||
echo "4) 更新 Nginx 配置"
|
||||
echo "5) 停止 Docker 服務"
|
||||
echo "6) 顯示當前狀態"
|
||||
echo "7) 完整遷移 (1-5)"
|
||||
echo "0) 退出"
|
||||
echo ""
|
||||
|
||||
read -p "請選擇操作: " choice
|
||||
|
||||
case $choice in
|
||||
1) backup_data ;;
|
||||
2) deploy_k8s ;;
|
||||
3) restore_data ;;
|
||||
4) update_nginx ;;
|
||||
5) stop_docker ;;
|
||||
6) show_status ;;
|
||||
7)
|
||||
backup_data
|
||||
deploy_k8s
|
||||
restore_data
|
||||
update_nginx
|
||||
stop_docker
|
||||
;;
|
||||
0) exit 0 ;;
|
||||
*) error "無效選項" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 如果有參數,直接執行對應步驟
|
||||
if [ -n "$1" ]; then
|
||||
case $1 in
|
||||
backup) backup_data ;;
|
||||
deploy) deploy_k8s ;;
|
||||
restore) restore_data ;;
|
||||
nginx) update_nginx ;;
|
||||
stop) stop_docker ;;
|
||||
status) show_status ;;
|
||||
all)
|
||||
backup_data
|
||||
deploy_k8s
|
||||
restore_data
|
||||
update_nginx
|
||||
stop_docker
|
||||
;;
|
||||
*) echo "用法: $0 [backup|deploy|restore|nginx|stop|status|all]" ;;
|
||||
esac
|
||||
else
|
||||
main
|
||||
fi
|
||||
28
scripts/tools/momo-startup-complete.service
Normal file
28
scripts/tools/momo-startup-complete.service
Normal file
@@ -0,0 +1,28 @@
|
||||
[Unit]
|
||||
Description=MOMO Pro System Complete Startup Service
|
||||
Documentation=https://mo.wooo.work
|
||||
After=docker.service k3s.service network-online.target
|
||||
Wants=docker.service k3s.service network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/home/wooo/momo_pro_system
|
||||
|
||||
# 等待系統完全啟動
|
||||
ExecStartPre=/bin/sleep 60
|
||||
|
||||
# 執行啟動腳本
|
||||
ExecStart=/home/wooo/momo_pro_system/scripts/tools/system_startup_complete.sh
|
||||
|
||||
# 超時設定(10分鐘)
|
||||
TimeoutStartSec=600
|
||||
|
||||
# 日誌
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=momo-startup
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
22
scripts/tools/momo-startup.service
Normal file
22
scripts/tools/momo-startup.service
Normal file
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=MOMO Pro System Startup Service
|
||||
After=docker.service network-online.target
|
||||
Wants=docker.service network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/home/wooo/momo_pro_system
|
||||
ExecStartPre=/bin/sleep 30
|
||||
ExecStart=/home/wooo/momo_pro_system/scripts/tools/system_startup.sh
|
||||
TimeoutStartSec=600
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# 環境變數(可選,用於 Telegram 通知)
|
||||
Environment="TELEGRAM_BOT_TOKEN=8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg"
|
||||
Environment="TELEGRAM_CHAT_ID=5619078117"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
73
scripts/tools/run_crawler_once.py
Normal file
73
scripts/tools/run_crawler_once.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# cSpell:ignore momo
|
||||
"""
|
||||
一次性執行爬蟲腳本
|
||||
用於手動觸發商品資料和圖片的更新
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# 設定路徑
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
|
||||
# 設定日誌
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
from scheduler import run_momo_task, run_edm_task, run_festival_task
|
||||
|
||||
def main():
|
||||
"""執行所有爬蟲任務"""
|
||||
print("=" * 80)
|
||||
print("🚀 開始執行完整爬蟲任務")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
try:
|
||||
# 1. 一般商品爬蟲
|
||||
print("📦 [1/3] 執行一般商品爬蟲...")
|
||||
print("-" * 80)
|
||||
run_momo_task()
|
||||
print()
|
||||
print("✅ 一般商品爬蟲完成")
|
||||
print()
|
||||
|
||||
# 2. EDM 促銷商品爬蟲
|
||||
print("🎁 [2/3] 執行 EDM 促銷商品爬蟲...")
|
||||
print("-" * 80)
|
||||
run_edm_task()
|
||||
print()
|
||||
print("✅ EDM 促銷商品爬蟲完成")
|
||||
print()
|
||||
|
||||
# 3. Festival 購物節商品爬蟲
|
||||
print("🎉 [3/3] 執行購物節商品爬蟲...")
|
||||
print("-" * 80)
|
||||
run_festival_task()
|
||||
print()
|
||||
print("✅ 購物節商品爬蟲完成")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("🎉 所有爬蟲任務執行完成!")
|
||||
print("=" * 80)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ 使用者中斷執行")
|
||||
print("=" * 80)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n\n❌ 執行過程發生錯誤: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("=" * 80)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
scripts/tools/run_scheduler.py
Normal file
87
scripts/tools/run_scheduler.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
獨立排程器服務
|
||||
負責定時執行商品看板和活動看板的爬蟲任務
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import schedule
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# 確保在正確的目錄運行
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(BASE_DIR)
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
|
||||
# 設定台北時區
|
||||
TAIPEI_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
# 設定日誌
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.join(BASE_DIR, "logs/scheduler.log"), encoding="utf-8"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("SchedulerService")
|
||||
|
||||
# 導入爬蟲任務
|
||||
from scheduler import run_momo_task, run_edm_task, run_festival_task, run_auto_import_task
|
||||
|
||||
def safe_task_wrapper(task_func, task_name):
|
||||
"""安全的任務包裝器,捕獲異常避免排程中斷"""
|
||||
def wrapper():
|
||||
try:
|
||||
timestamp = datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M:%S')
|
||||
logger.info(f"⏰ [{timestamp}] 開始執行任務: {task_name}")
|
||||
task_func()
|
||||
logger.info(f"✅ [{timestamp}] 任務完成: {task_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 任務執行失敗: {task_name} | Error: {e}", exc_info=True)
|
||||
return wrapper
|
||||
|
||||
def main():
|
||||
"""主程序"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("🚀 WOOO TECH 排程器服務啟動")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 設定排程任務
|
||||
# V-Opt 2026-01-14: 商品看板和 EDM 需每小時執行以監控價格變化
|
||||
# 商品看板(主站爬蟲)- 每 1 小時執行
|
||||
schedule.every(1).hours.do(safe_task_wrapper(run_momo_task, "商品看板爬蟲"))
|
||||
logger.info("📅 已設定:商品看板爬蟲 - 每 1 小時執行一次")
|
||||
|
||||
# EDM 限時搶購看板 - 每 1 小時執行
|
||||
schedule.every(1).hours.do(safe_task_wrapper(run_edm_task, "EDM 限時搶購爬蟲"))
|
||||
logger.info("📅 已設定:EDM 限時搶購爬蟲 - 每 1 小時執行一次")
|
||||
|
||||
# 購物節活動看板 - 每 6 小時執行
|
||||
schedule.every(6).hours.do(safe_task_wrapper(run_festival_task, "購物節活動爬蟲"))
|
||||
logger.info("📅 已設定:購物節活動爬蟲 - 每 6 小時執行一次")
|
||||
|
||||
# Google Drive 自動匯入 - 每 30 分鐘執行
|
||||
schedule.every(30).minutes.do(safe_task_wrapper(run_auto_import_task, "Google Drive 自動匯入"))
|
||||
logger.info("📅 已設定:Google Drive 自動匯入 - 每 30 分鐘執行一次")
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("✅ 所有排程任務已設定完成,開始監聽...")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 主循環
|
||||
try:
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("🔌 收到中斷信號,排程器服務正在關閉...")
|
||||
except Exception as e:
|
||||
logger.error(f"🚨 排程器服務異常: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
148
scripts/tools/run_telegram_bot.py
Normal file
148
scripts/tools/run_telegram_bot.py
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Telegram Bot 獨立執行腳本
|
||||
|
||||
用法:
|
||||
python run_telegram_bot.py
|
||||
|
||||
環境變數:
|
||||
TELEGRAM_BOT_TOKEN: Telegram Bot Token (必填)
|
||||
|
||||
功能:
|
||||
- 啟動 Telegram Bot 監聽
|
||||
- 每日 09:00 推播趨勢摘要
|
||||
- 處理用戶指令:/trend, /search, /copy, /keywords, /daily, /settings
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, time
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 載入環境變數
|
||||
load_dotenv()
|
||||
|
||||
# 設定日誌
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('logs/telegram_bot.log', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('TelegramBot')
|
||||
|
||||
def check_dependencies():
|
||||
"""檢查必要的套件"""
|
||||
try:
|
||||
from telegram import Update
|
||||
from telegram.ext import Application
|
||||
logger.info("✅ python-telegram-bot 已安裝")
|
||||
return True
|
||||
except ImportError:
|
||||
logger.error("❌ 請安裝 python-telegram-bot: pip install python-telegram-bot")
|
||||
return False
|
||||
|
||||
def check_token():
|
||||
"""檢查 Bot Token"""
|
||||
token = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||
if not token:
|
||||
logger.error("❌ 請在 .env 設定 TELEGRAM_BOT_TOKEN")
|
||||
logger.info(" 1. 在 Telegram 搜尋 @BotFather")
|
||||
logger.info(" 2. 發送 /newbot 建立新 Bot")
|
||||
logger.info(" 3. 複製 Token 到 .env 檔案")
|
||||
return None
|
||||
logger.info("✅ Bot Token 已設定")
|
||||
return token
|
||||
|
||||
async def main():
|
||||
"""主程式"""
|
||||
print("=" * 60)
|
||||
print(" MOMO Pro System - Telegram Bot")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# 檢查依賴
|
||||
if not check_dependencies():
|
||||
sys.exit(1)
|
||||
|
||||
# 檢查 Token
|
||||
token = check_token()
|
||||
if not token:
|
||||
sys.exit(1)
|
||||
|
||||
# 導入 Bot 服務
|
||||
try:
|
||||
from services.telegram_bot_service import TelegramBotService
|
||||
logger.info("✅ TelegramBotService 已載入")
|
||||
except ImportError as e:
|
||||
logger.error(f"❌ 無法載入 TelegramBotService: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 建立 Bot 服務
|
||||
bot_service = TelegramBotService(token)
|
||||
|
||||
# 取得 Application
|
||||
app = bot_service.get_application()
|
||||
|
||||
if not app:
|
||||
logger.error("❌ 無法建立 Bot Application")
|
||||
sys.exit(1)
|
||||
|
||||
# 設定每日推播排程 (每天 09:00)
|
||||
from telegram.ext import JobQueue
|
||||
|
||||
async def daily_summary_job(context):
|
||||
"""每日摘要推播任務"""
|
||||
logger.info("📤 執行每日趨勢摘要推播...")
|
||||
await bot_service.send_daily_summary()
|
||||
|
||||
# 啟動 Bot
|
||||
logger.info("🚀 啟動 Telegram Bot...")
|
||||
logger.info(" 指令列表:")
|
||||
logger.info(" /trend [分類] - 查看熱門趨勢")
|
||||
logger.info(" /search [關鍵字] - AI 網路搜尋")
|
||||
logger.info(" /copy [商品名] - 生成行銷文案")
|
||||
logger.info(" /keywords - 熱門關鍵字")
|
||||
logger.info(" /daily - 每日趨勢摘要")
|
||||
logger.info(" /settings - 通知設定")
|
||||
print()
|
||||
|
||||
# 初始化並啟動
|
||||
await app.initialize()
|
||||
await app.start()
|
||||
|
||||
# 設定每日推播 (09:00 台北時間)
|
||||
job_queue = app.job_queue
|
||||
if job_queue:
|
||||
# 計算下一個 09:00 的時間
|
||||
target_time = time(hour=9, minute=0, second=0)
|
||||
job_queue.run_daily(daily_summary_job, time=target_time, name='daily_summary')
|
||||
logger.info("📅 已設定每日 09:00 推播趨勢摘要")
|
||||
|
||||
# 開始輪詢
|
||||
await app.updater.start_polling(drop_pending_updates=True)
|
||||
|
||||
logger.info("✅ Bot 已啟動,按 Ctrl+C 停止")
|
||||
|
||||
# 保持運行
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("🛑 收到停止信號...")
|
||||
finally:
|
||||
await app.updater.stop()
|
||||
await app.stop()
|
||||
await app.shutdown()
|
||||
logger.info("👋 Bot 已停止")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 確保 logs 目錄存在
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
|
||||
# 執行
|
||||
asyncio.run(main())
|
||||
183
scripts/tools/system_startup.sh
Normal file
183
scripts/tools/system_startup.sh
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# MOMO Pro System - 系統啟動腳本 (v3.0)
|
||||
# 重開機後自動啟動所有服務
|
||||
# 2026-02-13 更新:
|
||||
# - n8n 和 Superset 已遷移到 K8s (tools namespace)
|
||||
# - 移除 Docker n8n/Superset 啟動
|
||||
# - 新增 K8s tools namespace 重啟
|
||||
# ==========================================================
|
||||
|
||||
LOG_FILE="/var/log/momo_startup.log"
|
||||
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg}"
|
||||
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-5619078117}"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
notify() {
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
-d chat_id="${TELEGRAM_CHAT_ID}" \
|
||||
-d text="🖥️ UAT Server 啟動通知
|
||||
|
||||
$1" \
|
||||
-d parse_mode="HTML" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
wait_container() {
|
||||
local name=$1
|
||||
local max_wait=${2:-60}
|
||||
local count=0
|
||||
while [ $count -lt $max_wait ]; do
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${name}$"; then
|
||||
STATUS=$(docker inspect --format='{{.State.Status}}' "$name" 2>/dev/null)
|
||||
if [ "$STATUS" = "running" ]; then
|
||||
log "✅ $name 運行中"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
count=$((count + 2))
|
||||
done
|
||||
log "⚠️ $name 等待超時"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_k8s_pod() {
|
||||
local label=$1
|
||||
local namespace=$2
|
||||
local max_wait=${3:-120}
|
||||
|
||||
export KUBECONFIG=/home/wooo/.kube/config
|
||||
|
||||
log "等待 K8s Pod ($label) 就緒..."
|
||||
kubectl wait --for=condition=ready pod -l "$label" -n "$namespace" --timeout="${max_wait}s" 2>/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
log "✅ K8s Pod ($label) 已就緒"
|
||||
return 0
|
||||
else
|
||||
log "⚠️ K8s Pod ($label) 等待超時"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log "=========================================="
|
||||
log "🚀 系統啟動流程開始 (v3.0)"
|
||||
log "=========================================="
|
||||
|
||||
notify "🔄 UAT 系統正在啟動..."
|
||||
|
||||
# 1. 確保 Docker 運行
|
||||
log "=== 1. 檢查 Docker ==="
|
||||
if ! systemctl is-active --quiet docker; then
|
||||
systemctl start docker
|
||||
sleep 10
|
||||
fi
|
||||
log "✅ Docker 服務正常"
|
||||
|
||||
# 2. 啟動 Docker Registry
|
||||
log "=== 2. 啟動 Docker Registry ==="
|
||||
cd /home/wooo/registry 2>/dev/null || cd /home/wooo/devops/registry 2>/dev/null
|
||||
docker compose up -d 2>/dev/null || docker start docker-registry 2>/dev/null
|
||||
wait_container "docker-registry" 60
|
||||
|
||||
# 3. 啟動 GitLab
|
||||
log "=== 3. 啟動 GitLab ==="
|
||||
docker start wooo-gitlab 2>/dev/null || true
|
||||
# GitLab 啟動較慢,不等待完全就緒
|
||||
|
||||
# 4. 重啟 K8s momo namespace (主應用)
|
||||
log "=== 4. 重啟 K8s momo 應用 ==="
|
||||
export KUBECONFIG=/home/wooo/.kube/config
|
||||
kubectl rollout restart deployment/momo-app -n momo 2>/dev/null || true
|
||||
kubectl rollout restart deployment/momo-scheduler -n momo 2>/dev/null || true
|
||||
|
||||
# 5. 重啟 K8s tools namespace (n8n, Superset)
|
||||
log "=== 5. 重啟 K8s tools 服務 ==="
|
||||
kubectl rollout restart deployment/n8n -n tools 2>/dev/null || true
|
||||
kubectl rollout restart deployment/superset -n tools 2>/dev/null || true
|
||||
|
||||
# 6. 等待 K8s Pod 就緒
|
||||
log "=== 6. 等待 K8s Pod 就緒 ==="
|
||||
wait_k8s_pod "app=momo-app" "momo" 120
|
||||
wait_k8s_pod "app=n8n" "tools" 60
|
||||
wait_k8s_pod "app=superset" "tools" 90
|
||||
|
||||
# 7. 健康檢查
|
||||
log "=== 7. 健康檢查 ==="
|
||||
FAILED=""
|
||||
|
||||
# 取得 K8s ClusterIP
|
||||
N8N_IP=$(kubectl get svc n8n -n tools -o jsonpath='{.spec.clusterIP}' 2>/dev/null)
|
||||
SUPERSET_IP=$(kubectl get svc superset -n tools -o jsonpath='{.spec.clusterIP}' 2>/dev/null)
|
||||
|
||||
# Registry
|
||||
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5002/v2/ | grep -q "200"; then
|
||||
log "✅ Registry: OK"
|
||||
else
|
||||
log "❌ Registry: FAILED"
|
||||
FAILED="${FAILED}Registry, "
|
||||
fi
|
||||
|
||||
# MOMO App
|
||||
if curl -s -o /dev/null -w '%{http_code}' https://mo.wooo.work/health | grep -q "200"; then
|
||||
log "✅ MOMO App: OK"
|
||||
else
|
||||
log "❌ MOMO App: FAILED"
|
||||
FAILED="${FAILED}MOMO App, "
|
||||
fi
|
||||
|
||||
# n8n (via K8s ClusterIP)
|
||||
if [ -n "$N8N_IP" ] && curl -s -o /dev/null -w '%{http_code}' "http://${N8N_IP}:5678/healthz" | grep -q "200"; then
|
||||
log "✅ n8n (K8s): OK"
|
||||
else
|
||||
log "❌ n8n (K8s): FAILED"
|
||||
FAILED="${FAILED}n8n, "
|
||||
fi
|
||||
|
||||
# Superset (via K8s ClusterIP)
|
||||
if [ -n "$SUPERSET_IP" ] && curl -s -o /dev/null -w '%{http_code}' "http://${SUPERSET_IP}:8088/health" | grep -q "200"; then
|
||||
log "✅ Superset (K8s): OK"
|
||||
else
|
||||
log "❌ Superset (K8s): FAILED"
|
||||
FAILED="${FAILED}Superset, "
|
||||
fi
|
||||
|
||||
# GitLab (可能需要更長時間)
|
||||
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8929/ | grep -q "200\|302"; then
|
||||
log "✅ GitLab: OK"
|
||||
else
|
||||
log "⚠️ GitLab: 啟動中... (需要幾分鐘)"
|
||||
fi
|
||||
|
||||
# 結果通知
|
||||
log "=========================================="
|
||||
if [ -z "$FAILED" ]; then
|
||||
log "✅ 所有服務啟動成功"
|
||||
notify "✅ 系統啟動完成
|
||||
|
||||
所有服務狀態:
|
||||
• Registry ✅
|
||||
• MOMO App (K8s) ✅
|
||||
• n8n (K8s) ✅
|
||||
• Superset (K8s) ✅
|
||||
• GitLab ✅
|
||||
|
||||
⏰ $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
else
|
||||
log "⚠️ 部分服務異常: $FAILED"
|
||||
notify "⚠️ 系統啟動完成,但部分服務異常
|
||||
|
||||
異常服務: $FAILED
|
||||
|
||||
請檢查日誌: /var/log/momo_startup.log
|
||||
|
||||
⏰ $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
fi
|
||||
log "=========================================="
|
||||
}
|
||||
|
||||
# 執行
|
||||
main "$@"
|
||||
283
scripts/tools/system_startup_complete.sh
Normal file
283
scripts/tools/system_startup_complete.sh
Normal file
@@ -0,0 +1,283 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# WOOO TECH - MOMO Pro System 完整啟動腳本
|
||||
# 用途: 系統重開機後自動啟動所有服務
|
||||
# 版本: 2.0
|
||||
# 日期: 2026-02-06
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
LOG_FILE="/var/log/momo_startup.log"
|
||||
TELEGRAM_BOT_TOKEN="8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg"
|
||||
TELEGRAM_CHAT_ID="5619078117"
|
||||
|
||||
# 顏色輸出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 日誌函數
|
||||
log() {
|
||||
local level=$1
|
||||
shift
|
||||
local message=$@
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo -e "${timestamp} [${level}] ${message}" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_info() { log "INFO" "$@"; }
|
||||
log_warn() { log "WARN" "${YELLOW}$@${NC}"; }
|
||||
log_error() { log "ERROR" "${RED}$@${NC}"; }
|
||||
log_success() { log "SUCCESS" "${GREEN}$@${NC}"; }
|
||||
|
||||
# 發送 Telegram 通知
|
||||
send_telegram() {
|
||||
local message=$1
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
-d chat_id="${TELEGRAM_CHAT_ID}" \
|
||||
-d parse_mode="HTML" \
|
||||
-d text="${message}" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# 等待服務就緒
|
||||
wait_for_service() {
|
||||
local name=$1
|
||||
local check_cmd=$2
|
||||
local max_attempts=${3:-30}
|
||||
local attempt=1
|
||||
|
||||
log_info "等待 ${name} 就緒..."
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if eval "$check_cmd" > /dev/null 2>&1; then
|
||||
log_success "${name} 已就緒 (嘗試 ${attempt}/${max_attempts})"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
log_error "${name} 啟動失敗 (超過 ${max_attempts} 次嘗試)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# 服務啟動順序和依賴關係
|
||||
# =============================================================================
|
||||
#
|
||||
# 啟動順序圖:
|
||||
#
|
||||
# ┌──────────────┐
|
||||
# │ 1. Docker │ ← 所有容器服務的基礎
|
||||
# └──────┬───────┘
|
||||
# │
|
||||
# ┌──────▼───────┐ ┌──────────────────┐
|
||||
# │ 2. Harbor │ │ 3. K8s (K3s) │
|
||||
# │ Registry │ │ 自動啟動 │
|
||||
# └──────┬───────┘ └────────┬─────────┘
|
||||
# │ │
|
||||
# ┌──────▼───────┐ ┌────────▼─────────┐
|
||||
# │ 4. GitLab │ │ 5. K8s Services │
|
||||
# │ CI/CD │ │ - PostgreSQL │
|
||||
# └──────────────┘ │ - momo-app │
|
||||
# │ - scheduler │
|
||||
# └────────┬─────────┘
|
||||
# │
|
||||
# ┌────────────────────────────▼───────────────────────────┐
|
||||
# │ 6. 監控服務 (Docker) │
|
||||
# │ - Prometheus │
|
||||
# │ - Grafana │
|
||||
# │ - Node Exporter │
|
||||
# │ - Alertmanager │
|
||||
# └────────────────────────────┬───────────────────────────┘
|
||||
# │
|
||||
# ┌────────▼─────────┐
|
||||
# │ 7. 健康檢查 │
|
||||
# │ 發送通知 │
|
||||
# └──────────────────┘
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
main() {
|
||||
local start_time=$(date +%s)
|
||||
local errors=0
|
||||
|
||||
log_info "=========================================="
|
||||
log_info "MOMO Pro System 啟動程序開始"
|
||||
log_info "=========================================="
|
||||
|
||||
# =========================================================================
|
||||
# 1. 確認 Docker 服務
|
||||
# =========================================================================
|
||||
log_info "[1/7] 確認 Docker 服務..."
|
||||
if ! systemctl is-active --quiet docker; then
|
||||
log_warn "Docker 未運行,正在啟動..."
|
||||
systemctl start docker
|
||||
wait_for_service "Docker" "docker info" 30 || { ((errors++)); log_error "Docker 啟動失敗"; }
|
||||
else
|
||||
log_success "Docker 已運行"
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# 2. 啟動 Harbor Registry
|
||||
# =========================================================================
|
||||
log_info "[2/7] 啟動 Harbor Registry..."
|
||||
cd /home/wooo/devops/harbor/harbor
|
||||
|
||||
# 確保完全停止後再啟動
|
||||
docker compose down --remove-orphans 2>/dev/null || true
|
||||
sleep 5
|
||||
docker compose up -d
|
||||
|
||||
wait_for_service "Harbor" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5050/api/v2.0/ping | grep -q 200" 60 || {
|
||||
((errors++))
|
||||
log_error "Harbor 啟動失敗"
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# 3. 確認 K3s 服務
|
||||
# =========================================================================
|
||||
log_info "[3/7] 確認 K3s 服務..."
|
||||
if ! systemctl is-active --quiet k3s; then
|
||||
log_warn "K3s 未運行,正在啟動..."
|
||||
systemctl start k3s
|
||||
wait_for_service "K3s" "kubectl get nodes" 60 || { ((errors++)); log_error "K3s 啟動失敗"; }
|
||||
else
|
||||
log_success "K3s 已運行"
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# 4. 啟動 GitLab
|
||||
# =========================================================================
|
||||
log_info "[4/7] 啟動 GitLab..."
|
||||
if ! docker ps | grep -q gitlab; then
|
||||
docker start gitlab gitlab-runner 2>/dev/null || {
|
||||
log_warn "GitLab 容器不存在,嘗試從 compose 啟動..."
|
||||
cd /home/wooo/devops/gitlab && docker compose up -d 2>/dev/null || true
|
||||
}
|
||||
fi
|
||||
wait_for_service "GitLab" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8929 | grep -q -E '200|302'" 120 || {
|
||||
((errors++))
|
||||
log_warn "GitLab 啟動緩慢,可能需要更多時間"
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# 5. 確認 K8s 服務
|
||||
# =========================================================================
|
||||
log_info "[5/7] 確認 K8s 服務..."
|
||||
|
||||
# 等待 PostgreSQL
|
||||
log_info "等待 PostgreSQL..."
|
||||
wait_for_service "PostgreSQL" "kubectl exec momo-postgres-0 -n momo -- pg_isready -U momo -d momo_analytics" 60 || {
|
||||
log_warn "PostgreSQL 未就緒,重啟中..."
|
||||
kubectl rollout restart statefulset/momo-postgres -n momo
|
||||
sleep 30
|
||||
}
|
||||
|
||||
# 等待 momo-app
|
||||
log_info "等待 momo-app..."
|
||||
wait_for_service "momo-app" "kubectl exec -n momo deploy/momo-app -- curl -s http://localhost:80/health | grep -q healthy" 90 || {
|
||||
log_warn "momo-app 未就緒,重啟中..."
|
||||
kubectl rollout restart deployment/momo-app -n momo
|
||||
sleep 30
|
||||
}
|
||||
|
||||
# 等待 scheduler
|
||||
log_info "等待 scheduler..."
|
||||
kubectl get pods -n momo -l app=momo-scheduler | grep -q Running || {
|
||||
log_warn "scheduler 未運行,重啟中..."
|
||||
kubectl rollout restart deployment/momo-scheduler -n momo
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# 6. 啟動監控服務
|
||||
# =========================================================================
|
||||
log_info "[6/7] 啟動監控服務..."
|
||||
cd /home/wooo/monitoring
|
||||
docker compose up -d 2>/dev/null || log_warn "監控服務啟動失敗(可能已存在)"
|
||||
|
||||
# 確認 Grafana
|
||||
wait_for_service "Grafana" "curl -s -o /dev/null http://127.0.0.1:3000/login" 30 || {
|
||||
((errors++))
|
||||
log_warn "Grafana 未就緒"
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# 7. 最終健康檢查
|
||||
# =========================================================================
|
||||
log_info "[7/7] 執行最終健康檢查..."
|
||||
|
||||
local health_status=""
|
||||
|
||||
# 檢查 mo.wooo.work
|
||||
if curl -s -o /dev/null -w '%{http_code}' https://mo.wooo.work/health 2>/dev/null | grep -q 200; then
|
||||
health_status+="✅ mo.wooo.work: 正常\n"
|
||||
else
|
||||
health_status+="❌ mo.wooo.work: 異常\n"
|
||||
((errors++))
|
||||
fi
|
||||
|
||||
# 檢查 Harbor
|
||||
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5050/api/v2.0/ping 2>/dev/null | grep -q 200; then
|
||||
health_status+="✅ Harbor: 正常\n"
|
||||
else
|
||||
health_status+="❌ Harbor: 異常\n"
|
||||
((errors++))
|
||||
fi
|
||||
|
||||
# 檢查 GitLab
|
||||
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8929 2>/dev/null | grep -q -E '200|302'; then
|
||||
health_status+="✅ GitLab: 正常\n"
|
||||
else
|
||||
health_status+="⚠️ GitLab: 啟動中\n"
|
||||
fi
|
||||
|
||||
# 檢查 K8s Pods
|
||||
local pod_status=$(kubectl get pods -n momo --no-headers 2>/dev/null | awk '{print $2}')
|
||||
if echo "$pod_status" | grep -v "1/1" > /dev/null; then
|
||||
health_status+="⚠️ K8s Pods: 部分異常\n"
|
||||
else
|
||||
health_status+="✅ K8s Pods: 全部正常\n"
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# 完成
|
||||
# =========================================================================
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
log_info "=========================================="
|
||||
log_info "啟動程序完成"
|
||||
log_info "耗時: ${duration} 秒"
|
||||
log_info "錯誤數: ${errors}"
|
||||
log_info "=========================================="
|
||||
|
||||
# 發送 Telegram 通知
|
||||
local emoji="🟢"
|
||||
local status_text="成功"
|
||||
if [ $errors -gt 0 ]; then
|
||||
emoji="🟡"
|
||||
status_text="部分失敗 (${errors} 個錯誤)"
|
||||
fi
|
||||
|
||||
send_telegram "$(cat <<EOF
|
||||
${emoji} <b>MOMO Pro System 啟動通知</b>
|
||||
|
||||
📋 <b>狀態:</b> ${status_text}
|
||||
⏱️ <b>耗時:</b> ${duration} 秒
|
||||
🕐 <b>時間:</b> $(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
<b>服務健康檢查:</b>
|
||||
$(echo -e "$health_status")
|
||||
|
||||
🏷️ <i>UAT Server (192.168.0.110)</i>
|
||||
EOF
|
||||
)"
|
||||
|
||||
return $errors
|
||||
}
|
||||
|
||||
# 執行主程序
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user