ae0064a2a7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
7.3 KiB
Python
184 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Logique de déploiement SSH partagée.
|
|
Utilisée par agent2_deploy.py (XMPP) et deploy.py (CLI).
|
|
"""
|
|
|
|
import json
|
|
import io
|
|
import paramiko
|
|
from pathlib import Path
|
|
|
|
CATALOG_FILE = Path(__file__).parent / "agents_catalog.json"
|
|
|
|
|
|
def load_catalog() -> dict:
|
|
with open(CATALOG_FILE, encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _run_ssh(client: paramiko.SSHClient, cmd: str, timeout: int = 60) -> tuple[int, str, str]:
|
|
"""Exécute une commande SSH et retourne (returncode, stdout, stderr)."""
|
|
_, stdout, stderr = client.exec_command(cmd, timeout=timeout)
|
|
rc = stdout.channel.recv_exit_status()
|
|
return rc, stdout.read().decode(errors="replace").strip(), stderr.read().decode(errors="replace").strip()
|
|
|
|
|
|
def connect_ssh(host: str, user: str, password: str = None, key_path: str = None) -> paramiko.SSHClient:
|
|
"""Crée et retourne une connexion SSH authentifiée."""
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
if key_path:
|
|
client.connect(host, username=user, key_filename=key_path, timeout=15)
|
|
else:
|
|
client.connect(host, username=user, password=password, timeout=15)
|
|
return client
|
|
|
|
|
|
def write_remote_file(client: paramiko.SSHClient, remote_path: str, content: str):
|
|
"""Écrit un fichier sur la machine distante via SFTP."""
|
|
sftp = client.open_sftp()
|
|
with sftp.open(remote_path, "w") as f:
|
|
f.write(content)
|
|
sftp.close()
|
|
|
|
|
|
def deploy_agent(
|
|
host: str,
|
|
ssh_user: str,
|
|
agent_type: str,
|
|
agent_name: str,
|
|
xmpp_jid: str,
|
|
xmpp_pass: str,
|
|
mqtt_host: str,
|
|
progress_cb,
|
|
ssh_password: str = None,
|
|
ssh_key_path: str = None,
|
|
) -> tuple[bool, str]:
|
|
"""
|
|
Déploie un agent sur la machine distante.
|
|
agent_name : nom libre choisi par l'utilisateur (ex: trouducul)
|
|
progress_cb(msg) : callback appelé à chaque étape pour informer l'utilisateur.
|
|
Retourne (succès, message_final).
|
|
"""
|
|
catalog = load_catalog()
|
|
if agent_type not in catalog:
|
|
return False, "Agent inconnu : {}".format(agent_type)
|
|
|
|
agent = catalog[agent_type]
|
|
repo_url = agent["repo_url"]
|
|
main_script = agent["main_script"]
|
|
deps = " ".join(agent["dependencies"])
|
|
|
|
# Le nom libre détermine install_path, service, MQTT
|
|
install_path = "/opt/{}".format(agent_name)
|
|
service_name = agent_name
|
|
mqtt_inbox = "agents/{}/inbox".format(agent_name)
|
|
mqtt_outbox = "agents/agent1/inbox"
|
|
|
|
try:
|
|
# ── Connexion SSH ──────────────────────────────────────────────────
|
|
progress_cb("Connexion SSH à {}@{}...".format(ssh_user, host))
|
|
client = connect_ssh(host, ssh_user, password=ssh_password, key_path=ssh_key_path)
|
|
progress_cb("Connexion SSH établie.")
|
|
|
|
# ── Prérequis système ──────────────────────────────────────────────
|
|
progress_cb("Installation des prérequis système...")
|
|
rc, out, err = _run_ssh(client,
|
|
"apt-get update -qq && apt-get install -y -qq python3 python3-pip python3-venv git python3-paramiko 2>&1",
|
|
timeout=180)
|
|
if rc != 0:
|
|
return False, "Erreur prérequis : {}".format(err or out)
|
|
progress_cb("Prérequis installés.")
|
|
|
|
# ── Clone ou mise à jour du dépôt ──────────────────────────────────
|
|
progress_cb("Clonage du dépôt {}...".format(repo_url))
|
|
rc, out, err = _run_ssh(client,
|
|
"if [ -d {path}/.git ]; then git -C {path} pull; else git clone {url} {path}; fi".format(
|
|
path=install_path, url=repo_url),
|
|
timeout=120)
|
|
if rc != 0:
|
|
return False, "Erreur git : {}".format(err or out)
|
|
progress_cb("Dépôt cloné dans {}.".format(install_path))
|
|
|
|
# ── Venv et dépendances Python ─────────────────────────────────────
|
|
progress_cb("Création du venv et installation des dépendances Python...")
|
|
rc, out, err = _run_ssh(client,
|
|
"python3 -m venv {path}/venv && {path}/venv/bin/pip install -q {deps}".format(
|
|
path=install_path, deps=deps),
|
|
timeout=300)
|
|
if rc != 0:
|
|
return False, "Erreur pip : {}".format(err or out)
|
|
progress_cb("Dépendances Python installées.")
|
|
|
|
# ── Configuration ──────────────────────────────────────────────────
|
|
progress_cb("Écriture de config.json...")
|
|
config = {
|
|
"ollama_url" : "http://192.168.7.119:11434/api/chat",
|
|
"model" : "qwen3:8b",
|
|
"xmpp_jid" : xmpp_jid,
|
|
"xmpp_pass" : xmpp_pass,
|
|
"admin_jid" : "sylvain@xmpp.ovh",
|
|
"db_path" : "{}/memory.db".format(install_path),
|
|
"mqtt_host" : mqtt_host,
|
|
"mqtt_port" : 1883,
|
|
"mqtt_client_id": agent_name,
|
|
"mqtt_inbox" : mqtt_inbox,
|
|
"mqtt_outbox" : mqtt_outbox,
|
|
}
|
|
write_remote_file(client,
|
|
"{}/config/config.json".format(install_path),
|
|
json.dumps(config, indent=2, ensure_ascii=False))
|
|
progress_cb("Configuration écrite.")
|
|
|
|
# ── Service systemd ────────────────────────────────────────────────
|
|
progress_cb("Création du service systemd...")
|
|
service_content = """[Unit]
|
|
Description=Agent {name}
|
|
After=network.target mosquitto.service
|
|
Wants=mosquitto.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory={path}
|
|
ExecStart={path}/venv/bin/python3 {path}/{script}
|
|
Restart=always
|
|
RestartSec=5
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
""".format(name=agent_name, path=install_path, script=main_script)
|
|
|
|
write_remote_file(client,
|
|
"/etc/systemd/system/{}.service".format(service_name),
|
|
service_content)
|
|
|
|
rc, out, err = _run_ssh(client,
|
|
"systemctl daemon-reload && systemctl enable {s} && systemctl restart {s}".format(
|
|
s=service_name),
|
|
timeout=30)
|
|
if rc != 0:
|
|
return False, "Erreur systemd : {}".format(err or out)
|
|
progress_cb("Service {} activé et démarré.".format(service_name))
|
|
|
|
client.close()
|
|
|
|
summary = (
|
|
"Déploiement de «{}» ({}) terminé sur {} !\n"
|
|
" JID XMPP : {}\n"
|
|
" MQTT inbox : {}\n"
|
|
" Service : systemctl status {}"
|
|
).format(agent_name, agent_type, host, xmpp_jid, mqtt_inbox, service_name)
|
|
|
|
return True, summary
|
|
|
|
except paramiko.AuthenticationException:
|
|
return False, "Erreur : authentification SSH refusée."
|
|
except paramiko.NoValidConnectionsError:
|
|
return False, "Erreur : impossible de se connecter à {}.".format(host)
|
|
except Exception as e:
|
|
return False, "Erreur inattendue : {}".format(e)
|