From 3575b391b608530c32897853a19c21544fb73cc0 Mon Sep 17 00:00:00 2001 From: sylvain Date: Sun, 8 Mar 2026 15:55:31 +0000 Subject: [PATCH] =?UTF-8?q?Ajout=20!agentUPDATE/UPGRADE=20:=20mises=20?= =?UTF-8?q?=C3=A0=20jour=20agents=20depuis=20git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - skills/agent_update.py : check_update (git fetch + log) et do_upgrade (git pull + systemctl restart) - agent1.py : commandes !agentUPDATE , !agentsUPDATE, !agentUPGRADE , !agentsUPGRADE - _handle_agent_command retourne (handled, reply) pour gérer le self-upgrade agent1 - !agentUPGRADE agent1 : envoie la réponse XMPP avant systemctl restart - !agentsUPGRADE : met à jour tous les agents puis agent1 en dernier - agents_registry.json : ajout install_path, service_name, git_branch + entrée agent1 - README.md : documentation des nouvelles commandes - TODO.md : tâches marquées comme terminées Co-Authored-By: Claude Sonnet 4.6 --- README.md | 11 +++ TODO.md | 11 ++- agent1.py | 139 ++++++++++++++++++++++++++++++++---- config/agents_registry.json | 24 ++++++- skills/agent_update.py | 101 ++++++++++++++++++++++++++ 5 files changed, 262 insertions(+), 24 deletions(-) create mode 100644 skills/agent_update.py diff --git a/README.md b/README.md index c562ee8..1c715fc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,17 @@ Agent principal du réseau. Il reçoit les instructions de l'utilisateur (via XM > En mode veille, agent1 reste connecté XMPP et répond uniquement aux commandes `!agentON`. +### Mises à jour git + +| Commande | Effet | +|---|---| +| `!agentUPDATE ` | Vérifie si une mise à jour est disponible sur le dépôt git de l'agent | +| `!agentsUPDATE` | Vérifie les dépôts de tous les agents enregistrés | +| `!agentUPGRADE ` | `git pull` + `systemctl restart` de l'agent | +| `!agentsUPGRADE` | `git pull` + restart de tous les agents (agent1 en dernier) | + +> `!agentUPGRADE agent1` redémarre agent1 lui-même via systemd. La réponse XMPP est envoyée avant le redémarrage. + ### Affichage des configurations | Commande | Effet | diff --git a/TODO.md b/TODO.md index 087f0d6..3cdc07c 100644 --- a/TODO.md +++ b/TODO.md @@ -116,12 +116,11 @@ Ajouter dans `agents_registry.json` : ### Points d'attention -- [ ] Exécuter `git` et `systemctl` via subprocess (nécessite que agent1 tourne en root ou avec les droits sudo systemctl) -- [ ] Timeout sur le restart (attendre max 30s que le service remonte) -- [ ] Ne pas upgrader agent1 lui-même sans précaution (le processus se couperait) -- [ ] Pour `!agentUPGRADE agent1` : git pull puis `systemctl restart agent` (agent1 se redémarre proprement via systemd) -- [ ] Gérer le cas où `install_path` n'existe pas dans le registre (agent déployé sans cette info) -- [ ] Proposer `!agentUPGRADE` après `!agentUPDATE` si une mise à jour est disponible +- [x] Exécuter `git` et `systemctl` via subprocess +- [x] Timeout sur les commandes git/systemctl +- [x] `!agentUPGRADE agent1` : git pull + réponse XMPP envoyée avant `systemctl restart agent` +- [x] Gérer le cas où `install_path` n'est pas dans le registre +- [x] `!agentUPDATE` suggère `!agentUPGRADE` si commits disponibles --- diff --git a/agent1.py b/agent1.py index bcff40c..4586357 100644 --- a/agent1.py +++ b/agent1.py @@ -137,10 +137,11 @@ def _get_all_agents() -> list: return [] # ── COMMANDES !agent ────────────────────────────────────────────────────── -def _handle_agent_command(text: str) -> str | None: +def _handle_agent_command(text: str) -> tuple: """ - Gère les commandes !agentON/OFF et !agentsON/OFF. - Retourne la réponse ou None si ce n'est pas une commande agent. + Gère toutes les commandes !agent*. + Retourne (handled: bool, reply: str|None). + reply=None signifie que la réponse XMPP a déjà été envoyée (ex: self-upgrade). """ global SLEEP_MODE @@ -152,7 +153,7 @@ def _handle_agent_command(text: str) -> str | None: for a in agents: _send_control(a, "pause") SLEEP_MODE = True - return "[VEILLE] Agent1 en veille. {} agent(s) mis en pause.\nEnvoyez !agentsON ou !agentON agent1 pour reprendre.".format(len(agents)) + return True, "[VEILLE] Agent1 en veille. {} agent(s) mis en pause.\nEnvoyez !agentsON ou !agentON agent1 pour reprendre.".format(len(agents)) # !agentsON — sortie veille agent1 + resume tous les agents if t == "!agentsON": @@ -160,27 +161,134 @@ def _handle_agent_command(text: str) -> str | None: for a in agents: _send_control(a, "resume") SLEEP_MODE = False - return "[ACTIF] Agent1 actif. {} agent(s) relancés.".format(len(agents)) + return True, "[ACTIF] Agent1 actif. {} agent(s) relancés.".format(len(agents)) # !agentOFF if t.startswith("!agentOFF "): name = t[len("!agentOFF "):].strip() if name == "agent1": SLEEP_MODE = True - return "[VEILLE] Agent1 en veille. Envoyez !agentON agent1 pour reprendre." + return True, "[VEILLE] Agent1 en veille. Envoyez !agentON agent1 pour reprendre." _send_control(name, "pause") - return "[PAUSE] Commande pause envoyée à {}.".format(name) + return True, "[PAUSE] Commande pause envoyée à {}.".format(name) # !agentON if t.startswith("!agentON "): name = t[len("!agentON "):].strip() if name == "agent1": SLEEP_MODE = False - return "[ACTIF] Agent1 actif." + return True, "[ACTIF] Agent1 actif." _send_control(name, "resume") - return "[ACTIF] Commande resume envoyée à {}.".format(name) + return True, "[ACTIF] Commande resume envoyée à {}.".format(name) - return None + # !agentsUPDATE — vérifier les mises à jour de tous les agents + if t == "!agentsUPDATE": + return True, _handle_update_all() + + # !agentUPDATE + if t.startswith("!agentUPDATE "): + name = t[len("!agentUPDATE "):].strip() + return True, _handle_update_one(name) + + # !agentsUPGRADE — mettre à jour tous les agents + if t == "!agentsUPGRADE": + return True, _handle_upgrade_all() + + # !agentUPGRADE + if t.startswith("!agentUPGRADE "): + name = t[len("!agentUPGRADE "):].strip() + return True, _handle_upgrade_one(name) + + return False, None + +# ── MISE À JOUR DEPUIS GIT ─────────────────────────────────────────────── +def _get_agent_git_info(name: str) -> dict | None: + """Retourne {install_path, service_name, git_branch} depuis le registre, ou None.""" + try: + registry = json.loads(REGISTRY_FILE.read_text(encoding="utf-8")) + agent = registry.get(name, {}) + if "install_path" not in agent: + return None + return { + "install_path": agent["install_path"], + "service_name": agent.get("service_name", name), + "git_branch" : agent.get("git_branch", "main"), + } + except Exception: + return None + +def _handle_update_one(name: str) -> str: + from skills.agent_update import check_update + info = _get_agent_git_info(name) + if not info: + return "[{}] Infos git absentes du registre (install_path manquant).".format(name) + return check_update(name, info["install_path"], info["git_branch"]) + +def _handle_update_all() -> str: + from skills.agent_update import check_update + try: + registry = json.loads(REGISTRY_FILE.read_text(encoding="utf-8")) + except Exception: + return "Erreur lecture registre." + results = [] + for name, info in registry.items(): + if "install_path" not in info: + continue + results.append(check_update(name, info["install_path"], info.get("git_branch", "main"))) + return "\n\n".join(results) if results else "Aucun agent avec install_path dans le registre." + +def _handle_upgrade_one(name: str) -> str: + from skills.agent_update import do_upgrade + info = _get_agent_git_info(name) + if not info: + return "[{}] Infos git absentes du registre.".format(name) + self_upgrade = (name == "agent1") + msg = do_upgrade(name, info["install_path"], info["service_name"], + info["git_branch"], self_upgrade=self_upgrade) + if self_upgrade and "Redémarrage en cours" in msg: + # Envoyer le message XMPP avant le restart + if xmpp_bot: + xmpp_bot.send_message(mto=ADMIN_JID, mbody=msg, mtype='chat') + import subprocess + subprocess.Popen(["systemctl", "restart", info["service_name"]]) + return None # Réponse déjà envoyée manuellement + return msg + +def _handle_upgrade_all() -> str: + from skills.agent_update import do_upgrade + try: + registry = json.loads(REGISTRY_FILE.read_text(encoding="utf-8")) + except Exception: + return "Erreur lecture registre." + + results = [] + agent1_info = None + + for name, info in registry.items(): + if "install_path" not in info: + continue + if name == "agent1": + agent1_info = (name, info) # traiter en dernier + continue + msg = do_upgrade(name, info["install_path"], + info.get("service_name", name), info.get("git_branch", "main")) + results.append(msg) + + summary = "\n\n".join(results) if results else "Aucun agent mis à jour." + + if agent1_info: + name, info = agent1_info + pull_msg = do_upgrade(name, info["install_path"], + info.get("service_name", "agent"), info.get("git_branch", "main"), + self_upgrade=True) + summary += "\n\n" + pull_msg + if xmpp_bot: + xmpp_bot.send_message(mto=ADMIN_JID, mbody=summary, mtype='chat') + import subprocess + subprocess.Popen(["systemctl", "restart", info.get("service_name", "agent")]) + return None # Réponse déjà envoyée + + return summary # ── GESTION CONFIGS AVEC CONFIRMATION ──────────────────────────────────── def _handle_config_command(text: str) -> str | None: @@ -495,11 +603,12 @@ class AgentBot(ClientXMPP): user_input = msg['body'].strip() - # ── Commandes !agentON/OFF (prioritaires, toujours traitées) ────── - agent_reply = _handle_agent_command(user_input) - if agent_reply is not None: - self.send_message(mto=ADMIN_JID, mbody=agent_reply, mtype='chat') - return + # ── Commandes !agent* (prioritaires, toujours traitées) ────────── + handled, agent_reply = _handle_agent_command(user_input) + if handled: + if agent_reply is not None: + self.send_message(mto=ADMIN_JID, mbody=agent_reply, mtype='chat') + return # None = réponse déjà envoyée manuellement (ex: self-upgrade) # ── Mode veille : ignorer tout sauf commandes agent ─────────────── if SLEEP_MODE: diff --git a/config/agents_registry.json b/config/agents_registry.json index ab84254..d36408a 100644 --- a/config/agents_registry.json +++ b/config/agents_registry.json @@ -4,26 +4,44 @@ "mqtt_inbox": "agents/agent2_debian13/inbox", "mqtt_outbox": "agents/agent1/inbox", "speciality": "Administration Debian : apt, dpkg, systemd, conteneurs LXC/Docker, KVM, réseau, sécurité système", - "work_hours": { "start": "07:00", "end": "23:00", "days": ["mon","tue","wed","thu","fri","sat","sun"], "enabled": true } + "work_hours": { "start": "07:00", "end": "23:00", "days": ["mon","tue","wed","thu","fri","sat","sun"], "enabled": true }, + "install_path": "/opt/agent2_debian13", + "service_name": "agent2_debian13", + "git_branch": "main" }, "agent2_ansible": { "jid": "agent2_ansible@xmpp.ovh", "mqtt_inbox": "agents/agent2_ansible/inbox", "mqtt_outbox": "agents/agent1/inbox", "speciality": "Automatisation infrastructure via Ansible : playbooks, commandes ad-hoc, déploiement multi-hôtes, gestion de configuration sur le réseau local", - "work_hours": { "start": "07:00", "end": "23:00", "days": ["mon","tue","wed","thu","fri","sat","sun"], "enabled": true } + "work_hours": { "start": "07:00", "end": "23:00", "days": ["mon","tue","wed","thu","fri","sat","sun"], "enabled": true }, + "install_path": "/opt/agent2_ansible", + "service_name": "agent2_ansible", + "git_branch": "main" }, "agent2_deploy": { "jid": "agent2_deploy@xmpp.ovh", "mqtt_inbox": "agents/agent2_deploy/inbox", "mqtt_outbox": "agents/agent1/inbox", "speciality": "Déploiement d'agents : installe et configure d'autres agents sur des machines distantes ou locales via SSH", - "work_hours": { "start": "08:00", "end": "20:00", "days": ["mon","tue","wed","thu","fri"], "enabled": true } + "work_hours": { "start": "08:00", "end": "20:00", "days": ["mon","tue","wed","thu","fri"], "enabled": true }, + "install_path": "/opt/agent2_deploy", + "service_name": "agent2_deploy", + "git_branch": "main" }, "agent2_test": { "jid": "agent2_test@xmpp.ovh", "mqtt_inbox": "agents/agent2_test/inbox", "mqtt_outbox": "agents/agent1/inbox", "speciality": "Spécialiste Debian : apt, systemd, conteneurs LXC/Docker, KVM, réseau, sécurité" + }, + "agent1": { + "jid": "agent1@xmpp.ovh", + "mqtt_inbox": "agents/agent1/inbox", + "mqtt_outbox": "agents/agent1/inbox", + "speciality": "Orchestrateur principal", + "install_path": "/opt/agent", + "service_name": "agent", + "git_branch": "main" } } diff --git a/skills/agent_update.py b/skills/agent_update.py new file mode 100644 index 0000000..02f9ff7 --- /dev/null +++ b/skills/agent_update.py @@ -0,0 +1,101 @@ +""" +Utilitaire : vérification et application des mises à jour git pour les agents. + +Fonctions appelées directement depuis agent1.py (pas de trigger LLM). + + check_update(name, install_path, branch) → rapport git fetch + do_upgrade(name, install_path, service, branch) → git pull + systemctl restart +""" +import subprocess +import shlex +from pathlib import Path + + +def _run(cmd: str, cwd: str = None, timeout: int = 30) -> tuple: + """Lance une commande shell, retourne (stdout, stderr, returncode).""" + try: + result = subprocess.run( + shlex.split(cmd), + cwd=cwd, + capture_output=True, + text=True, + timeout=timeout + ) + return result.stdout.strip(), result.stderr.strip(), result.returncode + except subprocess.TimeoutExpired: + return "", "Timeout ({} s)".format(timeout), -1 + except Exception as e: + return "", str(e), -1 + + +def check_update(agent_name: str, install_path: str, branch: str = "main") -> str: + """ + Vérifie si une mise à jour est disponible sur le dépôt distant. + Effectue un git fetch puis compare HEAD avec origin/. + """ + path = install_path + + if not Path(path).is_dir(): + return "[{}] Répertoire introuvable : {}".format(agent_name, path) + + # git fetch + out, err, rc = _run("git fetch origin {}".format(branch), cwd=path, timeout=20) + if rc != 0: + return "[{}] Erreur git fetch : {}".format(agent_name, err or out) + + # Compter les commits disponibles + out, err, rc = _run( + "git log HEAD..origin/{} --oneline".format(branch), cwd=path) + if rc != 0: + return "[{}] Erreur git log : {}".format(agent_name, err or out) + + commits = [l for l in out.splitlines() if l.strip()] + if not commits: + return "[{}] Déjà à jour.".format(agent_name) + + lines = ["[{}] {} commit(s) disponible(s) :".format(agent_name, len(commits))] + for c in commits[:10]: + lines.append(" {}".format(c)) + if len(commits) > 10: + lines.append(" ... et {} autre(s)".format(len(commits) - 10)) + lines.append("Lancez !agentUPGRADE {} pour appliquer.".format(agent_name)) + return "\n".join(lines) + + +def do_upgrade(agent_name: str, install_path: str, + service_name: str, branch: str = "main", + self_upgrade: bool = False) -> str: + """ + Applique la mise à jour : git pull + systemctl restart. + Pour agent1 (self_upgrade=True), envoie la réponse AVANT le restart. + """ + path = install_path + + if not Path(path).is_dir(): + return "[{}] Répertoire introuvable : {}".format(agent_name, path) + + # git pull + out, err, rc = _run( + "git pull origin {}".format(branch), cwd=path, timeout=60) + if rc != 0: + return "[{}] Erreur git pull : {}".format(agent_name, err or out) + + pull_msg = out or "Déjà à jour." + + if self_upgrade: + # On retourne le message AVANT le restart (agent1 l'envoie puis se redémarre) + return "[{}] {} \nRedémarrage en cours...".format(agent_name, pull_msg) + + # systemctl restart + _, err, rc = _run("systemctl restart {}".format(service_name), timeout=15) + if rc != 0: + return "[{}] Pull OK mais restart échoué : {}".format(agent_name, err) + + # Vérifier que le service est bien remonté + out, _, _ = _run("systemctl is-active {}".format(service_name), timeout=5) + state = out.strip() + if state == "active": + return "[{}] Mise à jour appliquée. Service actif.\n{}".format(agent_name, pull_msg) + else: + return "[{}] Pull OK, service état : {}. Vérifiez les logs.".format( + agent_name, state)