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