Initial commit — agent_deploy v2.0
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.db
|
||||||
|
*.log
|
||||||
|
data/
|
||||||
|
*.egg-info/
|
||||||
|
.vault_pass
|
||||||
|
config/config.json
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent Deploy — déploiement d'agents sur machines distantes ou locales via SSH.
|
||||||
|
Le nom de l'agent cible est choisi par l'utilisateur au déploiement.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
sys.path.insert(0, "/opt")
|
||||||
|
|
||||||
|
from agents_core import BaseAgent, AgentContext, Message, MessageType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentDeploy(BaseAgent):
|
||||||
|
AGENT_TYPE = "deploy"
|
||||||
|
DESCRIPTION = (
|
||||||
|
"Déploiement d'agents sur machines distantes ou locales via SSH. "
|
||||||
|
"Installe, configure et enregistre de nouveaux agents dans le système."
|
||||||
|
)
|
||||||
|
DEFAULT_CONFIG_PATH = "/opt/agent_deploy/config/config.json"
|
||||||
|
|
||||||
|
def get_skills_dir(self) -> str:
|
||||||
|
return os.path.join(os.path.dirname(__file__), "skills")
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
self.mqtt.send_to("nexus", f"Agent Deploy ({self.agent_id}) en ligne.")
|
||||||
|
|
||||||
|
def setup_extra_subscriptions(self):
|
||||||
|
self.mqtt.subscribe(
|
||||||
|
f"agents/{self.agent_id}/control",
|
||||||
|
self._on_control,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_control(self, msg, topic: str):
|
||||||
|
from agents_core.message_bus import Message as Msg
|
||||||
|
payload = msg.payload if isinstance(msg, Msg) else str(msg)
|
||||||
|
result = self._handle_system_command(payload)
|
||||||
|
if result and isinstance(msg, Msg):
|
||||||
|
self.mqtt.reply(msg, result)
|
||||||
|
|
||||||
|
def handle_custom_command(self, cmd: str, args: str, source_msg=None):
|
||||||
|
if cmd == "report":
|
||||||
|
return self._build_report()
|
||||||
|
if cmd == "catalog":
|
||||||
|
from deployer import AgentCatalog
|
||||||
|
catalog = AgentCatalog(
|
||||||
|
self.config.get("catalog", "/opt/agent_deploy/config/catalog.json")
|
||||||
|
)
|
||||||
|
return catalog.summary()
|
||||||
|
return f"Commande inconnue : /{cmd}"
|
||||||
|
|
||||||
|
def _build_report(self) -> str:
|
||||||
|
stats = self.queue.daily_stats()
|
||||||
|
return (
|
||||||
|
f"── Rapport {self.agent_id} ──\n"
|
||||||
|
f"Déploiements : {stats['total']} total / "
|
||||||
|
f"{stats['completed']} OK / {stats['failed']} erreurs / "
|
||||||
|
f"durée moy. {stats['avg_duration_s']}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
AgentDeploy().run()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Agent Deploy — Déploiement d'agents
|
||||||
|
After=network.target mosquitto.service
|
||||||
|
Wants=mosquitto.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/agent_deploy
|
||||||
|
ExecStart=/opt/agent_deploy/venv/bin/python /opt/agent_deploy/agent_deploy.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=agent-deploy
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
Tu es un agent de déploiement. Tu installes et configures de nouveaux agents sur des machines distantes ou locales.
|
||||||
|
|
||||||
|
## Workflow de déploiement
|
||||||
|
|
||||||
|
Quand l'utilisateur veut déployer un agent, collecte ces informations :
|
||||||
|
1. **Type d'agent** : debian, ansible, deploy (ou autre si dans le catalogue)
|
||||||
|
2. **Nom** : nom unique choisi par l'utilisateur — ce sera son identifiant permanent (@nom dans les commandes)
|
||||||
|
3. **Cible** : IP ou hostname (ou "local" pour la machine courante)
|
||||||
|
4. **SSH** : utilisateur + méthode (password ou clé) + credential
|
||||||
|
5. **XMPP** : JID et mot de passe pour le nouvel agent
|
||||||
|
6. **MQTT** : adresse du broker
|
||||||
|
|
||||||
|
Une fois toutes les infos collectées, lance le déploiement avec SKILL:deploy.
|
||||||
|
|
||||||
|
## Skills disponibles
|
||||||
|
|
||||||
|
- **deploy** : lancer un déploiement complet (SSH ou local)
|
||||||
|
- **ssh** : exécuter une commande sur une machine distante
|
||||||
|
- **catalog** : gérer le catalogue des types d'agents déployables
|
||||||
|
- **mqtt_send** : communiquer avec d'autres agents
|
||||||
|
|
||||||
|
## Exemple de déploiement
|
||||||
|
|
||||||
|
Utilisateur : "Déploie un agent debian sur 192.168.1.50, nom 'debian-srv2'"
|
||||||
|
|
||||||
|
Tu collectes les infos manquantes (user SSH, password, JID XMPP...) puis :
|
||||||
|
SKILL:deploy ARGS:start debian debian-srv2 192.168.1.50 root password MonMDP debian-srv2@xmpp.ovh MonXMPP localhost
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
|
||||||
|
- Le nom choisi est définitif — il sera utilisé pour appeler l'agent (@nom)
|
||||||
|
- Vérifie toujours la connectivité SSH avant de lancer un déploiement complet
|
||||||
|
- Les notifications de progression sont envoyées en temps réel à Nexus
|
||||||
|
- Réponds en français
|
||||||
|
- Demande confirmation avant de lancer un déploiement sur une machine de production
|
||||||
+341
@@ -0,0 +1,341 @@
|
|||||||
|
"""
|
||||||
|
Moteur de déploiement SSH — installe un agent sur une machine distante ou locale.
|
||||||
|
Le nom de l'agent est fourni par l'utilisateur et devient son identifiant permanent.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CATALOG_PATH = "/opt/agent_deploy/config/catalog.json"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeployConfig:
|
||||||
|
"""Paramètres d'un déploiement."""
|
||||||
|
agent_type: str # type d'agent : debian, ansible, deploy...
|
||||||
|
agent_name: str # nom choisi par l'utilisateur (ex: "debian-prod")
|
||||||
|
host: str # IP ou hostname cible
|
||||||
|
ssh_user: str # utilisateur SSH
|
||||||
|
ssh_auth: str # "password" ou "key"
|
||||||
|
ssh_credential: str # mot de passe ou chemin de clé privée
|
||||||
|
xmpp_jid: str
|
||||||
|
xmpp_password: str
|
||||||
|
mqtt_host: str
|
||||||
|
mqtt_port: int = 1883
|
||||||
|
local: bool = False # installation locale (pas de SSH)
|
||||||
|
extra: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCatalog:
|
||||||
|
"""Catalogue des types d'agents déployables."""
|
||||||
|
|
||||||
|
def __init__(self, path: str = CATALOG_PATH):
|
||||||
|
self.path = path
|
||||||
|
self._data = self._load()
|
||||||
|
|
||||||
|
def _load(self) -> dict:
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
return self._default_catalog()
|
||||||
|
with open(self.path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def _default_catalog(self) -> dict:
|
||||||
|
return {
|
||||||
|
"nexus": {
|
||||||
|
"description": "Orchestrateur principal — coordonne tous les agents",
|
||||||
|
"repo": "https://github.com/youruser/nexus.git",
|
||||||
|
"install_path_tpl": "/opt/{agent_name}",
|
||||||
|
"service_name_tpl": "{agent_name}",
|
||||||
|
"main_script": "nexus.py",
|
||||||
|
"apt_deps": ["python3", "python3-pip", "python3-venv", "git", "mosquitto-clients"],
|
||||||
|
"pip_deps": ["apscheduler", "duckduckgo-search", "beautifulsoup4"],
|
||||||
|
"config_template": "nexus",
|
||||||
|
"extra_config": {
|
||||||
|
"schedules": {"scheduled_tasks": [], "daily_reports": []}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"debian": {
|
||||||
|
"description": "Agent d'administration système Debian",
|
||||||
|
"repo": "https://github.com/youruser/agent_debian.git",
|
||||||
|
"install_path_tpl": "/opt/{agent_name}",
|
||||||
|
"service_name_tpl": "{agent_name}",
|
||||||
|
"main_script": "agent_debian.py",
|
||||||
|
"apt_deps": ["python3", "python3-pip", "python3-venv", "git", "mosquitto-clients"],
|
||||||
|
"pip_deps": [],
|
||||||
|
"config_template": "debian",
|
||||||
|
"extra_config": {"work_hours": "07:00-23:00"},
|
||||||
|
},
|
||||||
|
"ansible": {
|
||||||
|
"description": "Agent d'automatisation Ansible",
|
||||||
|
"repo": "https://github.com/youruser/agent_ansible.git",
|
||||||
|
"install_path_tpl": "/opt/{agent_name}",
|
||||||
|
"service_name_tpl": "{agent_name}",
|
||||||
|
"main_script": "agent_ansible.py",
|
||||||
|
"apt_deps": ["python3", "python3-pip", "python3-venv", "git", "ansible", "mosquitto-clients"],
|
||||||
|
"pip_deps": [],
|
||||||
|
"config_template": "ansible",
|
||||||
|
"extra_config": {"work_hours": "07:00-23:00"},
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
"description": "Agent de déploiement d'autres agents",
|
||||||
|
"repo": "https://github.com/youruser/agent_deploy.git",
|
||||||
|
"install_path_tpl": "/opt/{agent_name}",
|
||||||
|
"service_name_tpl": "{agent_name}",
|
||||||
|
"main_script": "agent_deploy.py",
|
||||||
|
"apt_deps": ["python3", "python3-pip", "python3-venv", "git", "python3-paramiko", "mosquitto-clients", "sshpass"],
|
||||||
|
"pip_deps": ["paramiko"],
|
||||||
|
"config_template": "deploy",
|
||||||
|
"extra_config": {
|
||||||
|
"work_hours": "08:00-20:00",
|
||||||
|
"catalog": "/opt/{agent_name}/config/catalog.json"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(self, agent_type: str) -> Optional[dict]:
|
||||||
|
return self._data.get(agent_type)
|
||||||
|
|
||||||
|
def list_types(self) -> list[str]:
|
||||||
|
return list(self._data.keys())
|
||||||
|
|
||||||
|
def summary(self) -> str:
|
||||||
|
lines = ["── Agents déployables ──────────────"]
|
||||||
|
for name, info in self._data.items():
|
||||||
|
lines.append(f" [{name}] {info['description']}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class Deployer:
|
||||||
|
"""
|
||||||
|
Orchestre le déploiement complet d'un agent.
|
||||||
|
Supporte SSH (distant) et local.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: DeployConfig, progress_cb: Optional[Callable] = None):
|
||||||
|
self.cfg = config
|
||||||
|
self.catalog = AgentCatalog()
|
||||||
|
self._cb = progress_cb or (lambda msg: logger.info(msg))
|
||||||
|
self._ssh = None
|
||||||
|
|
||||||
|
def deploy(self) -> tuple[bool, str]:
|
||||||
|
"""Lance le déploiement. Retourne (succès, message)."""
|
||||||
|
agent_info = self.catalog.get(self.cfg.agent_type)
|
||||||
|
if not agent_info:
|
||||||
|
return False, f"Type d'agent inconnu : '{self.cfg.agent_type}'. Disponibles : {self.catalog.list_types()}"
|
||||||
|
|
||||||
|
install_path = agent_info["install_path_tpl"].format(agent_name=self.cfg.agent_name)
|
||||||
|
service_name = agent_info["service_name_tpl"].format(agent_name=self.cfg.agent_name)
|
||||||
|
main_script = agent_info["main_script"]
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
("Connexion SSH", lambda: self._connect()),
|
||||||
|
("Dépendances système", lambda: self._install_apt(agent_info["apt_deps"])),
|
||||||
|
("Clonage du dépôt", lambda: self._clone_repo(agent_info["repo"], install_path)),
|
||||||
|
("Environnement Python", lambda: self._setup_venv(install_path, agent_info.get("pip_deps", []))),
|
||||||
|
("Configuration", lambda: self._write_config(install_path, agent_info["config_template"])),
|
||||||
|
("Service systemd", lambda: self._setup_service(install_path, service_name, main_script)),
|
||||||
|
("Démarrage", lambda: self._start_service(service_name)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for step_name, step_fn in steps:
|
||||||
|
self._cb(f"⏳ {step_name}...")
|
||||||
|
try:
|
||||||
|
ok, msg = step_fn()
|
||||||
|
if not ok:
|
||||||
|
self._cb(f"❌ {step_name} échoué : {msg}")
|
||||||
|
self._disconnect()
|
||||||
|
return False, f"Échec à l'étape '{step_name}' : {msg}"
|
||||||
|
self._cb(f"✓ {step_name}")
|
||||||
|
except Exception as e:
|
||||||
|
self._disconnect()
|
||||||
|
return False, f"Exception lors de '{step_name}' : {e}"
|
||||||
|
|
||||||
|
self._disconnect()
|
||||||
|
return True, (
|
||||||
|
f"Agent '{self.cfg.agent_name}' ({self.cfg.agent_type}) déployé avec succès sur {self.cfg.host}.\n"
|
||||||
|
f"Service : {service_name}\n"
|
||||||
|
f"Chemin : {install_path}\n"
|
||||||
|
f"JID XMPP : {self.cfg.xmpp_jid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Étapes de déploiement
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _connect(self) -> tuple[bool, str]:
|
||||||
|
if self.cfg.local:
|
||||||
|
return True, "local"
|
||||||
|
try:
|
||||||
|
import paramiko
|
||||||
|
self._ssh = paramiko.SSHClient()
|
||||||
|
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
kwargs = dict(
|
||||||
|
hostname=self.cfg.host,
|
||||||
|
username=self.cfg.ssh_user,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if self.cfg.ssh_auth == "password":
|
||||||
|
kwargs["password"] = self.cfg.ssh_credential
|
||||||
|
else:
|
||||||
|
kwargs["key_filename"] = self.cfg.ssh_credential
|
||||||
|
self._ssh.connect(**kwargs)
|
||||||
|
return True, "ok"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def _run_remote(self, cmd: str, timeout: int = 120) -> tuple[bool, str]:
|
||||||
|
if self.cfg.local:
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
out = (result.stdout + result.stderr).strip()
|
||||||
|
return result.returncode == 0, out
|
||||||
|
else:
|
||||||
|
stdin, stdout, stderr = self._ssh.exec_command(cmd, timeout=timeout)
|
||||||
|
exit_code = stdout.channel.recv_exit_status()
|
||||||
|
out = stdout.read().decode() + stderr.read().decode()
|
||||||
|
return exit_code == 0, out.strip()
|
||||||
|
|
||||||
|
def _install_apt(self, packages: list) -> tuple[bool, str]:
|
||||||
|
if not packages:
|
||||||
|
return True, "aucune dépendance"
|
||||||
|
pkgs = " ".join(packages)
|
||||||
|
ok, out = self._run_remote(
|
||||||
|
f"DEBIAN_FRONTEND=noninteractive apt-get install -y -q {pkgs}",
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
return ok, out[-500:] if not ok else "ok"
|
||||||
|
|
||||||
|
def _clone_repo(self, repo: str, install_path: str) -> tuple[bool, str]:
|
||||||
|
cmd = (
|
||||||
|
f"if [ -d '{install_path}/.git' ]; then "
|
||||||
|
f" cd {install_path} && git pull; "
|
||||||
|
f"else "
|
||||||
|
f" git clone {repo} {install_path}; "
|
||||||
|
f"fi"
|
||||||
|
)
|
||||||
|
return self._run_remote(cmd, timeout=120)
|
||||||
|
|
||||||
|
def _setup_venv(self, install_path: str, extra_pip: list) -> tuple[bool, str]:
|
||||||
|
venv = f"{install_path}/venv"
|
||||||
|
cmds = [
|
||||||
|
f"python3 -m venv {venv}",
|
||||||
|
f"{venv}/bin/pip install --quiet --upgrade pip",
|
||||||
|
f"{venv}/bin/pip install --quiet -r {install_path}/requirements.txt",
|
||||||
|
]
|
||||||
|
if extra_pip:
|
||||||
|
cmds.append(f"{venv}/bin/pip install --quiet {' '.join(extra_pip)}")
|
||||||
|
for cmd in cmds:
|
||||||
|
ok, out = self._run_remote(cmd, timeout=180)
|
||||||
|
if not ok:
|
||||||
|
return False, out[-500:]
|
||||||
|
return True, "ok"
|
||||||
|
|
||||||
|
def _write_config(self, install_path: str, template: str) -> tuple[bool, str]:
|
||||||
|
"""Écrit config.json avec les paramètres fournis par l'utilisateur."""
|
||||||
|
# Récupère les extra_config du catalogue pour ce type d'agent
|
||||||
|
agent_info = self.catalog.get(self.cfg.agent_type) or {}
|
||||||
|
catalog_extra = agent_info.get("extra_config", {})
|
||||||
|
|
||||||
|
# Résout les templates dans les valeurs (ex: /opt/{agent_name}/config/catalog.json)
|
||||||
|
resolved_extra = {}
|
||||||
|
for k, v in catalog_extra.items():
|
||||||
|
if isinstance(v, str):
|
||||||
|
resolved_extra[k] = v.format(agent_name=self.cfg.agent_name)
|
||||||
|
else:
|
||||||
|
resolved_extra[k] = v
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"agent_id": self.cfg.agent_name,
|
||||||
|
"xmpp": {
|
||||||
|
"jid": self.cfg.xmpp_jid,
|
||||||
|
"password": self.cfg.xmpp_password,
|
||||||
|
"admin_jids": self.cfg.extra.get("admin_jids", [self.cfg.extra.get("admin_jid", "")]),
|
||||||
|
"muc_room": self.cfg.extra.get("muc_room", "agents@conference.xmpp.ovh"),
|
||||||
|
"use_omemo": False,
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"host": self.cfg.mqtt_host,
|
||||||
|
"port": self.cfg.mqtt_port,
|
||||||
|
"username": self.cfg.extra.get("mqtt_username"),
|
||||||
|
"password": self.cfg.extra.get("mqtt_password"),
|
||||||
|
"tls": self.cfg.extra.get("mqtt_tls", False),
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"base_url": self.cfg.extra.get("ollama_url", "http://localhost:11434"),
|
||||||
|
"model": self.cfg.extra.get("model", "mistral"),
|
||||||
|
"temperature": 0.3,
|
||||||
|
},
|
||||||
|
"queue_db": f"{install_path}/data/queue.db",
|
||||||
|
"system_prompt": f"{install_path}/config/system_prompt.txt",
|
||||||
|
}
|
||||||
|
# Fusionne les extra_config du catalogue
|
||||||
|
config.update(resolved_extra)
|
||||||
|
# Les extra de l'utilisateur ont priorité
|
||||||
|
for k, v in self.cfg.extra.items():
|
||||||
|
if k not in ("admin_jid", "admin_jids", "muc_room", "mqtt_username",
|
||||||
|
"mqtt_password", "mqtt_tls", "ollama_url", "model"):
|
||||||
|
config[k] = v
|
||||||
|
|
||||||
|
config_json = json.dumps(config, indent=2, ensure_ascii=False)
|
||||||
|
# Écrit via heredoc pour éviter les problèmes d'échappement
|
||||||
|
cmd = (
|
||||||
|
f"mkdir -p {install_path}/config {install_path}/data && "
|
||||||
|
f"cat > {install_path}/config/config.json << 'DEPLOYEOF'\n"
|
||||||
|
f"{config_json}\n"
|
||||||
|
f"DEPLOYEOF"
|
||||||
|
)
|
||||||
|
return self._run_remote(cmd)
|
||||||
|
|
||||||
|
def _setup_service(self, install_path: str, service_name: str, main_script: str) -> tuple[bool, str]:
|
||||||
|
service_content = f"""[Unit]
|
||||||
|
Description=Agent {service_name}
|
||||||
|
After=network.target mosquitto.service
|
||||||
|
Wants=mosquitto.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory={install_path}
|
||||||
|
ExecStart={install_path}/venv/bin/python {install_path}/{main_script}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier={service_name}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"""
|
||||||
|
cmd = (
|
||||||
|
f"cat > /etc/systemd/system/{service_name}.service << 'SVCEOF'\n"
|
||||||
|
f"{service_content}\n"
|
||||||
|
f"SVCEOF\n"
|
||||||
|
f"systemctl daemon-reload && "
|
||||||
|
f"systemctl enable {service_name}"
|
||||||
|
)
|
||||||
|
return self._run_remote(cmd)
|
||||||
|
|
||||||
|
def _start_service(self, service_name: str) -> tuple[bool, str]:
|
||||||
|
ok, out = self._run_remote(f"systemctl start {service_name}")
|
||||||
|
if not ok:
|
||||||
|
return False, out
|
||||||
|
time.sleep(3)
|
||||||
|
ok2, status = self._run_remote(f"systemctl is-active {service_name}")
|
||||||
|
return status.strip() == "active", status
|
||||||
|
|
||||||
|
def _disconnect(self):
|
||||||
|
if self._ssh:
|
||||||
|
try:
|
||||||
|
self._ssh.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._ssh = None
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
agents_core @ file:///opt/agents_core
|
||||||
|
paramiko>=3.0
|
||||||
|
requests>=2.28
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Skill CATALOG — gérer le catalogue des agents déployables.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:catalog ARGS:list
|
||||||
|
SKILL:catalog ARGS:show <type>
|
||||||
|
SKILL:catalog ARGS:add <type> | <json_config>
|
||||||
|
SKILL:catalog ARGS:remove <type>
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/opt/agent_deploy")
|
||||||
|
|
||||||
|
from deployer import AgentCatalog, CATALOG_PATH
|
||||||
|
|
||||||
|
DESCRIPTION = "Gérer le catalogue des types d'agents déployables"
|
||||||
|
USAGE = "SKILL:catalog ARGS:list | show <type> | add <type>|<json> | remove <type>"
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
parts = args.strip().split(None, 1)
|
||||||
|
action = parts[0].lower() if parts else "list"
|
||||||
|
rest = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
catalog = AgentCatalog()
|
||||||
|
|
||||||
|
if action == "list":
|
||||||
|
return catalog.summary()
|
||||||
|
|
||||||
|
if action == "show":
|
||||||
|
agent_type = rest.strip()
|
||||||
|
info = catalog.get(agent_type)
|
||||||
|
if not info:
|
||||||
|
return f"Type '{agent_type}' inconnu."
|
||||||
|
return json.dumps(info, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
if "|" not in rest:
|
||||||
|
return "Format : add <type> | <json_config>"
|
||||||
|
agent_type, config_json = rest.split("|", 1)
|
||||||
|
agent_type = agent_type.strip()
|
||||||
|
try:
|
||||||
|
config = json.loads(config_json.strip())
|
||||||
|
# Charge le fichier existant et ajoute
|
||||||
|
if os.path.exists(CATALOG_PATH):
|
||||||
|
with open(CATALOG_PATH) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
else:
|
||||||
|
data = catalog._default_catalog()
|
||||||
|
data[agent_type] = config
|
||||||
|
with open(CATALOG_PATH, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
return f"Type '{agent_type}' ajouté au catalogue."
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return f"JSON invalide : {e}"
|
||||||
|
|
||||||
|
if action == "remove":
|
||||||
|
agent_type = rest.strip()
|
||||||
|
if os.path.exists(CATALOG_PATH):
|
||||||
|
with open(CATALOG_PATH) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if agent_type in data:
|
||||||
|
del data[agent_type]
|
||||||
|
with open(CATALOG_PATH, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
return f"Type '{agent_type}' supprimé du catalogue."
|
||||||
|
return f"Type '{agent_type}' introuvable."
|
||||||
|
|
||||||
|
return "Action inconnue. Disponible : list, show, add, remove"
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Skill DEPLOY — déployer un agent sur une machine distante ou locale.
|
||||||
|
|
||||||
|
Le LLM collecte les paramètres via une conversation guidée, puis lance le déploiement.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:deploy ARGS:start <type> <nom> <host> <user> <auth:password|key> <credential> <xmpp_jid> <xmpp_pass> <mqtt_host>
|
||||||
|
SKILL:deploy ARGS:local <type> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host>
|
||||||
|
SKILL:deploy ARGS:catalog
|
||||||
|
SKILL:deploy ARGS:status <host>
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/opt/agent_deploy")
|
||||||
|
|
||||||
|
from deployer import Deployer, DeployConfig, AgentCatalog
|
||||||
|
|
||||||
|
DESCRIPTION = "Déployer un agent sur une machine distante (SSH) ou locale"
|
||||||
|
USAGE = (
|
||||||
|
"SKILL:deploy ARGS:catalog — liste les agents déployables\n"
|
||||||
|
"SKILL:deploy ARGS:start <type> <nom> <host> <user> password <mdp> <xmpp_jid> <xmpp_pass> <mqtt_host>\n"
|
||||||
|
"SKILL:deploy ARGS:local <type> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
parts = args.strip().split(None, 1)
|
||||||
|
action = parts[0].lower() if parts else "catalog"
|
||||||
|
rest = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "catalog":
|
||||||
|
catalog = AgentCatalog()
|
||||||
|
return catalog.summary()
|
||||||
|
|
||||||
|
if action == "local":
|
||||||
|
# ARGS: local <type> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host>
|
||||||
|
p = rest.split()
|
||||||
|
if len(p) < 5:
|
||||||
|
return "Format : local <type> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host>"
|
||||||
|
agent_type, agent_name, xmpp_jid, xmpp_pass, mqtt_host = p[0], p[1], p[2], p[3], p[4]
|
||||||
|
|
||||||
|
cfg = DeployConfig(
|
||||||
|
agent_type=agent_type,
|
||||||
|
agent_name=agent_name,
|
||||||
|
host="localhost",
|
||||||
|
ssh_user="root",
|
||||||
|
ssh_auth="",
|
||||||
|
ssh_credential="",
|
||||||
|
xmpp_jid=xmpp_jid,
|
||||||
|
xmpp_password=xmpp_pass,
|
||||||
|
mqtt_host=mqtt_host,
|
||||||
|
local=True,
|
||||||
|
)
|
||||||
|
return _do_deploy(cfg, context)
|
||||||
|
|
||||||
|
if action == "start":
|
||||||
|
# ARGS: start <type> <nom> <host> <user> <auth> <credential> <xmpp_jid> <xmpp_pass> <mqtt_host>
|
||||||
|
p = rest.split()
|
||||||
|
if len(p) < 9:
|
||||||
|
return (
|
||||||
|
"Format : start <type> <nom> <host> <user> password|key <credential> "
|
||||||
|
"<xmpp_jid> <xmpp_pass> <mqtt_host>"
|
||||||
|
)
|
||||||
|
cfg = DeployConfig(
|
||||||
|
agent_type=p[0],
|
||||||
|
agent_name=p[1],
|
||||||
|
host=p[2],
|
||||||
|
ssh_user=p[3],
|
||||||
|
ssh_auth=p[4],
|
||||||
|
ssh_credential=p[5],
|
||||||
|
xmpp_jid=p[6],
|
||||||
|
xmpp_password=p[7],
|
||||||
|
mqtt_host=p[8],
|
||||||
|
mqtt_port=int(p[9]) if len(p) > 9 else 1883,
|
||||||
|
)
|
||||||
|
return _do_deploy(cfg, context)
|
||||||
|
|
||||||
|
if action == "status":
|
||||||
|
host = rest.strip()
|
||||||
|
if not host:
|
||||||
|
return "Précise l'hôte."
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
f"ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no {host} 'systemctl list-units --type=service --state=active | grep agent'",
|
||||||
|
shell=True, text=True, capture_output=True, timeout=15
|
||||||
|
)
|
||||||
|
return result.stdout.strip() or "Aucun agent actif trouvé sur cet hôte."
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
return "Action inconnue. Disponible : catalog, start, local, status"
|
||||||
|
|
||||||
|
|
||||||
|
def _do_deploy(cfg: DeployConfig, context) -> str:
|
||||||
|
"""Lance le déploiement et notifie via MQTT."""
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
def progress(msg: str):
|
||||||
|
messages.append(msg)
|
||||||
|
# Notifie en temps réel via MQTT
|
||||||
|
context.mqtt.send_to(
|
||||||
|
"nexus",
|
||||||
|
f"[Deploy {cfg.agent_name}] {msg}",
|
||||||
|
msg_type="result",
|
||||||
|
)
|
||||||
|
|
||||||
|
deployer = Deployer(cfg, progress_cb=progress)
|
||||||
|
success, result = deployer.deploy()
|
||||||
|
|
||||||
|
# Notification finale
|
||||||
|
if success:
|
||||||
|
# Notifie Nexus de l'enregistrement du nouvel agent
|
||||||
|
registration = json.dumps({
|
||||||
|
"agent_id": cfg.agent_name,
|
||||||
|
"agent_type": cfg.agent_type,
|
||||||
|
"host": cfg.host,
|
||||||
|
"xmpp_jid": cfg.xmpp_jid,
|
||||||
|
"mqtt_inbox": f"agents/{cfg.agent_name}/inbox",
|
||||||
|
})
|
||||||
|
context.mqtt.publish_raw("agents/nexus/inbox", registration)
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
Skill MQTT_SEND — publier sur un topic MQTT.
|
||||||
|
"""
|
||||||
|
DESCRIPTION = "Publier un message sur un topic MQTT (communication inter-agents)"
|
||||||
|
USAGE = "SKILL:mqtt_send ARGS:<topic> | <message>"
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
if "|" not in args:
|
||||||
|
return "Format : SKILL:mqtt_send ARGS:<topic> | <message>"
|
||||||
|
topic, message = args.split("|", 1)
|
||||||
|
context.mqtt.publish_raw(topic.strip(), message.strip())
|
||||||
|
return f"Message publié sur '{topic.strip()}'."
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
Skill SSH — exécuter des commandes sur une machine distante via SSH.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:ssh ARGS:<host> <user> password <mdp> | <commande>
|
||||||
|
SKILL:ssh ARGS:<host> <user> key <chemin_cle> | <commande>
|
||||||
|
SKILL:ssh ARGS:copy <host> <user> password <mdp> | <fichier_local> <chemin_distant>
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Exécuter des commandes SSH sur une machine distante"
|
||||||
|
USAGE = "SKILL:ssh ARGS:<host> <user> password <mdp> | <commande> ou key <chemin_cle> | <commande>"
|
||||||
|
|
||||||
|
|
||||||
|
def _ssh_cmd(host: str, user: str, auth: str, credential: str, cmd: str, timeout: int = 30) -> str:
|
||||||
|
if auth == "password":
|
||||||
|
full_cmd = (
|
||||||
|
f"sshpass -p '{credential}' ssh "
|
||||||
|
f"-o StrictHostKeyChecking=no "
|
||||||
|
f"-o ConnectTimeout=10 "
|
||||||
|
f"{user}@{host} '{cmd}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
full_cmd = (
|
||||||
|
f"ssh -i {credential} "
|
||||||
|
f"-o StrictHostKeyChecking=no "
|
||||||
|
f"-o ConnectTimeout=10 "
|
||||||
|
f"{user}@{host} '{cmd}'"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
full_cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
out = (result.stdout + result.stderr).strip()
|
||||||
|
return out[:4000] if out else f"(code retour : {result.returncode})"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return f"Timeout ({timeout}s)"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
if "|" not in args:
|
||||||
|
return "Format : <host> <user> password|key <credential> | <commande>"
|
||||||
|
|
||||||
|
left, command = args.split("|", 1)
|
||||||
|
command = command.strip()
|
||||||
|
parts = left.strip().split()
|
||||||
|
|
||||||
|
if len(parts) < 4:
|
||||||
|
return "Format : <host> <user> password|key <credential> | <commande>"
|
||||||
|
|
||||||
|
host, user, auth, credential = parts[0], parts[1], parts[2], parts[3]
|
||||||
|
|
||||||
|
if auth not in ("password", "key"):
|
||||||
|
return "Auth doit être 'password' ou 'key'"
|
||||||
|
|
||||||
|
# Sous-commande copy
|
||||||
|
if command.startswith("COPY "):
|
||||||
|
file_parts = command[5:].split()
|
||||||
|
if len(file_parts) < 2:
|
||||||
|
return "Format copy : COPY <fichier_local> <chemin_distant>"
|
||||||
|
local_file, remote_path = file_parts[0], file_parts[1]
|
||||||
|
if auth == "password":
|
||||||
|
scp_cmd = (
|
||||||
|
f"sshpass -p '{credential}' scp "
|
||||||
|
f"-o StrictHostKeyChecking=no "
|
||||||
|
f"{local_file} {user}@{host}:{remote_path}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
scp_cmd = (
|
||||||
|
f"scp -i {credential} "
|
||||||
|
f"-o StrictHostKeyChecking=no "
|
||||||
|
f"{local_file} {user}@{host}:{remote_path}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
scp_cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=60
|
||||||
|
)
|
||||||
|
return (result.stdout + result.stderr).strip() or f"Fichier copié vers {host}:{remote_path}"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
return _ssh_cmd(host, user, auth, credential, command)
|
||||||
Reference in New Issue
Block a user