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>
This commit is contained in:
root
2026-03-07 16:46:03 +00:00
commit e4ec487287
6 changed files with 968 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
# Agent2_Deploy — Agent de déploiement
Agent spécialisé dans le déploiement d'autres agents sur des machines distantes ou locales. Interactif via XMPP, il guide l'utilisateur étape par étape pour déployer un agent via SSH, puis notifie agent1 du nouveau déploiement.
## Rôle
- Déployer des agents spécialisés sur n'importe quelle machine via SSH
- Guider l'utilisateur interactivement (choix de l'agent, IP, credentials SSH, config XMPP/MQTT)
- Cloner automatiquement le dépôt git de l'agent cible
- Créer le service systemd sur la machine distante
- Notifier agent1 du nouveau déploiement (via MQTT)
## Agents déployables
Définis dans `agents_catalog.json` :
| Agent | Description |
|---|---|
| `agent2_debian13` | Spécialiste Debian (apt, systemd, docker...) |
| `agent2_ansible` | Automatisation Ansible (playbooks, ad-hoc...) |
| `agent2_deploy` | Agent de déploiement (ce même agent) |
## Utilisation via XMPP
Contacter `agent2_deploy@xmpp.ovh` via XMPP :
```
!deploy # Démarrer un déploiement guidé
!agents # Lister les agents disponibles
!annuler # Annuler le déploiement en cours
!help # Aide
```
### Déroulement du déploiement guidé
1. Choix de l'agent à déployer
2. Adresse IP de la machine cible
3. Nom d'utilisateur SSH
4. Authentification : mot de passe ou clé SSH
5. JID XMPP pour l'instance déployée
6. Mot de passe XMPP
7. Adresse du broker MQTT
8. Confirmation et déploiement
## Utilisation en mode CLI (depuis git clone)
Cloner ce dépôt sur n'importe quelle machine et lancer le script :
```bash
# Déploiement SSH vers une machine distante
git clone https://git.piaf.im/sylvain/agent2_deploy.git
cd agent2_deploy
python3 deploy.py
# Installation locale (sur la machine cible elle-même)
python3 deploy.py --local agent2_debian13
python3 deploy.py --local agent2_ansible
```
## Déploiement de agent2_deploy lui-même
### Prérequis sur la machine hôte
```bash
apt-get install -y python3 python3-venv python3-paramiko git mosquitto
```
### Installation manuelle
```bash
git clone https://git.piaf.im/sylvain/agent2_deploy.git /opt/agent2_deploy
cd /opt/agent2_deploy
# Configurer
nano config/config.json
# Service systemd
cat > /etc/systemd/system/agent2_deploy.service << 'EOF'
[Unit]
Description=Agent2 Deploy
After=network.target mosquitto.service agent.service
Wants=mosquitto.service
[Service]
Type=simple
WorkingDirectory=/opt/agent2_deploy
ExecStart=/usr/bin/python3 /opt/agent2_deploy/agent2_deploy.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable agent2_deploy
systemctl start agent2_deploy
```
## Configuration (`config/config.json`)
```json
{
"xmpp_jid" : "agent2_deploy@xmpp.ovh",
"xmpp_pass" : "<mot_de_passe>",
"admin_jid" : "sylvain@xmpp.ovh",
"mqtt_host" : "localhost",
"mqtt_port" : 1883,
"mqtt_client_id": "agent2_deploy",
"mqtt_inbox" : "agents/agent2_deploy/inbox",
"mqtt_outbox" : "agents/agent1/inbox",
"agent1_inbox" : "agents/agent1/inbox"
}
```
## Prérequis sur les machines cibles
La machine sur laquelle déployer doit être accessible via SSH. Le script installe automatiquement :
- `python3`, `python3-pip`, `python3-venv`, `git`
- Les dépendances Python de l'agent (via pip dans le venv)
L'utilisateur SSH doit avoir les droits `sudo` ou être `root` (pour systemd et apt).
## Ajouter un nouvel agent au catalogue
Editer `agents_catalog.json` :
```json
{
"agent2_monagent": {
"description" : "Description de l'agent",
"repo_url" : "https://git.piaf.im/sylvain/agent2_monagent.git",
"install_path" : "/opt/agent2_monagent",
"service_name" : "agent2_monagent",
"main_script" : "agent2_monagent.py",
"mqtt_inbox" : "agents/agent2_monagent/inbox",
"mqtt_outbox" : "agents/agent1/inbox",
"dependencies" : ["slixmpp", "paho-mqtt", "requests"]
}
}
```
## Logs
```bash
journalctl -u agent2_deploy -f
```
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Agent2_Deploy — Agent de déploiement interactif via XMPP.
Interaction guidée (machine à états) déclenchée par "!deploy".
L'agent pose les questions nécessaires, déploie via SSH,
puis notifie agent1 du nouvel agent via MQTT.
"""
import asyncio
import json
import threading
from pathlib import Path
from slixmpp import ClientXMPP
import paho.mqtt.client as mqtt
from deployer import deploy_agent, load_catalog
# ── CONFIG ────────────────────────────────────────────────────────────────
CONFIG_FILE = Path("/opt/agent2_deploy/config/config.json")
def load_config():
with open(CONFIG_FILE, encoding="utf-8") as f:
return json.load(f)
cfg = load_config()
XMPP_JID = cfg["xmpp_jid"]
XMPP_PASS = cfg["xmpp_pass"]
ADMIN_JID = cfg["admin_jid"]
MQTT_HOST = cfg["mqtt_host"]
MQTT_PORT = int(cfg["mqtt_port"])
MQTT_CLIENT = cfg["mqtt_client_id"]
AGENT1_INBOX = cfg["agent1_inbox"]
# ── MQTT ──────────────────────────────────────────────────────────────────
_mqtt_pub = None
def mqtt_publish(topic: str, message: str):
if _mqtt_pub:
_mqtt_pub.publish(topic, message)
def start_mqtt():
global _mqtt_pub
_mqtt_pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=MQTT_CLIENT + "_pub")
_mqtt_pub.connect(MQTT_HOST, MQTT_PORT)
_mqtt_pub.loop_start()
def notify_agent1(agent_type: str, host: str, xmpp_jid: str, mqtt_inbox: str):
"""Informe agent1 du nouveau déploiement."""
msg = (
"[DEPLOY] Nouvel agent déployé.\n"
" Type : {}\n"
" Hôte : {}\n"
" XMPP JID : {}\n"
" MQTT : {}"
).format(agent_type, host, xmpp_jid, mqtt_inbox)
mqtt_publish(AGENT1_INBOX, msg)
# ── MACHINE À ÉTATS ───────────────────────────────────────────────────────
# sessions[jid] = dict avec toutes les infos collectées + état courant
STEPS = [
"choose_agent",
"enter_ip",
"enter_user",
"enter_auth",
"enter_credential",
"enter_xmpp_jid",
"enter_xmpp_pass",
"enter_mqtt_host",
"confirm",
]
sessions: dict = {}
def list_agents() -> str:
catalog = load_catalog()
lines = ["Agents disponibles :"]
for i, (name, info) in enumerate(catalog.items(), 1):
lines.append(" {}. {}{}".format(i, name, info["description"]))
lines.append("\nTapez le numéro ou le nom de l'agent à déployer.")
return "\n".join(lines)
def start_session(jid: str) -> str:
sessions[jid] = {
"state" : "choose_agent",
"agent_type" : None,
"target_ip" : None,
"ssh_user" : None,
"auth_type" : None, # "password" ou "key"
"ssh_password": None,
"ssh_key_path": None,
"xmpp_jid" : None,
"xmpp_pass" : None,
"mqtt_host" : None,
}
return "Déploiement d'agent démarré. Tapez !annuler pour abandonner.\n\n" + list_agents()
def handle_session(jid: str, text: str) -> str | None:
"""
Gère un message dans le contexte d'une session de déploiement.
Retourne la réponse à envoyer, ou None si hors session.
"""
session = sessions.get(jid)
if session is None:
return None
text = text.strip()
if text.lower() in ("!annuler", "annuler", "cancel"):
del sessions[jid]
return "Déploiement annulé."
state = session["state"]
catalog = load_catalog()
# ── choose_agent ──────────────────────────────────────────────────────
if state == "choose_agent":
agents = list(catalog.keys())
# Accepte numéro ou nom
if text.isdigit():
idx = int(text) - 1
if 0 <= idx < len(agents):
session["agent_type"] = agents[idx]
else:
return "Numéro invalide. Choisissez entre 1 et {}.".format(len(agents))
elif text in catalog:
session["agent_type"] = text
else:
return "Agent inconnu. Tapez le numéro ou le nom exact.\n\n" + list_agents()
session["state"] = "enter_ip"
return "Agent choisi : {}.\n\nAdresse IP de la machine cible ?".format(session["agent_type"])
# ── enter_ip ──────────────────────────────────────────────────────────
elif state == "enter_ip":
session["target_ip"] = text
session["state"] = "enter_user"
return "Nom d'utilisateur SSH sur {} ?".format(text)
# ── enter_user ────────────────────────────────────────────────────────
elif state == "enter_user":
session["ssh_user"] = text
session["state"] = "enter_auth"
return (
"Méthode d'authentification SSH ?\n"
" 1. Mot de passe\n"
" 2. Clé SSH (chemin local)"
)
# ── enter_auth ────────────────────────────────────────────────────────
elif state == "enter_auth":
if text in ("1", "mot de passe", "password"):
session["auth_type"] = "password"
session["state"] = "enter_credential"
return "Mot de passe SSH pour {}@{} :".format(
session["ssh_user"], session["target_ip"])
elif text in ("2", "clé", "clé ssh", "key"):
session["auth_type"] = "key"
session["state"] = "enter_credential"
return "Chemin local de la clé SSH (ex: /root/.ssh/id_rsa) :"
else:
return "Répondez 1 (mot de passe) ou 2 (clé SSH)."
# ── enter_credential ─────────────────────────────────────────────────
elif state == "enter_credential":
if session["auth_type"] == "password":
session["ssh_password"] = text
else:
session["ssh_key_path"] = text
session["state"] = "enter_xmpp_jid"
agent_type = session["agent_type"]
return (
"JID XMPP pour cet agent sur la machine distante ?\n"
"(ex: {}@xmpp.ovh)".format(agent_type)
)
# ── enter_xmpp_jid ───────────────────────────────────────────────────
elif state == "enter_xmpp_jid":
session["xmpp_jid"] = text
session["state"] = "enter_xmpp_pass"
return "Mot de passe XMPP pour {} :".format(text)
# ── enter_xmpp_pass ──────────────────────────────────────────────────
elif state == "enter_xmpp_pass":
session["xmpp_pass"] = text
session["state"] = "enter_mqtt_host"
return (
"Adresse du broker MQTT pour cet agent ?\n"
"(laisser vide pour utiliser l'IP de la machine distante : {})".format(
session["target_ip"])
)
# ── enter_mqtt_host ──────────────────────────────────────────────────
elif state == "enter_mqtt_host":
session["mqtt_host"] = text if text else session["target_ip"]
session["state"] = "confirm"
s = session
agent_info = catalog[s["agent_type"]]
return (
"Récapitulatif du déploiement :\n"
" Agent : {agent_type}\n"
" Repo : {repo}\n"
" Machine : {ssh_user}@{target_ip}\n"
" Auth SSH : {auth}\n"
" XMPP JID : {xmpp_jid}\n"
" MQTT broker: {mqtt_host}\n"
" MQTT inbox : {mqtt_inbox}\n\n"
"Confirmer le déploiement ? (oui / non)"
).format(
agent_type=s["agent_type"],
repo=agent_info["repo_url"],
ssh_user=s["ssh_user"],
target_ip=s["target_ip"],
auth="clé SSH" if s["auth_type"] == "key" else "mot de passe",
xmpp_jid=s["xmpp_jid"],
mqtt_host=s["mqtt_host"],
mqtt_inbox=agent_info["mqtt_inbox"],
)
# ── confirm ───────────────────────────────────────────────────────────
elif state == "confirm":
if text.lower() in ("oui", "o", "yes", "y"):
session["state"] = "deploying"
return None # Signal pour lancer le déploiement en arrière-plan
else:
del sessions[jid]
return "Déploiement annulé."
return "État inconnu. Tapez !annuler pour recommencer."
# ── BOT XMPP ─────────────────────────────────────────────────────────────
class DeployBot(ClientXMPP):
def __init__(self):
ClientXMPP.__init__(self, XMPP_JID, XMPP_PASS)
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.on_message)
self.register_plugin('xep_0030')
self.register_plugin('xep_0199')
def reply(self, jid: str, body: str):
self.send_message(mto=jid, mbody=body, mtype='chat')
async def session_start(self, event):
self.send_presence()
await self.get_roster()
self.reply(ADMIN_JID, (
"Agent2_Deploy en ligne !\n"
"Tapez !deploy pour déployer un agent via SSH.\n"
"Tapez !agents pour voir les agents disponibles."
))
async def on_message(self, msg):
if msg['type'] not in ('chat', 'normal'):
return
jid = str(msg['from']).split('/')[0]
if jid != ADMIN_JID:
return
text = msg['body'].strip()
# Commandes globales
if text == "!agents":
self.reply(jid, list_agents())
return
if text == "!deploy":
self.reply(jid, start_session(jid))
return
if text == "!help":
self.reply(jid, (
"Commandes disponibles :\n"
" !deploy — Démarrer un déploiement guidé\n"
" !agents — Lister les agents déployables\n"
" !annuler — Annuler le déploiement en cours\n"
" !help — Afficher cette aide"
))
return
# Session de déploiement en cours ?
if jid in sessions:
reply = handle_session(jid, text)
if reply is not None:
self.reply(jid, reply)
else:
# reply=None signifie : lancer le déploiement
session = sessions[jid]
self.reply(jid, "Déploiement en cours... Cela peut prendre plusieurs minutes.")
def run_deploy():
def progress(msg):
self.reply(jid, "[{}]".format(msg))
catalog = load_catalog()
success, result = deploy_agent(
host = session["target_ip"],
ssh_user = session["ssh_user"],
agent_type = session["agent_type"],
xmpp_jid = session["xmpp_jid"],
xmpp_pass = session["xmpp_pass"],
mqtt_host = session["mqtt_host"],
progress_cb = progress,
ssh_password= session.get("ssh_password"),
ssh_key_path= session.get("ssh_key_path"),
)
if success:
mqtt_inbox = catalog[session["agent_type"]]["mqtt_inbox"]
notify_agent1(
agent_type = session["agent_type"],
host = session["target_ip"],
xmpp_jid = session["xmpp_jid"],
mqtt_inbox = mqtt_inbox,
)
self.reply(jid, result)
else:
self.reply(jid, "Déploiement échoué : " + result)
if jid in sessions:
del sessions[jid]
threading.Thread(target=run_deploy, daemon=True).start()
return
# Hors session → aide basique
self.reply(jid, (
"Je suis agent2_deploy, spécialisé dans le déploiement d'agents.\n"
"Tapez !deploy pour déployer un agent, ou !help pour l'aide."
))
# ── MAIN ─────────────────────────────────────────────────────────────────
if __name__ == "__main__":
mqtt_thread = threading.Thread(target=start_mqtt, daemon=True)
mqtt_thread.start()
bot = DeployBot()
bot.connect()
bot.loop.run_forever()
+32
View File
@@ -0,0 +1,32 @@
{
"agent2_debian13": {
"description" : "Spécialiste Debian : apt, systemd, conteneurs LXC/Docker, KVM, réseau, sécurité",
"repo_url" : "https://git.piaf.im/sylvain/agent2_debian13.git",
"install_path" : "/opt/agent2_debian13",
"service_name" : "agent2_debian13",
"main_script" : "agent2_debian13.py",
"mqtt_inbox" : "agents/agent2_debian13/inbox",
"mqtt_outbox" : "agents/agent1/inbox",
"dependencies" : ["slixmpp", "paho-mqtt", "requests", "ddgs", "beautifulsoup4", "chromadb"]
},
"agent2_ansible": {
"description" : "Automatisation Ansible : playbooks, commandes ad-hoc, déploiement multi-hôtes",
"repo_url" : "https://git.piaf.im/sylvain/agent2_ansible.git",
"install_path" : "/opt/agent2_ansible",
"service_name" : "agent2_ansible",
"main_script" : "agent2_ansible.py",
"mqtt_inbox" : "agents/agent2_ansible/inbox",
"mqtt_outbox" : "agents/agent1/inbox",
"dependencies" : ["slixmpp", "paho-mqtt", "requests", "ddgs", "beautifulsoup4", "chromadb"]
},
"agent2_deploy": {
"description" : "Agent de déploiement : installe et configure d'autres agents via SSH",
"repo_url" : "https://git.piaf.im/sylvain/agent2_deploy.git",
"install_path" : "/opt/agent2_deploy",
"service_name" : "agent2_deploy",
"main_script" : "agent2_deploy.py",
"mqtt_inbox" : "agents/agent2_deploy/inbox",
"mqtt_outbox" : "agents/agent1/inbox",
"dependencies" : ["slixmpp", "paho-mqtt", "requests"]
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"xmpp_jid" : "agent2_deploy@xmpp.ovh",
"xmpp_pass" : "Matador3721",
"admin_jid" : "sylvain@xmpp.ovh",
"mqtt_host" : "localhost",
"mqtt_port" : 1883,
"mqtt_client_id": "agent2_deploy",
"mqtt_inbox" : "agents/agent2_deploy/inbox",
"mqtt_outbox" : "agents/agent1/inbox",
"agent1_inbox" : "agents/agent1/inbox",
"ollama_url" : "http://192.168.7.119:11434/api/chat",
"model" : "qwen3:8b"
}
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de déploiement CLI standalone.
Utilisable après un git clone, sans avoir besoin d'un agent XMPP actif.
Usage :
python3 deploy.py # déploiement interactif via SSH
python3 deploy.py --local <agent> # installe l'agent sur la machine locale
"""
import sys
import argparse
import getpass
import json
from pathlib import Path
# Ajouter le répertoire courant au path pour importer deployer
sys.path.insert(0, str(Path(__file__).parent))
from deployer import deploy_agent, load_catalog
COLORS = {
"reset" : "\033[0m",
"bold" : "\033[1m",
"green" : "\033[32m",
"yellow": "\033[33m",
"red" : "\033[31m",
"cyan" : "\033[36m",
}
def c(color: str, text: str) -> str:
return COLORS.get(color, "") + text + COLORS["reset"]
def print_header():
print(c("bold", "\n=== Agent Deploy — Déploiement interactif ===\n"))
def choose_agent(catalog: dict) -> str:
print(c("cyan", "Agents disponibles :"))
agents = list(catalog.items())
for i, (name, info) in enumerate(agents, 1):
print(" {}. {}{}".format(i, c("bold", name), info["description"]))
print()
while True:
choice = input("Choisissez un agent (numéro ou nom) : ").strip()
if choice.isdigit():
idx = int(choice) - 1
if 0 <= idx < len(agents):
return agents[idx][0]
elif choice in catalog:
return choice
print(c("red", "Choix invalide, réessayez."))
def collect_ssh_info() -> dict:
print(c("cyan", "\nInformations SSH :"))
host = input(" Adresse IP de la machine cible : ").strip()
user = input(" Nom d'utilisateur SSH : ").strip()
print(" Authentification :")
print(" 1. Mot de passe")
print(" 2. Clé SSH")
while True:
auth = input(" Choix (1/2) : ").strip()
if auth == "1":
password = getpass.getpass(" Mot de passe SSH : ")
return {"host": host, "user": user, "auth": "password",
"password": password, "key_path": None}
elif auth == "2":
key_path = input(" Chemin de la clé SSH [~/.ssh/id_rsa] : ").strip()
if not key_path:
key_path = str(Path.home() / ".ssh" / "id_rsa")
return {"host": host, "user": user, "auth": "key",
"password": None, "key_path": key_path}
print(c("red", "Répondez 1 ou 2."))
def collect_agent_config(agent_type: str, catalog: dict, host: str) -> dict:
print(c("cyan", "\nConfiguration de l'agent :"))
default_jid = "{}@xmpp.ovh".format(agent_type)
xmpp_jid = input(" JID XMPP [{}] : ".format(default_jid)).strip() or default_jid
xmpp_pass = getpass.getpass(" Mot de passe XMPP : ")
mqtt_host = input(" Broker MQTT [{}] : ".format(host)).strip() or host
return {"xmpp_jid": xmpp_jid, "xmpp_pass": xmpp_pass, "mqtt_host": mqtt_host}
def show_summary(agent_type: str, ssh: dict, agent_cfg: dict, catalog: dict):
info = catalog[agent_type]
print(c("cyan", "\nRécapitulatif :"))
print(" Agent : {}".format(c("bold", agent_type)))
print(" Repo : {}".format(info["repo_url"]))
print(" Machine : {}@{}".format(ssh["user"], ssh["host"]))
print(" Auth SSH : {}".format("clé SSH" if ssh["auth"] == "key" else "mot de passe"))
print(" XMPP JID : {}".format(agent_cfg["xmpp_jid"]))
print(" MQTT broker: {}".format(agent_cfg["mqtt_host"]))
print(" MQTT inbox : {}".format(info["mqtt_inbox"]))
print()
def deploy_local(agent_type: str, catalog: dict):
"""
Installe l'agent localement (sur la machine courante).
Utile quand on clone le repo directement sur la machine cible.
"""
import subprocess
from pathlib import Path
info = catalog[agent_type]
install_path = info["install_path"]
repo_url = info["repo_url"]
service_name = info["service_name"]
main_script = info["main_script"]
deps = info["dependencies"]
print(c("cyan", "\nDéploiement local de {} dans {}...".format(agent_type, install_path)))
def run(cmd, **kwargs):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, **kwargs)
if result.returncode != 0:
print(c("red", " Erreur : {}".format(result.stderr.strip() or result.stdout.strip())))
sys.exit(1)
return result.stdout.strip()
# Prérequis
print(" Installation des prérequis...")
run("apt-get install -y -qq python3 python3-pip python3-venv git 2>&1")
# Clone
print(" Clonage du dépôt...")
if Path(install_path + "/.git").exists():
run("git -C {} pull".format(install_path))
else:
run("git clone {} {}".format(repo_url, install_path))
# Venv
print(" Création du venv...")
run("python3 -m venv {}/venv".format(install_path))
run("{}/venv/bin/pip install -q {}".format(install_path, " ".join(deps)))
# Config
print(c("cyan", "\nConfiguration de l'agent :"))
default_jid = "{}@xmpp.ovh".format(agent_type)
xmpp_jid = input(" JID XMPP [{}] : ".format(default_jid)).strip() or default_jid
xmpp_pass = getpass.getpass(" Mot de passe XMPP : ")
mqtt_host = input(" Broker MQTT [localhost] : ").strip() or "localhost"
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" : info["mqtt_inbox"],
"mqtt_outbox" : info["mqtt_outbox"],
}
config_path = Path(install_path) / "config" / "config.json"
config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False))
print(" config.json écrit.")
# Service systemd
service = """[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)
Path("/etc/systemd/system/{}.service".format(service_name)).write_text(service)
run("systemctl daemon-reload && systemctl enable {s} && systemctl start {s}".format(s=service_name))
print(c("green", "\nDéploiement local terminé !"))
print(" Service : systemctl status {}".format(service_name))
print(" Logs : journalctl -u {} -f".format(service_name))
def main():
parser = argparse.ArgumentParser(description="Déploiement interactif d'agents")
parser.add_argument("--local", metavar="AGENT",
help="Installer l'agent localement (sans SSH)")
args = parser.parse_args()
print_header()
catalog = load_catalog()
# Mode local
if args.local:
agent_type = args.local if args.local in catalog else None
if not agent_type:
print(c("red", "Agent inconnu : {}".format(args.local)))
print("Agents disponibles : {}".format(", ".join(catalog.keys())))
sys.exit(1)
deploy_local(agent_type, catalog)
return
# Mode SSH interactif
agent_type = choose_agent(catalog)
ssh = collect_ssh_info()
agent_cfg = collect_agent_config(agent_type, catalog, ssh["host"])
show_summary(agent_type, ssh, agent_cfg, catalog)
confirm = input("Confirmer le déploiement ? (oui/non) : ").strip().lower()
if confirm not in ("oui", "o", "yes", "y"):
print(c("yellow", "Déploiement annulé."))
sys.exit(0)
print(c("cyan", "\nDéploiement en cours..."))
def progress(msg):
print(c("yellow", " [{}]".format(msg)))
success, result = deploy_agent(
host = ssh["host"],
ssh_user = ssh["user"],
agent_type = agent_type,
xmpp_jid = agent_cfg["xmpp_jid"],
xmpp_pass = agent_cfg["xmpp_pass"],
mqtt_host = agent_cfg["mqtt_host"],
progress_cb = progress,
ssh_password= ssh["password"],
ssh_key_path= ssh["key_path"],
)
if success:
print(c("green", "\n" + result))
else:
print(c("red", "\nÉchec : " + result))
sys.exit(1)
if __name__ == "__main__":
main()
+179
View File
@@ -0,0 +1,179 @@
#!/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)