Ajout !agentUPDATE/UPGRADE : mises à jour agents depuis git

- skills/agent_update.py : check_update (git fetch + log) et do_upgrade (git pull + systemctl restart)
- agent1.py : commandes !agentUPDATE <nom>, !agentsUPDATE, !agentUPGRADE <nom>, !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 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 15:55:31 +00:00
parent 1c951f46f1
commit 3575b391b6
5 changed files with 262 additions and 24 deletions
+11
View File
@@ -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 <nom>` | 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 <nom>` | `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 |
+5 -6
View File
@@ -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
---
+121 -12
View File
@@ -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 <nom>
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 <nom>
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)
# !agentsUPDATE — vérifier les mises à jour de tous les agents
if t == "!agentsUPDATE":
return True, _handle_update_all()
# !agentUPDATE <nom>
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 <nom>
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)
# ── 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
return # None = réponse déjà envoyée manuellement (ex: self-upgrade)
# ── Mode veille : ignorer tout sauf commandes agent ───────────────
if SLEEP_MODE:
+21 -3
View File
@@ -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"
}
}
+101
View File
@@ -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/<branch>.
"""
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)