150 lines
5.6 KiB
Python
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()
|