feat: deploy agent from any git URL (from_git / from_git_local)
- DeployConfig: add git_url, main_script, apt_deps fields - Deployer: new _deploy_from_git() and _detect_main_script() methods Auto-detects main script (agent_*.py > main.py > grep __main__) Uses minimal apt defaults, reads requirements.txt for pip deps - deploy skill: add from_git and from_git_local actions from_git <url> <nom> <host> <user> password|key <cred> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script] from_git_local <url> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script] Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+83
-1
@@ -18,7 +18,7 @@ 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_type: str # type d'agent : debian, ansible, deploy... ou "custom"
|
||||
agent_name: str # nom choisi par l'utilisateur (ex: "debian-prod")
|
||||
host: str # IP ou hostname cible
|
||||
ssh_user: str # utilisateur SSH
|
||||
@@ -30,6 +30,9 @@ class DeployConfig:
|
||||
mqtt_port: int = 1883
|
||||
local: bool = False # installation locale (pas de SSH)
|
||||
extra: dict = field(default_factory=dict)
|
||||
git_url: Optional[str] = None # bypass catalogue : URL git directe
|
||||
main_script: Optional[str] = None # script principal (None = auto-détection)
|
||||
apt_deps: list = field(default_factory=list) # override des dépendances apt
|
||||
|
||||
|
||||
class AgentCatalog:
|
||||
@@ -123,8 +126,16 @@ class Deployer:
|
||||
self._cb = progress_cb or (lambda msg: logger.info(msg))
|
||||
self._ssh = None
|
||||
|
||||
# Dépendances apt minimales pour tout agent Python custom
|
||||
_DEFAULT_APT_DEPS = ["python3", "python3-pip", "python3-venv", "git"]
|
||||
|
||||
def deploy(self) -> tuple[bool, str]:
|
||||
"""Lance le déploiement. Retourne (succès, message)."""
|
||||
# ── Mode "from_git" : repo arbitraire, sans catalogue ──────────────
|
||||
if self.cfg.git_url:
|
||||
return self._deploy_from_git()
|
||||
|
||||
# ── Mode catalogue ──────────────────────────────────────────────────
|
||||
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()}"
|
||||
@@ -164,6 +175,73 @@ class Deployer:
|
||||
f"JID XMPP : {self.cfg.xmpp_jid}"
|
||||
)
|
||||
|
||||
def _deploy_from_git(self) -> tuple[bool, str]:
|
||||
"""Déploie un agent depuis une URL git arbitraire (sans catalogue)."""
|
||||
install_path = f"/opt/{self.cfg.agent_name}"
|
||||
service_name = self.cfg.agent_name
|
||||
apt_deps = self.cfg.apt_deps or self._DEFAULT_APT_DEPS
|
||||
|
||||
# Le main_script sera détecté après le clone si non fourni
|
||||
main_script_holder = [self.cfg.main_script]
|
||||
|
||||
def detect_script():
|
||||
if main_script_holder[0]:
|
||||
return True, main_script_holder[0]
|
||||
ok, result = self._detect_main_script(install_path)
|
||||
if ok:
|
||||
main_script_holder[0] = result
|
||||
return ok, result
|
||||
|
||||
steps = [
|
||||
("Connexion SSH", lambda: self._connect()),
|
||||
("Dépendances système", lambda: self._install_apt(apt_deps)),
|
||||
("Clonage du dépôt", lambda: self._clone_repo(self.cfg.git_url, install_path)),
|
||||
("Détection script", detect_script),
|
||||
("Environnement Python", lambda: self._setup_venv(install_path, [])),
|
||||
("Configuration", lambda: self._write_config(install_path, "custom")),
|
||||
("Service systemd", lambda: self._setup_service(install_path, service_name, main_script_holder[0])),
|
||||
("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}" + (f" → {msg}" if step_name == "Détection script" else ""))
|
||||
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}' déployé depuis {self.cfg.git_url}\n"
|
||||
f"Script : {main_script_holder[0]}\n"
|
||||
f"Service : {service_name}\n"
|
||||
f"Chemin : {install_path}\n"
|
||||
f"JID XMPP : {self.cfg.xmpp_jid}"
|
||||
)
|
||||
|
||||
def _detect_main_script(self, install_path: str) -> tuple[bool, str]:
|
||||
"""Détecte automatiquement le script principal dans le repo cloné."""
|
||||
# Priorité : agent_*.py > main.py > premier .py avec __main__
|
||||
cmd = (
|
||||
f"cd {install_path} && ("
|
||||
f"ls agent_*.py 2>/dev/null | head -1 || "
|
||||
f"ls main.py 2>/dev/null || "
|
||||
f"grep -rl '__main__' *.py 2>/dev/null | head -1 || "
|
||||
f"ls *.py 2>/dev/null | grep -v 'setup.py\\|conf' | head -1"
|
||||
f")"
|
||||
)
|
||||
ok, out = self._run_remote(cmd)
|
||||
script = out.strip().splitlines()[0].strip() if out.strip() else ""
|
||||
if not script:
|
||||
return False, "Aucun script Python trouvé à la racine du dépôt"
|
||||
return True, script
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Étapes de déploiement
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -241,6 +319,10 @@ class Deployer:
|
||||
|
||||
def _write_config(self, install_path: str, template: str) -> tuple[bool, str]:
|
||||
"""Écrit config.json avec les paramètres fournis par l'utilisateur."""
|
||||
# Pour un déploiement custom (from_git), pas d'infos catalogue
|
||||
if template == "custom":
|
||||
agent_info, catalog_extra = {}, {}
|
||||
else:
|
||||
# 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", {})
|
||||
|
||||
+48
-1
@@ -21,7 +21,9 @@ 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>"
|
||||
"SKILL:deploy ARGS:local <type> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host>\n"
|
||||
"SKILL:deploy ARGS:from_git <git_url> <nom> <host> <user> password|key <credential> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script]\n"
|
||||
"SKILL:deploy ARGS:from_git_local <git_url> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script]"
|
||||
)
|
||||
|
||||
|
||||
@@ -77,6 +79,51 @@ def run(args: str, context) -> str:
|
||||
)
|
||||
return _do_deploy(cfg, context)
|
||||
|
||||
if action == "from_git":
|
||||
# ARGS: from_git <git_url> <nom> <host> <user> password|key <credential> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script]
|
||||
p = rest.split()
|
||||
if len(p) < 9:
|
||||
return (
|
||||
"Format : from_git <git_url> <nom> <host> <user> password|key <credential> "
|
||||
"<xmpp_jid> <xmpp_pass> <mqtt_host> [main_script]"
|
||||
)
|
||||
cfg = DeployConfig(
|
||||
agent_type="custom",
|
||||
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 and p[9].isdigit() else 1883,
|
||||
git_url=p[0],
|
||||
main_script=p[10] if len(p) > 10 else None,
|
||||
)
|
||||
return _do_deploy(cfg, context)
|
||||
|
||||
if action == "from_git_local":
|
||||
# ARGS: from_git_local <git_url> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script]
|
||||
p = rest.split()
|
||||
if len(p) < 5:
|
||||
return "Format : from_git_local <git_url> <nom> <xmpp_jid> <xmpp_pass> <mqtt_host> [main_script]"
|
||||
cfg = DeployConfig(
|
||||
agent_type="custom",
|
||||
agent_name=p[1],
|
||||
host="localhost",
|
||||
ssh_user="root",
|
||||
ssh_auth="",
|
||||
ssh_credential="",
|
||||
xmpp_jid=p[2],
|
||||
xmpp_password=p[3],
|
||||
mqtt_host=p[4],
|
||||
local=True,
|
||||
git_url=p[0],
|
||||
main_script=p[5] if len(p) > 5 else None,
|
||||
)
|
||||
return _do_deploy(cfg, context)
|
||||
|
||||
if action == "status":
|
||||
host = rest.strip()
|
||||
if not host:
|
||||
|
||||
Reference in New Issue
Block a user