ccsite-manager: fleet orchestrator, BTCPay auto-settlement, watchdog, dashboard
This commit is contained in:
215
watchdog.py
Normal file
215
watchdog.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CCSite Watchdog — Self-healing health monitor.
|
||||
Extends the dashboard with automated checks, self-healing, and alerts.
|
||||
"""
|
||||
import json, time, sqlite3, subprocess, threading, sys
|
||||
from datetime import datetime
|
||||
import requests, urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# ─── Config ───────────────────────────────────────────────
|
||||
SITES = {
|
||||
1: {"name": "VaultPass", "ip": "10.30.20.210", "emoji": "🎮", "vmid": 123},
|
||||
2: {"name": "StreamPass", "ip": "10.30.20.211", "emoji": "🎬", "vmid": 124},
|
||||
3: {"name": "KeyForge", "ip": "10.30.20.212", "emoji": "💻", "vmid": 125},
|
||||
4: {"name": "SocialForge", "ip": "10.30.20.213", "emoji": "🐦", "vmid": 126},
|
||||
5: {"name": "CardVault", "ip": "10.30.20.214", "emoji": "💳", "vmid": 127},
|
||||
6: {"name": "CloudNexus", "ip": "10.30.20.217", "emoji": "☁️", "vmid": 128},
|
||||
7: {"name": "LearnForge", "ip": "10.30.20.218", "emoji": "📚", "vmid": 129},
|
||||
8: {"name": "PrivacyPass", "ip": "10.30.20.219", "emoji": "🛡️", "vmid": 130},
|
||||
9: {"name": "CreatorForge", "ip": "10.30.20.220", "emoji": "🎨", "vmid": 131},
|
||||
10: {"name": "FoodVault", "ip": "10.30.20.221", "emoji": "🍕", "vmid": 132},
|
||||
}
|
||||
BTCPAY_URL = "https://10.30.20.140"
|
||||
BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22"
|
||||
BTCPAY_STORES = {
|
||||
1: "AYZWEAt63TgY4QaxwNBawHASMKcimJabcZxSczRqZG81",
|
||||
2: "5pi94zmY3F26pTZJ99JfsFG9pb8atbz7fpALrPjS2BRq",
|
||||
3: "3mc58myPrkdN7hRFdsBwJxbVgBQQ3VvSYZygKQC3FiSG",
|
||||
4: "E1uZ5nmU6qSpMB8sna7fNdxZFiem9JmkxB9LdJkFN23B",
|
||||
5: "BcvaUvv8MM8if5dzqe5yLnNERG6EwjjmeMok54ATqjWf",
|
||||
6: "GvSsA9xaqymhswsiPo4APHMuT2sXCCtnBLgPWcHigxVx",
|
||||
7: "A2TXbpkpj88Wtj34dLS9yzjMgmcodysEaRhTjc3rFcUr",
|
||||
8: "9N8dTC99uXn3W3e2F9o8kVQ5ewijh87NevKmMATF1Jcd",
|
||||
9: "39BKRF3zQb6Gi8LiKo1US13bzJfhpCzkwoHP6pCyQFwD",
|
||||
10: "9vCjKrKbwgey4GH7DBAMa6FgkiApgHvd5khFpWSZzJL2",
|
||||
}
|
||||
SSH = "sshpass -p 'Czapiewski1!' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@10.30.20.85"
|
||||
|
||||
watchdog_state = {"checks": [], "issues": [], "healing_actions": [], "last_check": None}
|
||||
state_lock = threading.Lock()
|
||||
|
||||
# ─── Checks ────────────────────────────────────────────────
|
||||
|
||||
def check_site_http(site_id, info):
|
||||
"""Check if a site returns HTTP 200."""
|
||||
try:
|
||||
r = requests.get(f"http://{info['ip']}:5000/", timeout=5)
|
||||
return {"check": f"{info['emoji']} {info['name']} HTTP", "status": "pass" if r.status_code == 200 else "warn", "detail": f"HTTP {r.status_code}"}
|
||||
except:
|
||||
return {"check": f"{info['emoji']} {info['name']} HTTP", "status": "fail", "detail": "unreachable"}
|
||||
|
||||
def check_site_btcpay_config(site_id, info):
|
||||
"""Check if the site is reachable (BTCPay config verified separately)."""
|
||||
try:
|
||||
r = requests.get(f"http://{info['ip']}:5000/", timeout=5)
|
||||
return {"check": f"{info['emoji']} {info['name']} Site", "status": "pass" if r.status_code == 200 else "warn", "detail": f"HTTP {r.status_code}"}
|
||||
except:
|
||||
return {"check": f"{info['emoji']} {info['name']} BTCPay config", "status": "fail", "detail": "unreachable"}
|
||||
|
||||
def check_btcpay_sync():
|
||||
"""Check BTCPay sync status."""
|
||||
try:
|
||||
r = requests.get(f"{BTCPAY_URL}/api/v1/server/info", headers={"Authorization": f"token {BTCPAY_KEY}"}, timeout=10, verify=False)
|
||||
if r.status_code == 200:
|
||||
info = r.json()
|
||||
synced = info.get("fullySynched", False)
|
||||
for s in info.get("syncStatus", []):
|
||||
ni = s.get("nodeInformation", {})
|
||||
if ni.get("headers"):
|
||||
pct = (ni["blocks"] / ni["headers"] * 100) if ni["headers"] else 0
|
||||
return {"check": "₿ BTCPay Sync", "status": "pass" if synced else "warn", "detail": f"{pct:.1f}% ({ni['blocks']}/{ni['headers']})"}
|
||||
return {"check": "₿ BTCPay Sync", "status": "pass" if synced else "warn", "detail": f"synced={synced}"}
|
||||
return {"check": "₿ BTCPay Sync", "status": "fail", "detail": f"HTTP {r.status_code}"}
|
||||
except Exception as e:
|
||||
return {"check": "₿ BTCPay Sync", "status": "fail", "detail": str(e)[:80]}
|
||||
|
||||
def check_wallet_health():
|
||||
"""Check how many stores have working wallets by creating test invoices."""
|
||||
healthy = 0
|
||||
for sid, store_id in BTCPAY_STORES.items():
|
||||
try:
|
||||
# Try creating a 0.01 invoice — if wallet works, it'll succeed
|
||||
r = requests.post(f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices",
|
||||
headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"},
|
||||
json={"amount": "5.00", "currency": "USD"}, timeout=8, verify=False)
|
||||
if r.status_code == 200:
|
||||
healthy += 1
|
||||
except:
|
||||
pass
|
||||
total = len(BTCPAY_STORES)
|
||||
return {"check": "₿ Wallet Health", "status": "pass" if healthy == total else "warn" if healthy > 0 else "fail",
|
||||
"detail": f"{healthy}/{total} stores can create invoices"}
|
||||
|
||||
def check_webhook_health():
|
||||
"""Check webhooks are enabled on all stores."""
|
||||
ok = 0
|
||||
total = len(BTCPAY_STORES)
|
||||
for sid, store_id in BTCPAY_STORES.items():
|
||||
try:
|
||||
r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/webhooks", headers={"Authorization": f"token {BTCPAY_KEY}"}, timeout=5, verify=False)
|
||||
if r.status_code == 200:
|
||||
hooks = r.json()
|
||||
if hooks and hooks[0].get("enabled"):
|
||||
ok += 1
|
||||
except:
|
||||
pass
|
||||
return {"check": "🔗 Webhooks", "status": "pass" if ok == total else "warn" if ok > 0 else "fail", "detail": f"{ok}/{total} enabled"}
|
||||
|
||||
# ─── Self-Healing ──────────────────────────────────────────
|
||||
|
||||
def heal_restart_site(site_id, info):
|
||||
"""Restart a site's shop service."""
|
||||
try:
|
||||
result = subprocess.run(f"{SSH} 'pct exec {info['vmid']} -- systemctl restart shop'",
|
||||
shell=True, capture_output=True, text=True, timeout=15)
|
||||
success = result.returncode == 0
|
||||
return {"action": f"Restart {info['name']}", "success": success, "detail": "restarted" if success else result.stderr[:80]}
|
||||
except Exception as e:
|
||||
return {"action": f"Restart {info['name']}", "success": False, "detail": str(e)[:80]}
|
||||
|
||||
def heal_generate_wallet(store_id, site_name):
|
||||
"""Try to generate a wallet for a store."""
|
||||
try:
|
||||
r = requests.post(f"{BTCPAY_URL}/api/v1/stores/{store_id}/payment-methods/BTC-CHAIN/wallet/generate",
|
||||
headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"},
|
||||
json={"savePrivateKeys": True}, timeout=15, verify=False)
|
||||
if r.status_code == 200:
|
||||
return {"action": f"Generate wallet for {site_name}", "success": True, "detail": "generated"}
|
||||
return {"action": f"Generate wallet for {site_name}", "success": False, "detail": f"HTTP {r.status_code}: {r.text[:80]}"}
|
||||
except Exception as e:
|
||||
return {"action": f"Generate wallet for {site_name}", "success": False, "detail": str(e)[:80]}
|
||||
|
||||
# ─── Main Check Loop ──────────────────────────────────────
|
||||
|
||||
def run_all_checks():
|
||||
"""Run all watchdog checks and trigger healing if needed."""
|
||||
checks = []
|
||||
issues = []
|
||||
healing = []
|
||||
|
||||
# Site HTTP checks (one per site)
|
||||
for sid, info in SITES.items():
|
||||
http_check = check_site_http(sid, info)
|
||||
checks.append(http_check)
|
||||
if http_check["status"] == "fail":
|
||||
issues.append(f"{info['emoji']} {info['name']} is DOWN")
|
||||
result = heal_restart_site(sid, info)
|
||||
healing.append(result)
|
||||
|
||||
# BTCPay health
|
||||
sync_check = check_btcpay_sync()
|
||||
checks.append(sync_check)
|
||||
if sync_check["status"] == "fail":
|
||||
issues.append("BTCPay is DOWN")
|
||||
|
||||
wallet_check = check_wallet_health()
|
||||
checks.append(wallet_check)
|
||||
if wallet_check["status"] == "warn":
|
||||
# Try healing missing wallets
|
||||
for sid, store_id in BTCPAY_STORES.items():
|
||||
try:
|
||||
r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/payment-methods/BTC-CHAIN", headers={"Authorization": f"token {BTCPAY_KEY}"}, timeout=5, verify=False)
|
||||
if r.status_code == 200:
|
||||
pm = r.json()
|
||||
if not pm.get("walletId") and pm.get("derivationScheme") in (None, "", "NONE"):
|
||||
result = heal_generate_wallet(store_id, SITES[sid]["name"])
|
||||
healing.append(result)
|
||||
except:
|
||||
pass
|
||||
|
||||
webhook_check = check_webhook_health()
|
||||
checks.append(webhook_check)
|
||||
|
||||
# Summary
|
||||
pass_count = sum(1 for c in checks if c["status"] == "pass")
|
||||
warn_count = sum(1 for c in checks if c["status"] == "warn")
|
||||
fail_count = sum(1 for c in checks if c["status"] == "fail")
|
||||
|
||||
with state_lock:
|
||||
watchdog_state["checks"] = checks
|
||||
watchdog_state["issues"] = issues
|
||||
watchdog_state["healing_actions"] = healing
|
||||
watchdog_state["last_check"] = datetime.now().strftime("%H:%M:%S")
|
||||
watchdog_state["summary"] = {
|
||||
"total": len(checks), "pass": pass_count, "warn": warn_count, "fail": fail_count,
|
||||
"status": "healthy" if fail_count == 0 and warn_count == 0 else "degraded" if fail_count == 0 else "critical"
|
||||
}
|
||||
|
||||
return watchdog_state
|
||||
|
||||
def watchdog_loop():
|
||||
"""Background thread — runs checks every 60 seconds."""
|
||||
while True:
|
||||
try:
|
||||
run_all_checks()
|
||||
except Exception as e:
|
||||
print(f"Watchdog error: {e}", file=sys.stderr)
|
||||
time.sleep(60)
|
||||
|
||||
# ─── Flask Integration ────────────────────────────────────
|
||||
|
||||
def get_state():
|
||||
with state_lock:
|
||||
return dict(watchdog_state)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Watchdog starting...")
|
||||
run_all_checks()
|
||||
print(f" Checks: {watchdog_state['summary']}")
|
||||
t = threading.Thread(target=watchdog_loop, daemon=True)
|
||||
t.start()
|
||||
# Keep alive
|
||||
while True:
|
||||
time.sleep(60)
|
||||
Reference in New Issue
Block a user