#!/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()