shango/shango-daemon/shango_daemon.py

150 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""
shango_daemon.py — Moteur réseau auto-régressif de Shango
"""
import asyncio, json, hashlib, time, random, subprocess, os, sys
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
SHANGO_DIR = Path("/root/.shango")
SHANGO_DIR.mkdir(exist_ok=True)
PEERS_DB = SHANGO_DIR / "peers.json"
HEAL_LOG = SHANGO_DIR / "heal_log.jsonl"
MESH_STATE = SHANGO_DIR / "mesh_state.json"
# ── État courant du mesh ───────────────────────────────────────────
class ShangoNode:
def __init__(self, node_id: str = None):
self.node_id = node_id or self._generate_id()
self.peers: Dict[str, dict] = {}
self.health_score = 1.0
self.last_seen = time.time()
self.load()
def _generate_id(self) -> str:
host = subprocess.getoutput("hostname").strip()
ts = str(time.time())
return hashlib.blake2b(f"{host}:{ts}".encode(), digest_size=8).hexdigest()
def load(self):
if PEERS_DB.exists():
self.peers = json.load(open(PEERS_DB))
def save(self):
json.dump(self.peers, open(PEERS_DB, "w"), indent=2)
json.dump({
"node_id": self.node_id,
"health": self.health_score,
"last_seen": self.last_seen,
"peers_count": len(self.peers)
}, open(MESH_STATE, "w"), indent=2)
def discover_tailscale_peers(self) -> List[dict]:
"""Scan les devices Tailscale actuels et les convertit en peers Shango."""
try:
out = subprocess.check_output(["tailscale", "status", "--json"], text=True, timeout=10)
data = json.loads(out)
peers = []
for peer in data.get("Peer", []):
peers.append({
"id": peer.get("HostName", "unknown"),
"ts_ip": peer.get("TailAddr", "unknown"),
"online": peer.get("Online", False),
"last_write": peer.get("LastWrite", "never"),
"shango_enhanced": False, # upgrade possible
})
return peers
except Exception as e:
return [{"error": str(e)}]
def heal(self) -> dict:
"""Auto-régression : détecte pannes, tente fix, log."""
fixes = []
# Test connectivité
try:
subprocess.check_call(["tailscale", "status"], stdout=subprocess.DEVNULL, timeout=5)
fixes.append({"check": "tailscale_status", "status": "ok"})
except:
fixes.append({"check": "tailscale_status", "status": "down", "action": "restart_tailscaled"})
subprocess.call(["systemctl", "restart", "tailscaled"])
# Test DERP relay
try:
out = subprocess.check_output(["tailscale", "netcheck", "--json"], text=True, timeout=10)
netcheck = json.loads(out)
if netcheck.get("UDP"):
fixes.append({"check": "udp_direct", "status": "ok"})
else:
fixes.append({"check": "udp_direct", "status": "relayed", "action": "warn_user"})
except:
fixes.append({"check": "netcheck", "status": "failed"})
# Log
entry = {"time": datetime.now().isoformat(), "node": self.node_id, "fixes": fixes}
with open(HEAL_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
self.health_score = sum(1 for f in fixes if f.get("status") == "ok") / len(fixes) if fixes else 0
self.save()
return entry
def gossip(self, target_peer: str, message: dict) -> bool:
"""Envoie un message CRDT à un peer via Tailscale IP."""
# Placeholder — à implémenter avec UDP ou QUIC
print(f"[GOSSIP] {target_peer}: {json.dumps(message, indent=2)}")
return True
def singularize(self, services: List[str]) -> dict:
"""Unifie les identités Hermes / Odoo / Tailscale / ivoire-monade.shop."""
identity = {
"node_id": self.node_id,
"tailscale_ips": [p.get("ts_ip") for p in self.discover_tailscale_peers()],
"services": {},
"domain": "ivoire-monade.shop",
"wildcard_routes": {}
}
for svc in services:
identity["services"][svc] = {
"status": "unknown",
"health": 0.0,
"url": f"https://{svc}.ivoire-monade.shop"
}
self.save()
return identity
# ── CLI ────────────────────────────────────────────────────────────
def main():
import argparse
parser = argparse.ArgumentParser(prog="shango-daemon")
parser.add_argument("command", choices=["status", "heal", "discover", "singularize"])
parser.add_argument("--services", default="hermes,erp,ai,status")
args = parser.parse_args()
node = ShangoNode()
if args.command == "status":
print(json.dumps({
"node_id": node.node_id,
"health": node.health_score,
"peers": len(node.peers),
"tailscale": node.discover_tailscale_peers()
}, indent=2))
elif args.command == "heal":
result = node.heal()
print(json.dumps(result, indent=2))
elif args.command == "discover":
peers = node.discover_tailscale_peers()
print(json.dumps(peers, indent=2))
elif args.command == "singularize":
svcs = args.services.split(",")
result = node.singularize(svcs)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()