commit f2b0dad2d23ce3a152e748d234986dc3488e1c97 Author: sylvain Date: Mon Mar 9 09:01:34 2026 +0000 Initial commit — agent_ansible v2.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd248a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +venv/ +__pycache__/ +*.pyc +*.pyo +*.db +*.log +data/ +*.egg-info/ +.vault_pass +config/config.json diff --git a/agent_ansible.py b/agent_ansible.py new file mode 100644 index 0000000..1cb367a --- /dev/null +++ b/agent_ansible.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Agent Ansible — Automatisation d'infrastructure via Ansible. +Gère playbooks, commandes ad-hoc, inventaire, galaxy, vault. +""" +import os +import sys +import logging + +sys.path.insert(0, "/opt") + +from agents_core import BaseAgent, AgentContext, Message, MessageType + +logger = logging.getLogger(__name__) + + +class AgentAnsible(BaseAgent): + AGENT_TYPE = "ansible" + DESCRIPTION = ( + "Automatisation infrastructure via Ansible : " + "playbooks, commandes ad-hoc, gestion inventaire, galaxy, vault" + ) + DEFAULT_CONFIG_PATH = "/opt/agent_ansible/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 Ansible ({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 == "update": + return self._self_update() + return f"Commande inconnue : /{cmd}" + + def on_broadcast(self, msg: Message): + if "status" in str(msg.payload).lower(): + self.mqtt.reply(msg, self._build_report()) + + def _build_report(self) -> str: + import subprocess + stats = self.queue.daily_stats() + lines = [f"── Rapport {self.agent_id} ──"] + lines.append( + f"Tâches : {stats['total']} total / " + f"{stats['completed']} OK / {stats['failed']} erreurs / " + f"durée moy. {stats['avg_duration_s']}s" + ) + try: + version = subprocess.check_output( + "ansible --version | head -1", shell=True, text=True + ).strip() + lines.append(f"Ansible : {version}") + except Exception: + pass + return "\n".join(lines) + + def _self_update(self) -> str: + import subprocess + try: + out = subprocess.check_output( + "cd /opt/agent_ansible && git pull", + shell=True, text=True, stderr=subprocess.STDOUT + ) + subprocess.Popen(["systemctl", "restart", self.agent_id]) + return f"Mise à jour :\n{out}\nRedémarrage..." + except subprocess.CalledProcessError as e: + return f"Erreur : {e.output}" + + +if __name__ == "__main__": + AgentAnsible().run() diff --git a/agent_ansible.service b/agent_ansible.service new file mode 100644 index 0000000..7086353 --- /dev/null +++ b/agent_ansible.service @@ -0,0 +1,18 @@ +[Unit] +Description=Agent Ansible — Automatisation infrastructure +After=network.target mosquitto.service +Wants=mosquitto.service + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/agent_ansible +ExecStart=/opt/agent_ansible/venv/bin/python /opt/agent_ansible/agent_ansible.py +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=agent-ansible + +[Install] +WantedBy=multi-user.target diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..97dcf56 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,15 @@ +[defaults] +inventory = /opt/agent_ansible/inventory/hosts +roles_path = /opt/agent_ansible/roles +host_key_checking = False +retry_files_enabled = False +stdout_callback = yaml +gathering = smart +fact_caching = jsonfile +fact_caching_connection = /opt/agent_ansible/data/facts_cache +fact_caching_timeout = 86400 +forks = 10 + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s +pipelining = True diff --git a/config/system_prompt.txt b/config/system_prompt.txt new file mode 100644 index 0000000..4fbe046 --- /dev/null +++ b/config/system_prompt.txt @@ -0,0 +1,31 @@ +Tu es un agent d'automatisation Ansible. Tu gères l'infrastructure via des playbooks et des commandes ad-hoc. +Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP. + +## Tes skills et quand les utiliser + +- **playbook** : exécuter, créer, vérifier des playbooks Ansible +- **adhoc** : commandes ad-hoc rapides sur un ou plusieurs hôtes +- **inventory** : gérer l'inventaire (ajouter/supprimer hôtes et groupes, vérifier la connectivité) +- **galaxy** : installer des rôles et collections depuis Ansible Galaxy +- **vault** : chiffrer/déchiffrer des secrets Ansible +- **shell** : fallback pour commandes ansible ou bash directes +- **mqtt_send** : envoyer un résultat ou message à un autre agent + +## Workflow typique + +1. Vérifier la connectivité : SKILL:adhoc ARGS:all ping +2. Exécuter un playbook : SKILL:playbook ARGS:run site.yml +3. Résultat → renvoyé automatiquement à Nexus + +## Règles + +- Toujours faire un `check` (dry-run) avant un playbook destructif +- Vérifier la connectivité des hôtes avant d'exécuter +- Pour les secrets, utiliser Ansible Vault +- Réponds en français, sois concis +- Si un playbook échoue, analyse la sortie et propose une correction + +## Communication MQTT + +Tu peux envoyer des messages à d'autres agents : + SKILL:mqtt_send ARGS:agents/nexus/inbox | résultat ou information importante diff --git a/inventory/hosts b/inventory/hosts new file mode 100644 index 0000000..d48a41e --- /dev/null +++ b/inventory/hosts @@ -0,0 +1,13 @@ +# Inventaire Ansible +# Ajouter vos hôtes ici ou via : SKILL:inventory ARGS:add-host + +[local] +localhost ansible_connection=local + +# Exemple : +# [webservers] +# 192.168.1.10 ansible_user=root +# 192.168.1.11 ansible_user=root +# +# [dbservers] +# 192.168.1.20 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3e396ac --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +agents_core @ file:///opt/agents_core +ansible>=8.0 +requests>=2.28 diff --git a/skills/adhoc.py b/skills/adhoc.py new file mode 100644 index 0000000..78afa7c --- /dev/null +++ b/skills/adhoc.py @@ -0,0 +1,76 @@ +""" +Skill ADHOC — commandes Ansible ad-hoc sur l'inventaire. + +Usage LLM : + SKILL:adhoc ARGS: [args] + SKILL:adhoc ARGS:all ping + SKILL:adhoc ARGS:webservers shell cmd="uptime" + SKILL:adhoc ARGS:192.168.1.10 command cmd="df -h" + SKILL:adhoc ARGS:all setup filter="ansible_memory_mb" + SKILL:adhoc ARGS:all apt name=nginx state=present + SKILL:adhoc ARGS:all service name=nginx state=restarted +""" +import os +import subprocess + +DESCRIPTION = "Commandes Ansible ad-hoc sur les hôtes de l'inventaire" +USAGE = "SKILL:adhoc ARGS: [args] — Ex: all ping | webservers shell cmd='uptime' | all apt name=nginx state=present" + +INVENTORY = "/opt/agent_ansible/inventory/hosts" +ANSIBLE_CFG = "/opt/agent_ansible/ansible.cfg" + +# Modules courants et leurs alias simplifiés +MODULE_ALIASES = { + "ping": ("ping", ""), + "uptime": ("shell", "cmd='uptime'"), + "df": ("shell", "cmd='df -h'"), + "free": ("shell", "cmd='free -h'"), + "whoami": ("shell", "cmd='whoami'"), + "reboot": ("reboot", ""), + "gather": ("setup", ""), +} + + +def _run(cmd: str, timeout: int = 120) -> str: + env = os.environ.copy() + env["ANSIBLE_FORCE_COLOR"] = "0" + env["ANSIBLE_HOST_KEY_CHECKING"] = "False" + if os.path.exists(ANSIBLE_CFG): + env["ANSIBLE_CONFIG"] = ANSIBLE_CFG + try: + result = subprocess.run( + cmd, shell=True, text=True, + capture_output=True, timeout=timeout, env=env + ) + out = (result.stdout + result.stderr).strip() + return out[:5000] if out else "(aucune sortie)" + except subprocess.TimeoutExpired: + return f"Timeout ({timeout}s)" + except Exception as e: + return str(e) + + +def run(args: str, context) -> str: + parts = args.strip().split(None, 2) + if len(parts) < 2: + return ( + "Format : SKILL:adhoc ARGS: [args]\n" + "Exemple : all ping | webservers shell cmd='df -h'" + ) + + hosts = parts[0] + module = parts[1].lower() + margs = parts[2] if len(parts) > 2 else "" + + # Alias pratiques + if module in MODULE_ALIASES and not margs: + module, margs = MODULE_ALIASES[module] + + inv_flag = f"-i {INVENTORY}" if os.path.exists(INVENTORY) else "-i localhost," + + # Construction de la commande + cmd = f"ansible {hosts} {inv_flag} -m {module}" + if margs: + cmd += f" -a \"{margs}\"" + + return _run(cmd) diff --git a/skills/galaxy.py b/skills/galaxy.py new file mode 100644 index 0000000..ffb89e0 --- /dev/null +++ b/skills/galaxy.py @@ -0,0 +1,83 @@ +""" +Skill GALAXY — gestion des rôles et collections Ansible Galaxy. + +Usage LLM : + SKILL:galaxy ARGS:install + SKILL:galaxy ARGS:list + SKILL:galaxy ARGS:remove + SKILL:galaxy ARGS:search + SKILL:galaxy ARGS:info + SKILL:galaxy ARGS:init (crée un squelette de rôle) + SKILL:galaxy ARGS:collection install + SKILL:galaxy ARGS:collection list +""" +import subprocess +import os + +DESCRIPTION = "Gestion des rôles et collections Ansible Galaxy" +USAGE = "SKILL:galaxy ARGS:install | list | remove | search | info | init | collection install " + +ROLES_DIR = "/opt/agent_ansible/roles" + + +def _run(cmd: str, timeout: int = 60) -> str: + env = os.environ.copy() + env["ANSIBLE_FORCE_COLOR"] = "0" + try: + result = subprocess.run( + cmd, shell=True, text=True, + capture_output=True, timeout=timeout, env=env + ) + out = (result.stdout + result.stderr).strip() + return out[:4000] if out else "(aucune sortie)" + except subprocess.TimeoutExpired: + return f"Timeout ({timeout}s)" + except Exception as e: + return str(e) + + +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 "" + + if action == "install": + if not rest: + return "Précise le rôle ou la collection à installer." + return _run(f"ansible-galaxy install {rest} --roles-path {ROLES_DIR}", timeout=120) + + if action == "list": + return _run(f"ansible-galaxy list --roles-path {ROLES_DIR}") + + if action == "remove": + if not rest: + return "Précise le rôle à supprimer." + return _run(f"ansible-galaxy remove {rest} --roles-path {ROLES_DIR}") + + if action == "search": + if not rest: + return "Précise le terme de recherche." + return _run(f"ansible-galaxy search {rest} | head -30") + + if action == "info": + if not rest: + return "Précise le rôle." + return _run(f"ansible-galaxy info {rest}") + + if action == "init": + if not rest: + return "Précise le nom du rôle à créer." + os.makedirs(ROLES_DIR, exist_ok=True) + return _run(f"ansible-galaxy init {rest} --init-path {ROLES_DIR}") + + if action == "collection": + parts2 = rest.split(None, 1) + sub = parts2[0].lower() if parts2 else "list" + carg = parts2[1] if len(parts2) > 1 else "" + if sub == "install": + return _run(f"ansible-galaxy collection install {carg}", timeout=120) + if sub == "list": + return _run("ansible-galaxy collection list") + return f"Sous-commande inconnue : collection {sub}" + + return "Action inconnue. Disponible : install, list, remove, search, info, init, collection" diff --git a/skills/inventory.py b/skills/inventory.py new file mode 100644 index 0000000..7ee6445 --- /dev/null +++ b/skills/inventory.py @@ -0,0 +1,146 @@ +""" +Skill INVENTORY — gestion de l'inventaire Ansible. + +Usage LLM : + SKILL:inventory ARGS:show + SKILL:inventory ARGS:list (hôtes avec variables) + SKILL:inventory ARGS:hosts (liste des hôtes) + SKILL:inventory ARGS:groups (liste des groupes) + SKILL:inventory ARGS:add-host [groupe] [vars...] + SKILL:inventory ARGS:add-group + SKILL:inventory ARGS:remove-host + SKILL:inventory ARGS:ping-all (teste la connectivité) + SKILL:inventory ARGS:facts (collecte les facts) +""" +import os +import re +import subprocess + +DESCRIPTION = "Gestion de l'inventaire Ansible (hôtes, groupes, facts)" +USAGE = "SKILL:inventory ARGS:show | list | hosts | groups | add-host [groupe] | remove-host | ping-all | facts " + +INVENTORY_FILE = "/opt/agent_ansible/inventory/hosts" +ANSIBLE_CFG = "/opt/agent_ansible/ansible.cfg" + + +def _run(cmd: str, timeout: int = 60) -> str: + env = os.environ.copy() + env["ANSIBLE_FORCE_COLOR"] = "0" + env["ANSIBLE_HOST_KEY_CHECKING"] = "False" + if os.path.exists(ANSIBLE_CFG): + env["ANSIBLE_CONFIG"] = ANSIBLE_CFG + try: + result = subprocess.run( + cmd, shell=True, text=True, + capture_output=True, timeout=timeout, env=env + ) + out = (result.stdout + result.stderr).strip() + return out[:4000] if out else "(aucune sortie)" + except subprocess.TimeoutExpired: + return f"Timeout ({timeout}s)" + except Exception as e: + return str(e) + + +def _read_inventory() -> str: + if not os.path.exists(INVENTORY_FILE): + return "" + with open(INVENTORY_FILE) as f: + return f.read() + + +def _write_inventory(content: str): + os.makedirs(os.path.dirname(INVENTORY_FILE), exist_ok=True) + with open(INVENTORY_FILE, "w") as f: + f.write(content) + + +def run(args: str, context) -> str: + parts = args.strip().split(None, 1) + action = parts[0].lower() if parts else "show" + rest = parts[1] if len(parts) > 1 else "" + + if action == "show": + content = _read_inventory() + return content if content else f"Inventaire vide ou inexistant : {INVENTORY_FILE}" + + if action == "list": + return _run(f"ansible-inventory -i {INVENTORY_FILE} --list 2>/dev/null || cat {INVENTORY_FILE}") + + if action == "hosts": + return _run(f"ansible-inventory -i {INVENTORY_FILE} --list-hosts all 2>/dev/null") + + if action == "groups": + return _run(f"ansible-inventory -i {INVENTORY_FILE} --list 2>/dev/null | python3 -c \"import json,sys; d=json.load(sys.stdin); print('\\n'.join(k for k in d if not k.startswith('_')))\"") + + if action == "add-host": + rparts = rest.split() + if not rparts: + return "Précise l'IP ou le nom d'hôte." + host = rparts[0] + group = rparts[1] if len(rparts) > 1 else "all" + vars_ = " ".join(rparts[2:]) if len(rparts) > 2 else "" + + content = _read_inventory() + lines = content.splitlines() if content else [] + + # Vérifie si le groupe existe déjà + group_header = f"[{group}]" + if group_header not in content: + lines.append(f"\n{group_header}") + + # Ajoute l'hôte sous le groupe + entry = f"{host} {vars_}".strip() + if entry in content: + return f"Hôte '{host}' déjà dans l'inventaire." + + # Insère après l'en-tête de groupe + new_lines = [] + inserted = False + for line in lines: + new_lines.append(line) + if line.strip() == group_header and not inserted: + new_lines.append(entry) + inserted = True + + if not inserted: + new_lines.append(entry) + + _write_inventory("\n".join(new_lines) + "\n") + return f"Hôte '{host}' ajouté au groupe '{group}'." + + if action == "add-group": + group = rest.strip() + if not group: + return "Précise le nom du groupe." + content = _read_inventory() + if f"[{group}]" in content: + return f"Groupe '{group}' existe déjà." + _write_inventory(content + f"\n[{group}]\n") + return f"Groupe '{group}' créé." + + if action == "remove-host": + host = rest.strip() + if not host: + return "Précise l'hôte à supprimer." + content = _read_inventory() + new_content = "\n".join( + l for l in content.splitlines() + if not l.strip().startswith(host) + ) + _write_inventory(new_content + "\n") + return f"Hôte '{host}' supprimé de l'inventaire." + + if action == "ping-all": + return _run(f"ansible all -i {INVENTORY_FILE} -m ping", timeout=60) + + if action == "facts": + host = rest.strip() + if not host: + return "Précise l'hôte." + return _run( + f"ansible {host} -i {INVENTORY_FILE} -m setup 2>/dev/null | head -80", + timeout=30 + ) + + return "Action inconnue. Disponible : show, list, hosts, groups, add-host, add-group, remove-host, ping-all, facts" diff --git a/skills/mqtt_send.py b/skills/mqtt_send.py new file mode 100644 index 0000000..ba93d7e --- /dev/null +++ b/skills/mqtt_send.py @@ -0,0 +1,13 @@ +""" +Skill MQTT_SEND — publier un message sur un topic MQTT. +""" +DESCRIPTION = "Publier un message sur un topic MQTT (communication inter-agents)" +USAGE = "SKILL:mqtt_send ARGS: | " + + +def run(args: str, context) -> str: + if "|" not in args: + return "Format : SKILL:mqtt_send ARGS: | " + topic, message = args.split("|", 1) + context.mqtt.publish_raw(topic.strip(), message.strip()) + return f"Message publié sur '{topic.strip()}'." diff --git a/skills/playbook.py b/skills/playbook.py new file mode 100644 index 0000000..3241ff5 --- /dev/null +++ b/skills/playbook.py @@ -0,0 +1,117 @@ +""" +Skill PLAYBOOK — exécution et gestion de playbooks Ansible. + +Usage LLM : + SKILL:playbook ARGS:run [--limit ] [--tags ] [--extra-vars "k=v"] + SKILL:playbook ARGS:check (dry-run) + SKILL:playbook ARGS:list (liste les playbooks disponibles) + SKILL:playbook ARGS:show (affiche le contenu) + SKILL:playbook ARGS:create | (crée un nouveau playbook) + SKILL:playbook ARGS:syntax (vérifie la syntaxe) +""" +import os +import subprocess + +DESCRIPTION = "Exécution et gestion de playbooks Ansible" +USAGE = "SKILL:playbook ARGS:run [--limit ] [--tags ] | check | list | show | create | | syntax " + +PLAYBOOKS_DIR = "/opt/agent_ansible/playbooks" +INVENTORY = "/opt/agent_ansible/inventory/hosts" +ANSIBLE_CFG = "/opt/agent_ansible/ansible.cfg" + + +def _run(cmd: str, timeout: int = 300) -> str: + env = os.environ.copy() + env["ANSIBLE_FORCE_COLOR"] = "0" + env["ANSIBLE_HOST_KEY_CHECKING"] = "False" + if os.path.exists(ANSIBLE_CFG): + env["ANSIBLE_CONFIG"] = ANSIBLE_CFG + try: + result = subprocess.run( + cmd, shell=True, text=True, + capture_output=True, timeout=timeout, env=env + ) + out = (result.stdout + result.stderr).strip() + return out[:5000] if out else "(aucune sortie)" + except subprocess.TimeoutExpired: + return f"Timeout ({timeout}s) — playbook trop long." + except Exception as e: + return str(e) + + +def _resolve_playbook(name: str) -> str: + """Résout le chemin d'un playbook (relatif ou absolu).""" + if os.path.isabs(name) and os.path.exists(name): + return name + # Cherche dans le dossier playbooks + candidates = [ + os.path.join(PLAYBOOKS_DIR, name), + os.path.join(PLAYBOOKS_DIR, name + ".yml"), + os.path.join(PLAYBOOKS_DIR, name + ".yaml"), + name, + ] + for c in candidates: + if os.path.exists(c): + return c + return None + + +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 "" + + if action == "list": + files = [] + for d in [PLAYBOOKS_DIR]: + if os.path.isdir(d): + for f in sorted(os.listdir(d)): + if f.endswith((".yml", ".yaml")): + files.append(os.path.join(d, f)) + return "\n".join(files) if files else f"Aucun playbook dans {PLAYBOOKS_DIR}" + + if action == "show": + path = _resolve_playbook(rest.strip()) + if not path: + return f"Playbook '{rest.strip()}' introuvable." + with open(path) as f: + return f.read()[:3000] + + if action == "create": + if "|" not in rest: + return "Format : create | " + name, content = rest.split("|", 1) + name = name.strip() + if not name.endswith((".yml", ".yaml")): + name += ".yml" + path = os.path.join(PLAYBOOKS_DIR, name) + os.makedirs(PLAYBOOKS_DIR, exist_ok=True) + with open(path, "w") as f: + f.write(content.strip().replace("\\n", "\n")) + return f"Playbook créé : {path}" + + if action in ("run", "check", "syntax"): + # Parse : [options...] + rparts = rest.split() + if not rparts: + return "Précise le fichier playbook." + + playbook_arg = rparts[0] + options = " ".join(rparts[1:]) if len(rparts) > 1 else "" + + path = _resolve_playbook(playbook_arg) + if not path: + return f"Playbook '{playbook_arg}' introuvable dans {PLAYBOOKS_DIR}" + + inv_flag = f"-i {INVENTORY}" if os.path.exists(INVENTORY) else "" + + if action == "syntax": + cmd = f"ansible-playbook {inv_flag} --syntax-check {path}" + elif action == "check": + cmd = f"ansible-playbook {inv_flag} --check {path} {options}" + else: + cmd = f"ansible-playbook {inv_flag} {path} {options}" + + return _run(cmd, timeout=600) + + return "Action inconnue. Disponible : run, check, syntax, list, show, create" diff --git a/skills/shell.py b/skills/shell.py new file mode 100644 index 0000000..7a2636c --- /dev/null +++ b/skills/shell.py @@ -0,0 +1,29 @@ +""" +Skill SHELL — commandes shell et ansible directes (fallback). + +Usage LLM : SKILL:shell ARGS: +""" +import subprocess + +DESCRIPTION = "Exécution de commandes shell et ansible directes (fallback)" +USAGE = "SKILL:shell ARGS:" + + +def run(args: str, context) -> str: + cmd = args.strip() + if not cmd: + return "Commande vide." + try: + result = subprocess.run( + cmd, shell=True, text=True, + capture_output=True, timeout=120, + executable="/bin/bash" + ) + out = (result.stdout + result.stderr).strip() + if len(out) > 4000: + out = out[:4000] + "\n... [tronqué]" + return out or f"(code retour : {result.returncode})" + except subprocess.TimeoutExpired: + return "Timeout (120s)" + except Exception as e: + return str(e) diff --git a/skills/vault.py b/skills/vault.py new file mode 100644 index 0000000..caf6331 --- /dev/null +++ b/skills/vault.py @@ -0,0 +1,82 @@ +""" +Skill VAULT — gestion des secrets Ansible Vault. + +Usage LLM : + SKILL:vault ARGS:encrypt + SKILL:vault ARGS:decrypt + SKILL:vault ARGS:view + SKILL:vault ARGS:encrypt-string + SKILL:vault ARGS:rekey +""" +import os +import subprocess + +DESCRIPTION = "Gestion des secrets chiffrés avec Ansible Vault" +USAGE = "SKILL:vault ARGS:encrypt | decrypt | view | encrypt-string " + +VAULT_PASS_FILE = "/opt/agent_ansible/config/.vault_pass" + + +def _run(cmd: str, timeout: int = 30) -> str: + try: + result = subprocess.run( + cmd, shell=True, text=True, + capture_output=True, timeout=timeout + ) + out = (result.stdout + result.stderr).strip() + return out[:3000] if out else "(aucune sortie)" + except Exception as e: + return str(e) + + +def _vault_flag() -> str: + if os.path.exists(VAULT_PASS_FILE): + return f"--vault-password-file {VAULT_PASS_FILE}" + return "--ask-vault-pass" + + +def run(args: str, context) -> str: + parts = args.strip().split(None, 1) + action = parts[0].lower() if parts else "" + rest = parts[1] if len(parts) > 1 else "" + + flag = _vault_flag() + + if action == "encrypt": + if not rest: + return "Précise le fichier à chiffrer." + return _run(f"ansible-vault encrypt {flag} {rest}") + + if action == "decrypt": + if not rest: + return "Précise le fichier à déchiffrer." + return _run(f"ansible-vault decrypt {flag} {rest}") + + if action == "view": + if not rest: + return "Précise le fichier à visualiser." + return _run(f"ansible-vault view {flag} {rest}") + + if action == "encrypt-string": + parts2 = rest.split(None, 1) + if len(parts2) < 2: + return "Format : encrypt-string " + value, name = parts2[0], parts2[1] + return _run(f"ansible-vault encrypt_string {flag} '{value}' --name '{name}'") + + if action == "rekey": + if not rest: + return "Précise le fichier." + return _run(f"ansible-vault rekey {flag} {rest}") + + if action == "set-password": + # Définir le mot de passe du vault (stocké dans .vault_pass) + password = rest.strip() + if not password: + return "Précise le mot de passe." + with open(VAULT_PASS_FILE, "w") as f: + f.write(password) + os.chmod(VAULT_PASS_FILE, 0o600) + return f"Mot de passe vault enregistré dans {VAULT_PASS_FILE}" + + return "Action inconnue. Disponible : encrypt, decrypt, view, encrypt-string, rekey, set-password"