deploy.py : choix local/distant interactif + agent_name partout
agent2_deploy : MQTT Last Will + publish retain sur agents/status/{name}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,10 +42,22 @@ def mqtt_publish(topic: str, message: str):
|
|||||||
|
|
||||||
def start_mqtt():
|
def start_mqtt():
|
||||||
global _mqtt_pub
|
global _mqtt_pub
|
||||||
|
import json as _json
|
||||||
|
_status_topic = "agents/status/{}".format(MQTT_CLIENT)
|
||||||
|
_offline_payload = _json.dumps({"status": "offline", "agent": MQTT_CLIENT})
|
||||||
|
_online_payload = _json.dumps({
|
||||||
|
"status" : "online",
|
||||||
|
"agent" : MQTT_CLIENT,
|
||||||
|
"jid" : XMPP_JID,
|
||||||
|
"mqtt_inbox": cfg["mqtt_inbox"],
|
||||||
|
})
|
||||||
|
|
||||||
_mqtt_pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
|
_mqtt_pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
|
||||||
client_id=MQTT_CLIENT + "_pub")
|
client_id=MQTT_CLIENT + "_pub")
|
||||||
|
_mqtt_pub.will_set(_status_topic, _offline_payload, retain=True)
|
||||||
_mqtt_pub.connect(MQTT_HOST, MQTT_PORT)
|
_mqtt_pub.connect(MQTT_HOST, MQTT_PORT)
|
||||||
_mqtt_pub.loop_start()
|
_mqtt_pub.loop_start()
|
||||||
|
_mqtt_pub.publish(_status_topic, _online_payload, retain=True)
|
||||||
register_to_agent1()
|
register_to_agent1()
|
||||||
|
|
||||||
def register_to_agent1():
|
def register_to_agent1():
|
||||||
|
|||||||
@@ -2,20 +2,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Script de déploiement CLI standalone.
|
Script de déploiement CLI standalone.
|
||||||
Utilisable après un git clone, sans avoir besoin d'un agent XMPP actif.
|
Utilisable après un git clone, sans besoin d'un agent XMPP actif.
|
||||||
|
|
||||||
Usage :
|
Usage :
|
||||||
python3 deploy.py # déploiement interactif via SSH
|
python3 deploy.py # déploiement interactif (local ou SSH)
|
||||||
python3 deploy.py --local <agent> # installe l'agent sur la machine locale
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import re
|
||||||
import getpass
|
import getpass
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Ajouter le répertoire courant au path pour importer deployer
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
from deployer import deploy_agent, load_catalog
|
from deployer import deploy_agent, load_catalog
|
||||||
|
|
||||||
@@ -32,17 +31,14 @@ def c(color: str, text: str) -> str:
|
|||||||
return COLORS.get(color, "") + text + COLORS["reset"]
|
return COLORS.get(color, "") + text + COLORS["reset"]
|
||||||
|
|
||||||
|
|
||||||
def print_header():
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
print(c("bold", "\n=== Agent Deploy — Déploiement interactif ===\n"))
|
|
||||||
|
|
||||||
|
|
||||||
def choose_agent(catalog: dict) -> str:
|
def choose_agent(catalog: dict) -> str:
|
||||||
print(c("cyan", "Agents disponibles :"))
|
print(c("cyan", "\nAgents disponibles :"))
|
||||||
agents = list(catalog.items())
|
agents = list(catalog.items())
|
||||||
for i, (name, info) in enumerate(agents, 1):
|
for i, (name, info) in enumerate(agents, 1):
|
||||||
print(" {}. {} — {}".format(i, c("bold", name), info["description"]))
|
print(" {}. {} — {}".format(i, c("bold", name), info["description"]))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
choice = input("Choisissez un agent (numéro ou nom) : ").strip()
|
choice = input("Choisissez un agent (numéro ou nom) : ").strip()
|
||||||
if choice.isdigit():
|
if choice.isdigit():
|
||||||
@@ -54,15 +50,38 @@ def choose_agent(catalog: dict) -> str:
|
|||||||
print(c("red", "Choix invalide, réessayez."))
|
print(c("red", "Choix invalide, réessayez."))
|
||||||
|
|
||||||
|
|
||||||
|
def choose_name(agent_type: str) -> str:
|
||||||
|
print(c("cyan", "\nNom de l'agent :"))
|
||||||
|
print(" Ce nom sera utilisé dans vos commandes (ex: \"mets à jour trouducul\"),")
|
||||||
|
print(" comme identifiant MQTT et nom du service systemd.")
|
||||||
|
print(" Lettres, chiffres, tirets et underscores uniquement.")
|
||||||
|
while True:
|
||||||
|
name = input(" Nom [{}] : ".format(agent_type)).strip().lower() or agent_type
|
||||||
|
if re.match(r'^[a-z0-9_-]+$', name):
|
||||||
|
return name
|
||||||
|
print(c("red", " Nom invalide. Utilisez lettres, chiffres, tirets ou underscores."))
|
||||||
|
|
||||||
|
|
||||||
|
def choose_mode() -> str:
|
||||||
|
print(c("cyan", "\nMode de déploiement :"))
|
||||||
|
print(" 1. Localement (sur cette machine)")
|
||||||
|
print(" 2. Machine distante via SSH")
|
||||||
|
while True:
|
||||||
|
choice = input(" Choix (1/2) : ").strip()
|
||||||
|
if choice == "1":
|
||||||
|
return "local"
|
||||||
|
if choice == "2":
|
||||||
|
return "ssh"
|
||||||
|
print(c("red", " Répondez 1 ou 2."))
|
||||||
|
|
||||||
|
|
||||||
def collect_ssh_info() -> dict:
|
def collect_ssh_info() -> dict:
|
||||||
print(c("cyan", "\nInformations SSH :"))
|
print(c("cyan", "\nInformations SSH :"))
|
||||||
host = input(" Adresse IP de la machine cible : ").strip()
|
host = input(" Adresse IP de la machine cible : ").strip()
|
||||||
user = input(" Nom d'utilisateur SSH : ").strip()
|
user = input(" Nom d'utilisateur SSH : ").strip()
|
||||||
|
|
||||||
print(" Authentification :")
|
print(" Authentification :")
|
||||||
print(" 1. Mot de passe")
|
print(" 1. Mot de passe")
|
||||||
print(" 2. Clé SSH")
|
print(" 2. Clé SSH")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
auth = input(" Choix (1/2) : ").strip()
|
auth = input(" Choix (1/2) : ").strip()
|
||||||
if auth == "1":
|
if auth == "1":
|
||||||
@@ -78,93 +97,68 @@ def collect_ssh_info() -> dict:
|
|||||||
print(c("red", " Répondez 1 ou 2."))
|
print(c("red", " Répondez 1 ou 2."))
|
||||||
|
|
||||||
|
|
||||||
def collect_agent_config(agent_type: str, catalog: dict, host: str) -> dict:
|
def collect_agent_config(agent_name: str, host: str) -> dict:
|
||||||
print(c("cyan", "\nConfiguration de l'agent :"))
|
print(c("cyan", "\nConfiguration XMPP/MQTT :"))
|
||||||
default_jid = "{}@xmpp.ovh".format(agent_type)
|
default_jid = "{}@xmpp.ovh".format(agent_name)
|
||||||
xmpp_jid = input(" JID XMPP [{}] : ".format(default_jid)).strip() or default_jid
|
xmpp_jid = input(" JID XMPP [{}] : ".format(default_jid)).strip() or default_jid
|
||||||
xmpp_pass = getpass.getpass(" Mot de passe XMPP : ")
|
xmpp_pass = getpass.getpass(" Mot de passe XMPP : ")
|
||||||
mqtt_host = input(" Broker MQTT [{}] : ".format(host)).strip() or host
|
default_mqtt = host if host else "localhost"
|
||||||
|
mqtt_host = input(" Broker MQTT [{}] : ".format(default_mqtt)).strip() or default_mqtt
|
||||||
return {"xmpp_jid": xmpp_jid, "xmpp_pass": xmpp_pass, "mqtt_host": mqtt_host}
|
return {"xmpp_jid": xmpp_jid, "xmpp_pass": xmpp_pass, "mqtt_host": mqtt_host}
|
||||||
|
|
||||||
|
|
||||||
def show_summary(agent_type: str, ssh: dict, agent_cfg: dict, catalog: dict):
|
# ── Déploiement local ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def deploy_local(agent_type: str, agent_name: str, catalog: dict):
|
||||||
info = catalog[agent_type]
|
info = catalog[agent_type]
|
||||||
print(c("cyan", "\nRécapitulatif :"))
|
|
||||||
print(" Agent : {}".format(c("bold", agent_type)))
|
|
||||||
print(" Repo : {}".format(info["repo_url"]))
|
|
||||||
print(" Machine : {}@{}".format(ssh["user"], ssh["host"]))
|
|
||||||
print(" Auth SSH : {}".format("clé SSH" if ssh["auth"] == "key" else "mot de passe"))
|
|
||||||
print(" XMPP JID : {}".format(agent_cfg["xmpp_jid"]))
|
|
||||||
print(" MQTT broker: {}".format(agent_cfg["mqtt_host"]))
|
|
||||||
print(" MQTT inbox : {}".format(info["mqtt_inbox"]))
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def deploy_local(agent_type: str, catalog: dict):
|
|
||||||
"""
|
|
||||||
Installe l'agent localement (sur la machine courante).
|
|
||||||
Utile quand on clone le repo directement sur la machine cible.
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
info = catalog[agent_type]
|
|
||||||
install_path = info["install_path"]
|
|
||||||
repo_url = info["repo_url"]
|
repo_url = info["repo_url"]
|
||||||
service_name = info["service_name"]
|
|
||||||
main_script = info["main_script"]
|
main_script = info["main_script"]
|
||||||
deps = info["dependencies"]
|
deps = info["dependencies"]
|
||||||
|
install_path = "/opt/{}".format(agent_name)
|
||||||
|
service_name = agent_name
|
||||||
|
|
||||||
print(c("cyan", "\nDéploiement local de {} dans {}...".format(agent_type, install_path)))
|
print(c("cyan", "\nDéploiement local de «{}» ({}) dans {}...".format(
|
||||||
|
agent_name, agent_type, install_path)))
|
||||||
|
|
||||||
def run(cmd, **kwargs):
|
def run(cmd):
|
||||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, **kwargs)
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(c("red", " Erreur : {}".format(result.stderr.strip() or result.stdout.strip())))
|
print(c("red", " Erreur : {}".format(result.stderr.strip() or result.stdout.strip())))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
||||||
# Prérequis
|
print(" [1/5] Installation des prérequis...")
|
||||||
print(" Installation des prérequis...")
|
|
||||||
run("apt-get install -y -qq python3 python3-pip python3-venv git 2>&1")
|
run("apt-get install -y -qq python3 python3-pip python3-venv git 2>&1")
|
||||||
|
|
||||||
# Clone
|
print(" [2/5] Clonage du dépôt...")
|
||||||
print(" Clonage du dépôt...")
|
|
||||||
if Path(install_path + "/.git").exists():
|
if Path(install_path + "/.git").exists():
|
||||||
run("git -C {} pull".format(install_path))
|
run("git -C {} pull".format(install_path))
|
||||||
else:
|
else:
|
||||||
run("git clone {} {}".format(repo_url, install_path))
|
run("git clone {} {}".format(repo_url, install_path))
|
||||||
|
|
||||||
# Venv
|
print(" [3/5] Création du venv et installation des dépendances...")
|
||||||
print(" Création du venv...")
|
|
||||||
run("python3 -m venv {}/venv".format(install_path))
|
run("python3 -m venv {}/venv".format(install_path))
|
||||||
run("{}/venv/bin/pip install -q {}".format(install_path, " ".join(deps)))
|
run("{}/venv/bin/pip install -q {}".format(install_path, " ".join(deps)))
|
||||||
|
|
||||||
# Config
|
print(" [4/5] Configuration...")
|
||||||
print(c("cyan", "\nConfiguration de l'agent :"))
|
agent_cfg = collect_agent_config(agent_name, "localhost")
|
||||||
default_jid = "{}@xmpp.ovh".format(agent_type)
|
|
||||||
xmpp_jid = input(" JID XMPP [{}] : ".format(default_jid)).strip() or default_jid
|
|
||||||
xmpp_pass = getpass.getpass(" Mot de passe XMPP : ")
|
|
||||||
mqtt_host = input(" Broker MQTT [localhost] : ").strip() or "localhost"
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"ollama_url" : "http://192.168.7.119:11434/api/chat",
|
"ollama_url" : "http://192.168.7.119:11434/api/chat",
|
||||||
"model" : "qwen3:8b",
|
"model" : "qwen3:8b",
|
||||||
"xmpp_jid" : xmpp_jid,
|
"xmpp_jid" : agent_cfg["xmpp_jid"],
|
||||||
"xmpp_pass" : xmpp_pass,
|
"xmpp_pass" : agent_cfg["xmpp_pass"],
|
||||||
"admin_jid" : "sylvain@xmpp.ovh",
|
"admin_jid" : "sylvain@xmpp.ovh",
|
||||||
"db_path" : "{}/memory.db".format(install_path),
|
"db_path" : "{}/memory.db".format(install_path),
|
||||||
"mqtt_host" : mqtt_host,
|
"mqtt_host" : agent_cfg["mqtt_host"],
|
||||||
"mqtt_port" : 1883,
|
"mqtt_port" : 1883,
|
||||||
"mqtt_client_id": agent_type,
|
"mqtt_client_id": agent_name,
|
||||||
"mqtt_inbox" : info["mqtt_inbox"],
|
"mqtt_inbox" : "agents/{}/inbox".format(agent_name),
|
||||||
"mqtt_outbox" : info["mqtt_outbox"],
|
"mqtt_outbox" : "agents/agent1/inbox",
|
||||||
}
|
}
|
||||||
config_path = Path(install_path) / "config" / "config.json"
|
config_path = Path(install_path) / "config" / "config.json"
|
||||||
config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False))
|
config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False))
|
||||||
print(" config.json écrit.")
|
|
||||||
|
|
||||||
# Service systemd
|
print(" [5/5] Création et démarrage du service systemd...")
|
||||||
service = """[Unit]
|
service = """[Unit]
|
||||||
Description=Agent {name}
|
Description=Agent {name}
|
||||||
After=network.target mosquitto.service
|
After=network.target mosquitto.service
|
||||||
@@ -181,41 +175,49 @@ StandardError=journal
|
|||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
""".format(name=agent_type, path=install_path, script=main_script)
|
""".format(name=agent_name, path=install_path, script=main_script)
|
||||||
|
|
||||||
Path("/etc/systemd/system/{}.service".format(service_name)).write_text(service)
|
Path("/etc/systemd/system/{}.service".format(service_name)).write_text(service)
|
||||||
run("systemctl daemon-reload && systemctl enable {s} && systemctl start {s}".format(s=service_name))
|
run("systemctl daemon-reload && systemctl enable {s} && systemctl start {s}".format(
|
||||||
|
s=service_name))
|
||||||
|
|
||||||
print(c("green", "\nDéploiement local terminé !"))
|
print(c("green", "\nDéploiement local de «{}» terminé !".format(agent_name)))
|
||||||
|
print(" XMPP JID : {}".format(agent_cfg["xmpp_jid"]))
|
||||||
|
print(" MQTT inbox : agents/{}/inbox".format(agent_name))
|
||||||
print(" Service : systemctl status {}".format(service_name))
|
print(" Service : systemctl status {}".format(service_name))
|
||||||
print(" Logs : journalctl -u {} -f".format(service_name))
|
print(" Logs : journalctl -u {} -f".format(service_name))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
# ── Main ───────────────────────────────────────────────────────────────────
|
||||||
parser = argparse.ArgumentParser(description="Déploiement interactif d'agents")
|
|
||||||
parser.add_argument("--local", metavar="AGENT",
|
|
||||||
help="Installer l'agent localement (sans SSH)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
print_header()
|
def main():
|
||||||
|
print(c("bold", "\n=== Agent Deploy — Déploiement interactif ==="))
|
||||||
catalog = load_catalog()
|
catalog = load_catalog()
|
||||||
|
|
||||||
# Mode local
|
agent_type = choose_agent(catalog)
|
||||||
if args.local:
|
agent_name = choose_name(agent_type)
|
||||||
agent_type = args.local if args.local in catalog else None
|
mode = choose_mode()
|
||||||
if not agent_type:
|
|
||||||
print(c("red", "Agent inconnu : {}".format(args.local)))
|
if mode == "local":
|
||||||
print("Agents disponibles : {}".format(", ".join(catalog.keys())))
|
deploy_local(agent_type, agent_name, catalog)
|
||||||
sys.exit(1)
|
|
||||||
deploy_local(agent_type, catalog)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Mode SSH interactif
|
# Mode SSH
|
||||||
agent_type = choose_agent(catalog)
|
|
||||||
ssh = collect_ssh_info()
|
ssh = collect_ssh_info()
|
||||||
agent_cfg = collect_agent_config(agent_type, catalog, ssh["host"])
|
agent_cfg = collect_agent_config(agent_name, ssh["host"])
|
||||||
|
|
||||||
|
print(c("cyan", "\nRécapitulatif :"))
|
||||||
|
print(" Type : {}".format(agent_type))
|
||||||
|
print(" Nom : {}".format(c("bold", agent_name)))
|
||||||
|
print(" Repo : {}".format(catalog[agent_type]["repo_url"]))
|
||||||
|
print(" Machine : {}@{}".format(ssh["user"], ssh["host"]))
|
||||||
|
print(" Auth SSH : {}".format("clé SSH" if ssh["auth"] == "key" else "mot de passe"))
|
||||||
|
print(" XMPP JID : {}".format(agent_cfg["xmpp_jid"]))
|
||||||
|
print(" MQTT broker: {}".format(agent_cfg["mqtt_host"]))
|
||||||
|
print(" MQTT inbox : agents/{}/inbox".format(agent_name))
|
||||||
|
print(" Service : {}.service".format(agent_name))
|
||||||
|
print()
|
||||||
|
|
||||||
show_summary(agent_type, ssh, agent_cfg, catalog)
|
|
||||||
confirm = input("Confirmer le déploiement ? (oui/non) : ").strip().lower()
|
confirm = input("Confirmer le déploiement ? (oui/non) : ").strip().lower()
|
||||||
if confirm not in ("oui", "o", "yes", "y"):
|
if confirm not in ("oui", "o", "yes", "y"):
|
||||||
print(c("yellow", "Déploiement annulé."))
|
print(c("yellow", "Déploiement annulé."))
|
||||||
@@ -230,6 +232,7 @@ def main():
|
|||||||
host = ssh["host"],
|
host = ssh["host"],
|
||||||
ssh_user = ssh["user"],
|
ssh_user = ssh["user"],
|
||||||
agent_type = agent_type,
|
agent_type = agent_type,
|
||||||
|
agent_name = agent_name,
|
||||||
xmpp_jid = agent_cfg["xmpp_jid"],
|
xmpp_jid = agent_cfg["xmpp_jid"],
|
||||||
xmpp_pass = agent_cfg["xmpp_pass"],
|
xmpp_pass = agent_cfg["xmpp_pass"],
|
||||||
mqtt_host = agent_cfg["mqtt_host"],
|
mqtt_host = agent_cfg["mqtt_host"],
|
||||||
|
|||||||
Reference in New Issue
Block a user