Files
agent2_deploy/deployer.py
T
root e4ec487287 Initial commit : agent2_deploy - déploiement interactif via XMPP/SSH
- agent2_deploy.py : bot XMPP avec machine à états pour le déploiement guidé
- deployer.py : logique SSH partagée (paramiko)
- deploy.py : script CLI standalone (après git clone)
- agents_catalog.json : catalogue des agents déployables
- README.md : documentation complète

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 16:46:03 +00:00

180 lines
7.1 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,
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.
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]
install_path = agent["install_path"]
repo_url = agent["repo_url"]
service_name = agent["service_name"]
main_script = agent["main_script"]
deps = " ".join(agent["dependencies"])
mqtt_inbox = agent["mqtt_inbox"]
mqtt_outbox = agent["mqtt_outbox"]
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_type,
"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_type, 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_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)