Files
agent2_deploy/agent2_deploy.py
T
2026-03-07 16:55:55 +00:00

360 lines
14 KiB
Python

#!/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()
register_to_agent1()
def register_to_agent1():
"""Publie une déclaration de mise en ligne sur agents/register."""
import json as _json
payload = _json.dumps({
"agent" : MQTT_CLIENT,
"jid" : XMPP_JID,
"mqtt_inbox": cfg["mqtt_inbox"],
"speciality": "Déploiement d'agents : installe et configure d'autres agents sur des machines distantes ou locales via SSH",
})
mqtt_publish("agents/register", payload)
print("[REGISTER] Déclaration envoyée à agent1.")
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()