Initial commit — agent_debian 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
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent Debian — Administration système complète.
|
||||||
|
Contrôle apt, systemd, réseau, filesystem, processus, logs, conteneurs, utilisateurs.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
|
||||||
|
sys.path.insert(0, "/opt")
|
||||||
|
|
||||||
|
from agents_core import BaseAgent, AgentContext, Message, MessageType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentDebian(BaseAgent):
|
||||||
|
AGENT_TYPE = "debian"
|
||||||
|
DESCRIPTION = (
|
||||||
|
"Administration système Debian : paquets apt, services systemd, "
|
||||||
|
"réseau, filesystem, processus, logs, conteneurs Docker/LXC, utilisateurs"
|
||||||
|
)
|
||||||
|
DEFAULT_CONFIG_PATH = "/opt/agent_debian/config/config.json"
|
||||||
|
|
||||||
|
def get_skills_dir(self) -> str:
|
||||||
|
return os.path.join(os.path.dirname(__file__), "skills")
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
"""Au démarrage, signale à Nexus que l'agent est prêt."""
|
||||||
|
self.mqtt.send_to("nexus", f"Agent Debian ({self.agent_id}) en ligne.")
|
||||||
|
# Lance la surveillance proactive
|
||||||
|
self._start_monitoring()
|
||||||
|
|
||||||
|
def setup_extra_subscriptions(self):
|
||||||
|
"""Souscrit aussi aux commandes de contrôle."""
|
||||||
|
self.mqtt.subscribe(
|
||||||
|
f"agents/{self.agent_id}/control",
|
||||||
|
self._on_control_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_control_message(self, msg, topic: str):
|
||||||
|
"""Messages de contrôle (pause, resume, report...)."""
|
||||||
|
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):
|
||||||
|
"""Commandes spécifiques à l'agent Debian."""
|
||||||
|
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):
|
||||||
|
"""Réagit aux broadcasts (ex: demande de statut globale)."""
|
||||||
|
if "status" in str(msg.payload).lower():
|
||||||
|
self.mqtt.reply(msg, self._build_report())
|
||||||
|
|
||||||
|
def _build_report(self) -> str:
|
||||||
|
"""Génère un rapport quotidien du système."""
|
||||||
|
context = AgentContext(self)
|
||||||
|
lines = [f"── Rapport {self.agent_id} ──"]
|
||||||
|
stats = self.queue.daily_stats()
|
||||||
|
lines.append(
|
||||||
|
f"Tâches : {stats['total']} total / "
|
||||||
|
f"{stats['completed']} OK / {stats['failed']} erreurs / "
|
||||||
|
f"durée moy. {stats['avg_duration_s']}s"
|
||||||
|
)
|
||||||
|
# Infos système rapides
|
||||||
|
try:
|
||||||
|
uptime = subprocess.check_output("uptime -p", shell=True, text=True).strip()
|
||||||
|
disk = subprocess.check_output("df -h / | tail -1 | awk '{print $3\"/\"$2\" (\"$5\" utilisé)\"}'",
|
||||||
|
shell=True, text=True).strip()
|
||||||
|
mem = subprocess.check_output(
|
||||||
|
"free -h | awk '/^Mem:/{print $3\"/\"$2}'", shell=True, text=True
|
||||||
|
).strip()
|
||||||
|
lines.append(f"Uptime : {uptime} | RAM : {mem} | Disque / : {disk}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _self_update(self) -> str:
|
||||||
|
"""Git pull + redémarrage du service."""
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
"cd /opt/agent_debian && git pull",
|
||||||
|
shell=True, text=True, stderr=subprocess.STDOUT
|
||||||
|
)
|
||||||
|
subprocess.Popen(["systemctl", "restart", self.agent_id])
|
||||||
|
return f"Mise à jour effectuée :\n{out}\nRedémarrage en cours..."
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return f"Erreur mise à jour : {e.output}"
|
||||||
|
|
||||||
|
def _start_monitoring(self):
|
||||||
|
"""Lance la surveillance proactive en arrière-plan."""
|
||||||
|
t = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _monitor_loop(self):
|
||||||
|
"""Vérifie périodiquement les ressources critiques et alerte si nécessaire."""
|
||||||
|
import time
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
self._check_disk_usage()
|
||||||
|
self._check_memory()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[Monitor] {e}")
|
||||||
|
time.sleep(300) # Toutes les 5 minutes
|
||||||
|
|
||||||
|
def _check_disk_usage(self):
|
||||||
|
"""Alerte si un disque dépasse 85%."""
|
||||||
|
result = subprocess.run(
|
||||||
|
"df -h | awk 'NR>1 && $5+0 > 85 {print $0}'",
|
||||||
|
shell=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.stdout.strip():
|
||||||
|
self.mqtt.alert(
|
||||||
|
f"Espace disque critique :\n{result.stdout.strip()}",
|
||||||
|
severity="critical"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_memory(self):
|
||||||
|
"""Alerte si la RAM disponible < 10%."""
|
||||||
|
result = subprocess.run(
|
||||||
|
"free | awk '/^Mem:/{if ($3/$2*100 > 90) print \"RAM utilisée à \"int($3/$2*100)\"%\"}'",
|
||||||
|
shell=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.stdout.strip():
|
||||||
|
self.mqtt.alert(result.stdout.strip(), severity="warning")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
AgentDebian().run()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Agent Debian — Administration système
|
||||||
|
After=network.target mosquitto.service
|
||||||
|
Wants=mosquitto.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/agent_debian
|
||||||
|
ExecStart=/opt/agent_debian/venv/bin/python /opt/agent_debian/agent_debian.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=agent-debian
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
Tu es un agent d'administration système Debian. Tu contrôles pleinement ce serveur.
|
||||||
|
Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP (directement).
|
||||||
|
|
||||||
|
## Tes skills disponibles et quand les utiliser
|
||||||
|
|
||||||
|
- **sysinfo** : informations système (CPU, RAM, disque, uptime, réseau)
|
||||||
|
- **apt** : gestion des paquets (install, remove, update, upgrade, search)
|
||||||
|
- **systemd** : gestion des services (start, stop, restart, status, logs, enable)
|
||||||
|
- **filesystem** : opérations fichiers (ls, cat, write, delete, find, grep, df, du)
|
||||||
|
- **network** : réseau (ip, ping, traceroute, DNS, ports, firewall)
|
||||||
|
- **process** : processus (list, kill, top, find)
|
||||||
|
- **journal** : logs système (tail, service, errors, since, grep)
|
||||||
|
- **user** : utilisateurs (add, delete, passwd, groups, sudo)
|
||||||
|
- **container** : Docker et LXC (ps, start, stop, logs, exec, stats)
|
||||||
|
- **cron** : tâches planifiées (list, add, remove)
|
||||||
|
- **script** : créer et exécuter des scripts bash
|
||||||
|
- **shell** : commande bash directe (fallback si aucun skill ne convient)
|
||||||
|
- **mqtt_send** : envoyer un message à un autre agent ou topic MQTT
|
||||||
|
|
||||||
|
## Règles importantes
|
||||||
|
|
||||||
|
1. Utilise toujours le skill le plus spécifique disponible
|
||||||
|
2. Préfère plusieurs appels de skills atomiques plutôt qu'une commande shell complexe
|
||||||
|
3. Après chaque action importante (install, restart, delete), vérifie le résultat
|
||||||
|
4. Si une tâche génère un script, utilise SKILL:script pour le créer et l'exécuter,
|
||||||
|
et le résultat sera automatiquement renvoyé via MQTT
|
||||||
|
5. En cas d'erreur, diagnostique avant de réessayer
|
||||||
|
6. Réponds toujours en français
|
||||||
|
7. Sois concis dans tes réponses — l'essentiel, pas tout le stdout brut
|
||||||
|
|
||||||
|
## Communication MQTT
|
||||||
|
|
||||||
|
Tu peux envoyer des messages à d'autres agents :
|
||||||
|
SKILL:mqtt_send ARGS:agents/nexus/inbox | {"type":"result","payload":"mon résultat"}
|
||||||
|
|
||||||
|
Pour les scripts qui doivent retourner un résultat :
|
||||||
|
Les variables $MQTT_BROKER et $MQTT_REPLY_TOPIC sont disponibles dans l'environnement.
|
||||||
|
mosquitto_pub -h $MQTT_BROKER -t $MQTT_REPLY_TOPIC -m "résultat"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
agents_core @ file:///opt/agents_core
|
||||||
|
requests>=2.28
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
Skill APT — gestion des paquets Debian.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:apt ARGS:update
|
||||||
|
SKILL:apt ARGS:upgrade
|
||||||
|
SKILL:apt ARGS:install <paquet1> [paquet2...]
|
||||||
|
SKILL:apt ARGS:remove <paquet>
|
||||||
|
SKILL:apt ARGS:purge <paquet>
|
||||||
|
SKILL:apt ARGS:search <terme>
|
||||||
|
SKILL:apt ARGS:show <paquet>
|
||||||
|
SKILL:apt ARGS:list-installed [filtre]
|
||||||
|
SKILL:apt ARGS:list-upgradable
|
||||||
|
SKILL:apt ARGS:autoremove
|
||||||
|
SKILL:apt ARGS:check-updates
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Gestion des paquets Debian via apt/dpkg"
|
||||||
|
USAGE = "SKILL:apt ARGS:install <paquet> | remove <paquet> | update | upgrade | search <terme> | show <paquet> | list-installed | list-upgradable | autoremove"
|
||||||
|
|
||||||
|
ENV = {"DEBIAN_FRONTEND": "noninteractive", "PATH": "/usr/bin:/bin:/usr/sbin:/sbin"}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 120) -> str:
|
||||||
|
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) — commande trop longue."
|
||||||
|
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 ""
|
||||||
|
extra = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "update":
|
||||||
|
return _run("apt-get update -q")
|
||||||
|
|
||||||
|
if action == "upgrade":
|
||||||
|
return _run("apt-get upgrade -y -q", timeout=300)
|
||||||
|
|
||||||
|
if action == "dist-upgrade":
|
||||||
|
return _run("apt-get dist-upgrade -y -q", timeout=300)
|
||||||
|
|
||||||
|
if action == "install":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le(s) paquet(s) à installer."
|
||||||
|
return _run(f"apt-get install -y -q {extra}", timeout=180)
|
||||||
|
|
||||||
|
if action == "remove":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le paquet à supprimer."
|
||||||
|
return _run(f"apt-get remove -y {extra}")
|
||||||
|
|
||||||
|
if action == "purge":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le paquet à purger."
|
||||||
|
return _run(f"apt-get purge -y {extra}")
|
||||||
|
|
||||||
|
if action == "autoremove":
|
||||||
|
return _run("apt-get autoremove -y")
|
||||||
|
|
||||||
|
if action == "search":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le terme de recherche."
|
||||||
|
return _run(f"apt-cache search {extra} | head -30")
|
||||||
|
|
||||||
|
if action == "show":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le paquet."
|
||||||
|
return _run(f"apt-cache show {extra}")
|
||||||
|
|
||||||
|
if action in ("list-installed", "list"):
|
||||||
|
cmd = f"dpkg -l | grep '^ii' | awk '{{print $2\" \"$3}}'"
|
||||||
|
if extra:
|
||||||
|
cmd += f" | grep {extra}"
|
||||||
|
return _run(cmd)
|
||||||
|
|
||||||
|
if action == "list-upgradable":
|
||||||
|
return _run("apt list --upgradable 2>/dev/null")
|
||||||
|
|
||||||
|
if action == "check-updates":
|
||||||
|
out = _run("apt-get -s upgrade 2>/dev/null | grep '^[0-9]'")
|
||||||
|
return out or "Système à jour."
|
||||||
|
|
||||||
|
if action == "hold":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le paquet."
|
||||||
|
return _run(f"apt-mark hold {extra}")
|
||||||
|
|
||||||
|
if action == "unhold":
|
||||||
|
if not extra:
|
||||||
|
return "Précise le paquet."
|
||||||
|
return _run(f"apt-mark unhold {extra}")
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Action inconnue. Disponible : update, upgrade, install, remove, purge, "
|
||||||
|
"search, show, list-installed, list-upgradable, autoremove, check-updates, hold, unhold"
|
||||||
|
)
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
Skill CONTAINER — gestion des conteneurs Docker et LXC/LXD.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:container ARGS:docker ps [all]
|
||||||
|
SKILL:container ARGS:docker start <nom>
|
||||||
|
SKILL:container ARGS:docker stop <nom>
|
||||||
|
SKILL:container ARGS:docker restart <nom>
|
||||||
|
SKILL:container ARGS:docker logs <nom> [N]
|
||||||
|
SKILL:container ARGS:docker stats
|
||||||
|
SKILL:container ARGS:docker images
|
||||||
|
SKILL:container ARGS:docker pull <image>
|
||||||
|
SKILL:container ARGS:docker rm <nom>
|
||||||
|
SKILL:container ARGS:docker rmi <image>
|
||||||
|
SKILL:container ARGS:docker exec <nom> <commande>
|
||||||
|
SKILL:container ARGS:docker inspect <nom>
|
||||||
|
SKILL:container ARGS:lxc list
|
||||||
|
SKILL:container ARGS:lxc start <nom>
|
||||||
|
SKILL:container ARGS:lxc stop <nom>
|
||||||
|
SKILL:container ARGS:lxc exec <nom> <commande>
|
||||||
|
SKILL:container ARGS:lxc info <nom>
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Gestion conteneurs Docker et LXC/LXD : start, stop, logs, exec, stats, images"
|
||||||
|
USAGE = "SKILL:container ARGS:docker ps|start|stop|restart|logs|exec|stats|images|pull | lxc list|start|stop|exec|info"
|
||||||
|
|
||||||
|
|
||||||
|
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[:4000] if out else "(aucune sortie)"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return f"Timeout ({timeout}s)"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _docker(action: str, args: list) -> str:
|
||||||
|
a = args[0] if args else ""
|
||||||
|
|
||||||
|
if action == "ps":
|
||||||
|
flag = "-a" if (a == "all" or a == "-a") else ""
|
||||||
|
return _run(f"docker ps {flag} --format 'table {{{{.Names}}}}\\t{{{{.Image}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}'")
|
||||||
|
|
||||||
|
if action == "start":
|
||||||
|
return _run(f"docker start {a}") if a else "Précise le nom du conteneur."
|
||||||
|
|
||||||
|
if action == "stop":
|
||||||
|
return _run(f"docker stop {a}") if a else "Précise le nom du conteneur."
|
||||||
|
|
||||||
|
if action == "restart":
|
||||||
|
return _run(f"docker restart {a}") if a else "Précise le nom du conteneur."
|
||||||
|
|
||||||
|
if action == "logs":
|
||||||
|
if not a:
|
||||||
|
return "Précise le nom du conteneur."
|
||||||
|
n = args[1] if len(args) > 1 else "50"
|
||||||
|
return _run(f"docker logs --tail {n} {a}")
|
||||||
|
|
||||||
|
if action == "stats":
|
||||||
|
return _run("docker stats --no-stream --format 'table {{{{.Name}}}}\\t{{{{.CPUPerc}}}}\\t{{{{.MemUsage}}}}'")
|
||||||
|
|
||||||
|
if action == "images":
|
||||||
|
return _run("docker images --format 'table {{{{.Repository}}}}\\t{{{{.Tag}}}}\\t{{{{.Size}}}}\\t{{{{.CreatedSince}}}}'")
|
||||||
|
|
||||||
|
if action == "pull":
|
||||||
|
return _run(f"docker pull {a}", timeout=120) if a else "Précise l'image."
|
||||||
|
|
||||||
|
if action == "rm":
|
||||||
|
return _run(f"docker rm {a}") if a else "Précise le nom du conteneur."
|
||||||
|
|
||||||
|
if action == "rm-stopped":
|
||||||
|
return _run("docker container prune -f")
|
||||||
|
|
||||||
|
if action == "rmi":
|
||||||
|
return _run(f"docker rmi {a}") if a else "Précise l'image."
|
||||||
|
|
||||||
|
if action == "exec":
|
||||||
|
if len(args) < 2:
|
||||||
|
return "Format : docker exec <conteneur> <commande>"
|
||||||
|
container = args[0]
|
||||||
|
cmd = " ".join(args[1:])
|
||||||
|
return _run(f"docker exec {container} {cmd}")
|
||||||
|
|
||||||
|
if action == "inspect":
|
||||||
|
return _run(f"docker inspect {a}") if a else "Précise le nom du conteneur."
|
||||||
|
|
||||||
|
if action == "network":
|
||||||
|
return _run("docker network ls")
|
||||||
|
|
||||||
|
if action == "volumes":
|
||||||
|
return _run("docker volume ls")
|
||||||
|
|
||||||
|
if action == "compose-up":
|
||||||
|
return _run("docker compose up -d", timeout=120) if not a else _run(f"docker compose -f {a} up -d", timeout=120)
|
||||||
|
|
||||||
|
if action == "compose-down":
|
||||||
|
return _run("docker compose down")
|
||||||
|
|
||||||
|
return f"Action docker inconnue : {action}"
|
||||||
|
|
||||||
|
|
||||||
|
def _lxc(action: str, args: list) -> str:
|
||||||
|
a = args[0] if args else ""
|
||||||
|
|
||||||
|
if action == "list":
|
||||||
|
return _run("lxc list --format table 2>/dev/null || lxc-ls -f 2>/dev/null")
|
||||||
|
|
||||||
|
if action == "start":
|
||||||
|
return _run(f"lxc start {a}") if a else "Précise le nom."
|
||||||
|
|
||||||
|
if action == "stop":
|
||||||
|
return _run(f"lxc stop {a}") if a else "Précise le nom."
|
||||||
|
|
||||||
|
if action == "restart":
|
||||||
|
return _run(f"lxc restart {a}") if a else "Précise le nom."
|
||||||
|
|
||||||
|
if action == "exec":
|
||||||
|
if len(args) < 2:
|
||||||
|
return "Format : lxc exec <conteneur> <commande>"
|
||||||
|
cmd = " ".join(args[1:])
|
||||||
|
return _run(f"lxc exec {a} -- {cmd}")
|
||||||
|
|
||||||
|
if action == "info":
|
||||||
|
return _run(f"lxc info {a}") if a else _run("lxc info")
|
||||||
|
|
||||||
|
if action == "snapshot":
|
||||||
|
return _run(f"lxc snapshot {a}") if a else "Précise le nom."
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
return _run(f"lxc delete {a} --force") if a else "Précise le nom."
|
||||||
|
|
||||||
|
return f"Action lxc inconnue : {action}"
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
parts = args.strip().split()
|
||||||
|
if not parts:
|
||||||
|
return "Précise : docker ou lxc suivi d'une action."
|
||||||
|
|
||||||
|
runtime = parts[0].lower()
|
||||||
|
action = parts[1].lower() if len(parts) > 1 else "ps"
|
||||||
|
rest = parts[2:] if len(parts) > 2 else []
|
||||||
|
|
||||||
|
if runtime == "docker":
|
||||||
|
return _docker(action, rest)
|
||||||
|
|
||||||
|
if runtime in ("lxc", "lxd"):
|
||||||
|
return _lxc(action, rest)
|
||||||
|
|
||||||
|
# Tentative de détection auto
|
||||||
|
if runtime in ("ps", "stats", "images", "logs", "start", "stop", "restart", "exec"):
|
||||||
|
return _docker(runtime, parts[1:])
|
||||||
|
|
||||||
|
return "Précise le runtime : docker ou lxc. Ex: SKILL:container ARGS:docker ps"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""
|
||||||
|
Skill CRON — gestion des tâches cron.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:cron ARGS:list [utilisateur]
|
||||||
|
SKILL:cron ARGS:add <expression_cron> <commande>
|
||||||
|
SKILL:cron ARGS:remove <pattern>
|
||||||
|
SKILL:cron ARGS:system-list
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
DESCRIPTION = "Gestion des tâches cron (crontab)"
|
||||||
|
USAGE = "SKILL:cron ARGS:list | add <* * * * *> <commande> | remove <pattern> | system-list"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 10) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
return (result.stdout + result.stderr).strip() or "(aucune sortie)"
|
||||||
|
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 == "list":
|
||||||
|
user = rest.strip() or ""
|
||||||
|
flag = f"-u {user}" if user else ""
|
||||||
|
result = _run(f"crontab {flag} -l 2>/dev/null")
|
||||||
|
return result if result else "Crontab vide."
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
# Format attendu : "* * * * * commande"
|
||||||
|
# On split les 5 premiers champs (expression cron) + le reste (commande)
|
||||||
|
words = rest.split()
|
||||||
|
if len(words) < 6:
|
||||||
|
return "Format : add <min> <heure> <jour> <mois> <jourSem> <commande>\nEx: add 0 3 * * * /usr/bin/apt-get update"
|
||||||
|
cron_expr = " ".join(words[:5])
|
||||||
|
command = " ".join(words[5:])
|
||||||
|
entry = f"{cron_expr} {command}"
|
||||||
|
|
||||||
|
# Récupère le crontab actuel, ajoute la ligne
|
||||||
|
current = _run("crontab -l 2>/dev/null")
|
||||||
|
if entry in current:
|
||||||
|
return f"Cette entrée existe déjà : {entry}"
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".cron", delete=False) as f:
|
||||||
|
if current and "no crontab" not in current.lower():
|
||||||
|
f.write(current + "\n")
|
||||||
|
f.write(entry + "\n")
|
||||||
|
tmpfile = f.name
|
||||||
|
|
||||||
|
out = _run(f"crontab {tmpfile}")
|
||||||
|
os.unlink(tmpfile)
|
||||||
|
return f"Entrée ajoutée : {entry}\n{out}"
|
||||||
|
|
||||||
|
if action == "remove":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le pattern à supprimer."
|
||||||
|
current = _run("crontab -l 2>/dev/null")
|
||||||
|
lines = [l for l in current.splitlines() if rest not in l]
|
||||||
|
new_cron = "\n".join(lines)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".cron", delete=False) as f:
|
||||||
|
f.write(new_cron + "\n")
|
||||||
|
tmpfile = f.name
|
||||||
|
|
||||||
|
out = _run(f"crontab {tmpfile}")
|
||||||
|
os.unlink(tmpfile)
|
||||||
|
removed = len(current.splitlines()) - len(lines)
|
||||||
|
return f"{removed} entrée(s) supprimée(s) contenant '{rest}'.\n{out}"
|
||||||
|
|
||||||
|
if action == "clear":
|
||||||
|
return _run("crontab -r 2>/dev/null && echo 'Crontab effacé'")
|
||||||
|
|
||||||
|
if action == "system-list":
|
||||||
|
# Crons système dans /etc/cron.*
|
||||||
|
out = []
|
||||||
|
for d in ["/etc/cron.d", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly"]:
|
||||||
|
files = _run(f"ls {d} 2>/dev/null")
|
||||||
|
if files:
|
||||||
|
out.append(f"{d}:\n{files}")
|
||||||
|
return "\n\n".join(out) or "Aucun cron système trouvé."
|
||||||
|
|
||||||
|
return "Action inconnue. Disponible : list, add, remove, clear, system-list"
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""
|
||||||
|
Skill FILESYSTEM — opérations sur le système de fichiers.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:filesystem ARGS:ls <chemin>
|
||||||
|
SKILL:filesystem ARGS:cat <fichier>
|
||||||
|
SKILL:filesystem ARGS:write <fichier> | <contenu>
|
||||||
|
SKILL:filesystem ARGS:append <fichier> | <contenu>
|
||||||
|
SKILL:filesystem ARGS:delete <chemin>
|
||||||
|
SKILL:filesystem ARGS:mkdir <chemin>
|
||||||
|
SKILL:filesystem ARGS:move <src> | <dst>
|
||||||
|
SKILL:filesystem ARGS:copy <src> | <dst>
|
||||||
|
SKILL:filesystem ARGS:chmod <mode> <chemin>
|
||||||
|
SKILL:filesystem ARGS:chown <owner> <chemin>
|
||||||
|
SKILL:filesystem ARGS:find <chemin> <pattern>
|
||||||
|
SKILL:filesystem ARGS:grep <pattern> <fichier>
|
||||||
|
SKILL:filesystem ARGS:df
|
||||||
|
SKILL:filesystem ARGS:du <chemin>
|
||||||
|
SKILL:filesystem ARGS:stat <chemin>
|
||||||
|
SKILL:filesystem ARGS:tail <fichier> [N]
|
||||||
|
SKILL:filesystem ARGS:head <fichier> [N]
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Opérations filesystem : ls, cat, write, delete, move, copy, chmod, find, grep, df, du"
|
||||||
|
USAGE = "SKILL:filesystem ARGS:ls <path> | cat <file> | write <file>|<content> | delete <path> | find <path> <pattern> | grep <pattern> <file> | df | du <path> | tail <file> [N]"
|
||||||
|
|
||||||
|
# Chemins interdits pour éviter les accidents
|
||||||
|
FORBIDDEN = ["/proc", "/sys", "/dev", "/run/systemd"]
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_path(path: str) -> bool:
|
||||||
|
path = os.path.realpath(path)
|
||||||
|
return not any(path.startswith(f) for f in FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 15) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
# Sépare l'action du reste
|
||||||
|
parts = args.strip().split(None, 1)
|
||||||
|
action = parts[0].lower() if parts else ""
|
||||||
|
rest = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "ls":
|
||||||
|
path = rest or "."
|
||||||
|
return _run(f"ls -lah {path}")
|
||||||
|
|
||||||
|
if action == "cat":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le fichier."
|
||||||
|
if not _safe_path(rest):
|
||||||
|
return f"Accès refusé : {rest}"
|
||||||
|
return _run(f"cat {rest}")
|
||||||
|
|
||||||
|
if action == "tail":
|
||||||
|
parts2 = rest.split()
|
||||||
|
filepath = parts2[0] if parts2 else ""
|
||||||
|
n = parts2[1] if len(parts2) > 1 else "50"
|
||||||
|
return _run(f"tail -n {n} {filepath}")
|
||||||
|
|
||||||
|
if action == "head":
|
||||||
|
parts2 = rest.split()
|
||||||
|
filepath = parts2[0] if parts2 else ""
|
||||||
|
n = parts2[1] if len(parts2) > 1 else "30"
|
||||||
|
return _run(f"head -n {n} {filepath}")
|
||||||
|
|
||||||
|
if action == "write":
|
||||||
|
if "|" not in rest:
|
||||||
|
return "Format : write <fichier> | <contenu>"
|
||||||
|
filepath, content = rest.split("|", 1)
|
||||||
|
filepath = filepath.strip()
|
||||||
|
content = content.strip()
|
||||||
|
if not _safe_path(filepath):
|
||||||
|
return f"Accès refusé : {filepath}"
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return f"Fichier écrit : {filepath} ({len(content)} caractères)"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
if action == "append":
|
||||||
|
if "|" not in rest:
|
||||||
|
return "Format : append <fichier> | <contenu>"
|
||||||
|
filepath, content = rest.split("|", 1)
|
||||||
|
filepath = filepath.strip()
|
||||||
|
if not _safe_path(filepath):
|
||||||
|
return f"Accès refusé : {filepath}"
|
||||||
|
try:
|
||||||
|
with open(filepath, "a") as f:
|
||||||
|
f.write(content.strip() + "\n")
|
||||||
|
return f"Contenu ajouté à {filepath}"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le chemin."
|
||||||
|
if not _safe_path(rest):
|
||||||
|
return f"Accès refusé : {rest}"
|
||||||
|
# Confirmation implicite : on ne supprime pas récursivement sans -r explicite
|
||||||
|
if os.path.isdir(rest):
|
||||||
|
return _run(f"rm -rf {rest}")
|
||||||
|
return _run(f"rm -f {rest}")
|
||||||
|
|
||||||
|
if action == "mkdir":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le chemin."
|
||||||
|
return _run(f"mkdir -p {rest}")
|
||||||
|
|
||||||
|
if action == "move":
|
||||||
|
if "|" not in rest:
|
||||||
|
return "Format : move <src> | <dst>"
|
||||||
|
src, dst = rest.split("|", 1)
|
||||||
|
return _run(f"mv {src.strip()} {dst.strip()}")
|
||||||
|
|
||||||
|
if action == "copy":
|
||||||
|
if "|" not in rest:
|
||||||
|
return "Format : copy <src> | <dst>"
|
||||||
|
src, dst = rest.split("|", 1)
|
||||||
|
return _run(f"cp -r {src.strip()} {dst.strip()}")
|
||||||
|
|
||||||
|
if action == "chmod":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
if len(parts2) < 2:
|
||||||
|
return "Format : chmod <mode> <chemin>"
|
||||||
|
return _run(f"chmod {parts2[0]} {parts2[1]}")
|
||||||
|
|
||||||
|
if action == "chown":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
if len(parts2) < 2:
|
||||||
|
return "Format : chown <owner:group> <chemin>"
|
||||||
|
return _run(f"chown -R {parts2[0]} {parts2[1]}")
|
||||||
|
|
||||||
|
if action == "find":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
path = parts2[0] if parts2 else "."
|
||||||
|
pattern = parts2[1] if len(parts2) > 1 else "*"
|
||||||
|
return _run(f"find {path} -name '{pattern}' 2>/dev/null | head -50")
|
||||||
|
|
||||||
|
if action == "grep":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
if len(parts2) < 2:
|
||||||
|
return "Format : grep <pattern> <fichier>"
|
||||||
|
return _run(f"grep -n '{parts2[0]}' {parts2[1]} 2>/dev/null | head -50")
|
||||||
|
|
||||||
|
if action == "df":
|
||||||
|
return _run("df -h")
|
||||||
|
|
||||||
|
if action == "du":
|
||||||
|
path = rest or "."
|
||||||
|
return _run(f"du -sh {path}/* 2>/dev/null | sort -rh | head -20")
|
||||||
|
|
||||||
|
if action == "stat":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le chemin."
|
||||||
|
return _run(f"stat {rest}")
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Action inconnue. Disponible : ls, cat, tail, head, write, append, delete, "
|
||||||
|
"mkdir, move, copy, chmod, chown, find, grep, df, du, stat"
|
||||||
|
)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
Skill JOURNAL — consultation des logs système via journalctl.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:journal ARGS:tail [N]
|
||||||
|
SKILL:journal ARGS:service <service> [N]
|
||||||
|
SKILL:journal ARGS:boot [N]
|
||||||
|
SKILL:journal ARGS:errors [N]
|
||||||
|
SKILL:journal ARGS:since <durée> (ex: "1h", "30min", "yesterday")
|
||||||
|
SKILL:journal ARGS:grep <pattern> [service]
|
||||||
|
SKILL:journal ARGS:kernel [N]
|
||||||
|
SKILL:journal ARGS:disk-usage
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Consultation des logs système via journalctl et /var/log"
|
||||||
|
USAGE = "SKILL:journal ARGS:tail [N] | service <service> [N] | errors [N] | since <durée> | grep <pattern> | boot | kernel"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 15) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
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 "tail"
|
||||||
|
rest = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "tail":
|
||||||
|
n = rest.strip() or "50"
|
||||||
|
return _run(f"journalctl -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "service":
|
||||||
|
parts2 = rest.split()
|
||||||
|
service = parts2[0] if parts2 else ""
|
||||||
|
n = parts2[1] if len(parts2) > 1 else "50"
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"journalctl -u {service} -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "boot":
|
||||||
|
n = rest.strip() or "50"
|
||||||
|
return _run(f"journalctl -b -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "errors":
|
||||||
|
n = rest.strip() or "30"
|
||||||
|
return _run(f"journalctl -p err..emerg -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "warnings":
|
||||||
|
n = rest.strip() or "30"
|
||||||
|
return _run(f"journalctl -p warning..emerg -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "since":
|
||||||
|
if not rest:
|
||||||
|
return "Précise la durée (ex: '1h', '30min', 'yesterday', '2024-01-01')"
|
||||||
|
# Convertit "1h" → "1 hour ago", "30min" → "30 minutes ago"
|
||||||
|
since = rest.strip()
|
||||||
|
if since.endswith("h") and since[:-1].isdigit():
|
||||||
|
since = f"{since[:-1]} hours ago"
|
||||||
|
elif since.endswith("min") and since[:-3].isdigit():
|
||||||
|
since = f"{since[:-3]} minutes ago"
|
||||||
|
return _run(f"journalctl --since='{since}' --no-pager -o short-iso | tail -100")
|
||||||
|
|
||||||
|
if action == "grep":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
pattern = parts2[0] if parts2 else ""
|
||||||
|
service = parts2[1].strip() if len(parts2) > 1 else ""
|
||||||
|
if not pattern:
|
||||||
|
return "Précise le pattern."
|
||||||
|
cmd = f"journalctl --no-pager -o short-iso"
|
||||||
|
if service:
|
||||||
|
cmd += f" -u {service}"
|
||||||
|
cmd += f" | grep -i '{pattern}' | tail -50"
|
||||||
|
return _run(cmd)
|
||||||
|
|
||||||
|
if action == "kernel":
|
||||||
|
n = rest.strip() or "30"
|
||||||
|
return _run(f"journalctl -k -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "disk-usage":
|
||||||
|
return _run("journalctl --disk-usage")
|
||||||
|
|
||||||
|
if action == "vacuum":
|
||||||
|
# Nettoyage des vieux logs
|
||||||
|
size = rest.strip() or "500M"
|
||||||
|
return _run(f"journalctl --vacuum-size={size}")
|
||||||
|
|
||||||
|
if action == "file":
|
||||||
|
# Lire un fichier de log classique
|
||||||
|
filepath = rest.strip()
|
||||||
|
if not filepath:
|
||||||
|
return "Précise le fichier."
|
||||||
|
return _run(f"tail -100 {filepath}")
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Action inconnue. Disponible : tail, service, boot, errors, warnings, "
|
||||||
|
"since, grep, kernel, disk-usage, vacuum, file"
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Skill MQTT_SEND — publier un message sur n'importe quel topic MQTT.
|
||||||
|
Permet à l'agent de communiquer proactivement avec d'autres agents.
|
||||||
|
|
||||||
|
Usage LLM : SKILL:mqtt_send ARGS:<topic> | <message>
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
topic = topic.strip()
|
||||||
|
message = message.strip()
|
||||||
|
|
||||||
|
if not topic:
|
||||||
|
return "Topic vide."
|
||||||
|
|
||||||
|
context.mqtt.publish_raw(topic, message)
|
||||||
|
return f"Message publié sur '{topic}'."
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
"""
|
||||||
|
Skill NETWORK — administration réseau.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:network ARGS:ip [show|route|link]
|
||||||
|
SKILL:network ARGS:ping <hôte> [count]
|
||||||
|
SKILL:network ARGS:traceroute <hôte>
|
||||||
|
SKILL:network ARGS:dns <nom>
|
||||||
|
SKILL:network ARGS:ports [tcp|udp]
|
||||||
|
SKILL:network ARGS:connections
|
||||||
|
SKILL:network ARGS:firewall status
|
||||||
|
SKILL:network ARGS:firewall allow <port/service>
|
||||||
|
SKILL:network ARGS:firewall deny <port/service>
|
||||||
|
SKILL:network ARGS:firewall delete <règle>
|
||||||
|
SKILL:network ARGS:firewall list
|
||||||
|
SKILL:network ARGS:bandwidth [interface]
|
||||||
|
SKILL:network ARGS:hosts [add|remove] <ip> <nom>
|
||||||
|
SKILL:network ARGS:wget <url>
|
||||||
|
SKILL:network ARGS:curl <url>
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Administration réseau : ip, ping, traceroute, DNS, ports, firewall ufw/iptables"
|
||||||
|
USAGE = "SKILL:network ARGS:ip | ping <host> | traceroute <host> | dns <host> | ports | connections | firewall status|allow|deny|list | wget <url>"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 20) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
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 ""
|
||||||
|
rest = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "ip":
|
||||||
|
sub = rest.strip().lower() or "show"
|
||||||
|
if sub == "show" or sub == "addr":
|
||||||
|
return _run("ip -br addr show")
|
||||||
|
if sub == "route":
|
||||||
|
return _run("ip route show")
|
||||||
|
if sub == "link":
|
||||||
|
return _run("ip -br link show")
|
||||||
|
if sub == "full":
|
||||||
|
return _run("ip addr show")
|
||||||
|
return _run(f"ip {rest}")
|
||||||
|
|
||||||
|
if action == "ping":
|
||||||
|
parts2 = rest.split()
|
||||||
|
host = parts2[0] if parts2 else ""
|
||||||
|
count = parts2[1] if len(parts2) > 1 else "4"
|
||||||
|
if not host:
|
||||||
|
return "Précise l'hôte."
|
||||||
|
return _run(f"ping -c {count} {host}", timeout=int(count) * 3 + 5)
|
||||||
|
|
||||||
|
if action == "traceroute":
|
||||||
|
host = rest.strip()
|
||||||
|
if not host:
|
||||||
|
return "Précise l'hôte."
|
||||||
|
return _run(f"traceroute -m 15 {host}", timeout=60)
|
||||||
|
|
||||||
|
if action == "dns":
|
||||||
|
host = rest.strip()
|
||||||
|
if not host:
|
||||||
|
return "Précise le nom à résoudre."
|
||||||
|
return _run(f"dig +short {host} && dig +short -x $(dig +short {host} | head -1) 2>/dev/null || nslookup {host}")
|
||||||
|
|
||||||
|
if action == "ports":
|
||||||
|
proto = rest.strip().lower()
|
||||||
|
if proto == "udp":
|
||||||
|
return _run("ss -ulnp")
|
||||||
|
return _run("ss -tlnp")
|
||||||
|
|
||||||
|
if action == "connections":
|
||||||
|
return _run("ss -tunp | head -50")
|
||||||
|
|
||||||
|
if action == "netstat":
|
||||||
|
return _run("ss -s")
|
||||||
|
|
||||||
|
if action == "firewall":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
sub = parts2[0].lower() if parts2 else "status"
|
||||||
|
arg = parts2[1] if len(parts2) > 1 else ""
|
||||||
|
|
||||||
|
# Détecte ufw ou iptables
|
||||||
|
ufw_available = _run("which ufw") != ""
|
||||||
|
|
||||||
|
if sub == "status":
|
||||||
|
if ufw_available:
|
||||||
|
return _run("ufw status verbose")
|
||||||
|
return _run("iptables -L -n -v --line-numbers")
|
||||||
|
|
||||||
|
if sub == "allow":
|
||||||
|
if not arg:
|
||||||
|
return "Précise le port/service."
|
||||||
|
if ufw_available:
|
||||||
|
return _run(f"ufw allow {arg}")
|
||||||
|
return _run(f"iptables -A INPUT -p tcp --dport {arg} -j ACCEPT")
|
||||||
|
|
||||||
|
if sub == "deny":
|
||||||
|
if not arg:
|
||||||
|
return "Précise le port/service."
|
||||||
|
if ufw_available:
|
||||||
|
return _run(f"ufw deny {arg}")
|
||||||
|
return _run(f"iptables -A INPUT -p tcp --dport {arg} -j DROP")
|
||||||
|
|
||||||
|
if sub == "delete":
|
||||||
|
if not arg:
|
||||||
|
return "Précise la règle ou le numéro."
|
||||||
|
if ufw_available:
|
||||||
|
return _run(f"ufw delete {arg}")
|
||||||
|
return _run(f"iptables -D INPUT {arg}")
|
||||||
|
|
||||||
|
if sub == "list":
|
||||||
|
if ufw_available:
|
||||||
|
return _run("ufw status numbered")
|
||||||
|
return _run("iptables -L INPUT -n -v --line-numbers")
|
||||||
|
|
||||||
|
if sub == "enable":
|
||||||
|
return _run("ufw --force enable") if ufw_available else "ufw non disponible."
|
||||||
|
|
||||||
|
if sub == "disable":
|
||||||
|
return _run("ufw disable") if ufw_available else "ufw non disponible."
|
||||||
|
|
||||||
|
return f"Sous-commande firewall inconnue : {sub}"
|
||||||
|
|
||||||
|
if action == "bandwidth":
|
||||||
|
iface = rest.strip() or "eth0"
|
||||||
|
# ifstat ou /proc/net/dev si pas dispo
|
||||||
|
result = _run(f"cat /proc/net/dev | grep {iface}")
|
||||||
|
if not result:
|
||||||
|
return f"Interface {iface} introuvable."
|
||||||
|
return result
|
||||||
|
|
||||||
|
if action == "hosts":
|
||||||
|
parts2 = rest.split()
|
||||||
|
sub = parts2[0].lower() if parts2 else "list"
|
||||||
|
if sub == "list" or not parts2:
|
||||||
|
return _run("cat /etc/hosts")
|
||||||
|
if sub == "add" and len(parts2) >= 3:
|
||||||
|
ip, name = parts2[1], parts2[2]
|
||||||
|
return _run(f"echo '{ip} {name}' >> /etc/hosts && echo 'Ajouté : {ip} {name}'")
|
||||||
|
if sub == "remove" and len(parts2) >= 2:
|
||||||
|
name = parts2[1]
|
||||||
|
return _run(f"sed -i '/{name}/d' /etc/hosts && echo 'Supprimé : {name}'")
|
||||||
|
return "Usage : hosts list | add <ip> <nom> | remove <nom>"
|
||||||
|
|
||||||
|
if action == "wget":
|
||||||
|
url = rest.strip()
|
||||||
|
if not url:
|
||||||
|
return "Précise l'URL."
|
||||||
|
return _run(f"wget -q --spider {url} && echo 'URL accessible' || echo 'URL inaccessible'")
|
||||||
|
|
||||||
|
if action == "curl":
|
||||||
|
url = rest.strip()
|
||||||
|
if not url:
|
||||||
|
return "Précise l'URL."
|
||||||
|
return _run(f"curl -sI {url} | head -10")
|
||||||
|
|
||||||
|
if action == "arp":
|
||||||
|
return _run("arp -n")
|
||||||
|
|
||||||
|
if action == "hostname":
|
||||||
|
return _run("hostname -f")
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Action inconnue. Disponible : ip, ping, traceroute, dns, ports, connections, "
|
||||||
|
"firewall, hosts, wget, curl, arp, hostname, netstat"
|
||||||
|
)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
Skill PROCESS — gestion des processus.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:process ARGS:list [filtre]
|
||||||
|
SKILL:process ARGS:top [N]
|
||||||
|
SKILL:process ARGS:kill <pid> [signal]
|
||||||
|
SKILL:process ARGS:killall <nom>
|
||||||
|
SKILL:process ARGS:nice <pid> <priorité>
|
||||||
|
SKILL:process ARGS:info <pid>
|
||||||
|
SKILL:process ARGS:tree
|
||||||
|
SKILL:process ARGS:find <nom>
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Gestion des processus : list, top, kill, killall, nice, find"
|
||||||
|
USAGE = "SKILL:process ARGS:list [filtre] | top [N] | kill <pid> [signal] | killall <nom> | info <pid> | tree | find <nom>"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 10) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout
|
||||||
|
)
|
||||||
|
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 == "list":
|
||||||
|
cmd = "ps aux --sort=-%cpu | head -30"
|
||||||
|
if rest:
|
||||||
|
cmd = f"ps aux | grep -i {rest} | grep -v grep"
|
||||||
|
return _run(cmd)
|
||||||
|
|
||||||
|
if action == "top":
|
||||||
|
n = rest.strip() or "15"
|
||||||
|
return _run(
|
||||||
|
f"ps aux --sort=-%cpu | head -{n} | "
|
||||||
|
"awk 'NR==1{print} NR>1{printf \"%-10s %-6s %-5s %-5s %s\\n\",$1,$2,$3,$4,$11}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "kill":
|
||||||
|
parts2 = rest.split()
|
||||||
|
if not parts2:
|
||||||
|
return "Précise le PID."
|
||||||
|
pid = parts2[0]
|
||||||
|
signal = parts2[1] if len(parts2) > 1 else "15"
|
||||||
|
return _run(f"kill -{signal} {pid}")
|
||||||
|
|
||||||
|
if action == "kill9":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le PID."
|
||||||
|
return _run(f"kill -9 {rest.strip()}")
|
||||||
|
|
||||||
|
if action == "killall":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom du processus."
|
||||||
|
return _run(f"killall {rest.strip()}")
|
||||||
|
|
||||||
|
if action == "nice":
|
||||||
|
parts2 = rest.split()
|
||||||
|
if len(parts2) < 2:
|
||||||
|
return "Format : nice <pid> <priorité (-20 à 19)>"
|
||||||
|
pid, prio = parts2[0], parts2[1]
|
||||||
|
return _run(f"renice {prio} -p {pid}")
|
||||||
|
|
||||||
|
if action == "info":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le PID."
|
||||||
|
pid = rest.strip()
|
||||||
|
out = _run(f"ps -p {pid} -o pid,ppid,user,%cpu,%mem,vsz,rss,stat,start,time,comm --no-headers")
|
||||||
|
cmdline = _run(f"cat /proc/{pid}/cmdline 2>/dev/null | tr '\\0' ' '")
|
||||||
|
return f"Process {pid}:\n{out}\nCmdline: {cmdline}"
|
||||||
|
|
||||||
|
if action == "tree":
|
||||||
|
return _run("pstree -p | head -50")
|
||||||
|
|
||||||
|
if action == "find":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom du processus."
|
||||||
|
return _run(f"pgrep -a -i {rest.strip()}")
|
||||||
|
|
||||||
|
if action == "lsof":
|
||||||
|
# Fichiers ouverts par un processus
|
||||||
|
if rest:
|
||||||
|
return _run(f"lsof -p {rest.strip()} | head -30")
|
||||||
|
return _run("lsof | wc -l && echo fichiers ouverts au total")
|
||||||
|
|
||||||
|
return "Action inconnue. Disponible : list, top, kill, kill9, killall, nice, info, tree, find, lsof"
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
Skill SCRIPT — créer et exécuter un script bash, avec renvoi du résultat via MQTT.
|
||||||
|
|
||||||
|
L'environnement du script expose automatiquement :
|
||||||
|
MQTT_BROKER, MQTT_REPLY_TOPIC, AGENT_ID
|
||||||
|
|
||||||
|
Ainsi un script peut publier son résultat directement :
|
||||||
|
mosquitto_pub -h $MQTT_BROKER -t $MQTT_REPLY_TOPIC -m "mon résultat"
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:script ARGS:run | <contenu du script>
|
||||||
|
SKILL:script ARGS:save <nom> | <contenu>
|
||||||
|
SKILL:script ARGS:exec <nom> [args]
|
||||||
|
SKILL:script ARGS:list
|
||||||
|
SKILL:script ARGS:show <nom>
|
||||||
|
SKILL:script ARGS:delete <nom>
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import stat
|
||||||
|
|
||||||
|
DESCRIPTION = "Créer/exécuter des scripts bash avec renvoi du résultat via MQTT"
|
||||||
|
USAGE = "SKILL:script ARGS:run|<contenu> | save <nom>|<contenu> | exec <nom> | list | show <nom>"
|
||||||
|
|
||||||
|
SCRIPTS_DIR = "/opt/agent_debian/scripts"
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_dir():
|
||||||
|
os.makedirs(SCRIPTS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, env: dict = None, timeout: int = 60) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, text=True,
|
||||||
|
capture_output=True, timeout=timeout,
|
||||||
|
env=env, 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 f"Timeout ({timeout}s)"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_env(context) -> dict:
|
||||||
|
"""Environnement injecté dans chaque script."""
|
||||||
|
env = os.environ.copy()
|
||||||
|
mc = context.config.get("mqtt", {})
|
||||||
|
env["MQTT_BROKER"] = mc.get("host", "localhost")
|
||||||
|
env["MQTT_PORT"] = str(mc.get("port", 1883))
|
||||||
|
env["MQTT_REPLY_TOPIC"] = "agents/nexus/inbox"
|
||||||
|
env["AGENT_ID"] = context.agent_id
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
_ensure_dir()
|
||||||
|
parts = args.strip().split(None, 1)
|
||||||
|
action = parts[0].lower() if parts else "run"
|
||||||
|
rest = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "run":
|
||||||
|
# Exécution directe d'un script inline
|
||||||
|
if not rest:
|
||||||
|
return "Précise le contenu du script."
|
||||||
|
content = rest.replace("\\n", "\n")
|
||||||
|
# Fichier temporaire
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".sh", delete=False, dir="/tmp"
|
||||||
|
) as f:
|
||||||
|
f.write("#!/bin/bash\nset -e\n" + content)
|
||||||
|
tmpfile = f.name
|
||||||
|
os.chmod(tmpfile, stat.S_IRWXU)
|
||||||
|
env = _build_env(context)
|
||||||
|
out = _run(tmpfile, env=env, timeout=60)
|
||||||
|
os.unlink(tmpfile)
|
||||||
|
return out
|
||||||
|
|
||||||
|
if action == "save":
|
||||||
|
if "|" not in rest:
|
||||||
|
return "Format : save <nom> | <contenu du script>"
|
||||||
|
name, content = rest.split("|", 1)
|
||||||
|
name = name.strip().replace("/", "_") # Sécurité
|
||||||
|
content = content.strip().replace("\\n", "\n")
|
||||||
|
path = os.path.join(SCRIPTS_DIR, name + ".sh")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write("#!/bin/bash\n" + content)
|
||||||
|
os.chmod(path, stat.S_IRWXU)
|
||||||
|
return f"Script sauvegardé : {path}"
|
||||||
|
|
||||||
|
if action == "exec":
|
||||||
|
parts2 = rest.split(None, 1)
|
||||||
|
name = parts2[0] if parts2 else ""
|
||||||
|
sargs = parts2[1] if len(parts2) > 1 else ""
|
||||||
|
if not name:
|
||||||
|
return "Précise le nom du script."
|
||||||
|
path = os.path.join(SCRIPTS_DIR, name + ".sh")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return f"Script '{name}' introuvable dans {SCRIPTS_DIR}"
|
||||||
|
env = _build_env(context)
|
||||||
|
return _run(f"{path} {sargs}", env=env, timeout=120)
|
||||||
|
|
||||||
|
if action == "list":
|
||||||
|
files = [f for f in os.listdir(SCRIPTS_DIR) if f.endswith(".sh")]
|
||||||
|
return "\n".join(files) if files else "Aucun script sauvegardé."
|
||||||
|
|
||||||
|
if action == "show":
|
||||||
|
name = rest.strip()
|
||||||
|
if not name:
|
||||||
|
return "Précise le nom du script."
|
||||||
|
path = os.path.join(SCRIPTS_DIR, name + ".sh")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return f"Script '{name}' introuvable."
|
||||||
|
with open(path) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
name = rest.strip()
|
||||||
|
if not name:
|
||||||
|
return "Précise le nom du script."
|
||||||
|
path = os.path.join(SCRIPTS_DIR, name + ".sh")
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
return f"Script '{name}' supprimé."
|
||||||
|
return f"Script '{name}' introuvable."
|
||||||
|
|
||||||
|
return "Action inconnue. Disponible : run, save, exec, list, show, delete"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Skill SHELL — exécution de commandes shell arbitraires.
|
||||||
|
Skill de dernier recours quand aucun skill spécialisé ne convient.
|
||||||
|
|
||||||
|
Usage LLM : SKILL:shell ARGS:<commande bash>
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Exécution de commandes shell arbitraires (fallback général)"
|
||||||
|
USAGE = "SKILL:shell ARGS:<commande bash complète>"
|
||||||
|
|
||||||
|
# Commandes bloquées pour éviter les accidents critiques
|
||||||
|
BLOCKED = [
|
||||||
|
"rm -rf /",
|
||||||
|
"dd if=/dev/zero of=/dev/",
|
||||||
|
"mkfs",
|
||||||
|
"> /dev/sda",
|
||||||
|
":(){ :|:& };:", # fork bomb
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
cmd = args.strip()
|
||||||
|
if not cmd:
|
||||||
|
return "Commande vide."
|
||||||
|
|
||||||
|
# Vérification des commandes dangereuses
|
||||||
|
for blocked in BLOCKED:
|
||||||
|
if blocked in cmd:
|
||||||
|
return f"Commande bloquée pour sécurité : {blocked}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
shell=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=60,
|
||||||
|
executable="/bin/bash",
|
||||||
|
)
|
||||||
|
stdout = result.stdout.strip()
|
||||||
|
stderr = result.stderr.strip()
|
||||||
|
returncode = result.returncode
|
||||||
|
|
||||||
|
output = ""
|
||||||
|
if stdout:
|
||||||
|
output += stdout
|
||||||
|
if stderr:
|
||||||
|
output += ("\n" if output else "") + f"[stderr] {stderr}"
|
||||||
|
if not output:
|
||||||
|
output = f"(Commande exécutée, code retour : {returncode})"
|
||||||
|
|
||||||
|
# Tronqué à 4000 caractères
|
||||||
|
if len(output) > 4000:
|
||||||
|
output = output[:4000] + f"\n... [tronqué, {len(output)} caractères total]"
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "Timeout (60s) — commande trop longue."
|
||||||
|
except Exception as e:
|
||||||
|
return f"Erreur : {e}"
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Skill SYSINFO — informations système complètes.
|
||||||
|
|
||||||
|
Usage LLM : SKILL:sysinfo ARGS:all | cpu | mem | disk | uptime | load | net
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Informations système : CPU, RAM, disque, uptime, charge, réseau"
|
||||||
|
USAGE = "SKILL:sysinfo ARGS:all | cpu | mem | disk | uptime | load | net"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str) -> str:
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.STDOUT).strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return e.output.strip() or str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
what = args.strip().lower() or "all"
|
||||||
|
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
if what in ("all", "uptime"):
|
||||||
|
uptime = _run("uptime -p")
|
||||||
|
since = _run("uptime -s")
|
||||||
|
sections.append(f"Uptime : {uptime} (depuis {since})")
|
||||||
|
|
||||||
|
if what in ("all", "load"):
|
||||||
|
load = _run("cat /proc/loadavg")
|
||||||
|
cpus = _run("nproc")
|
||||||
|
sections.append(f"Charge système : {load} ({cpus} CPU)")
|
||||||
|
|
||||||
|
if what in ("all", "cpu"):
|
||||||
|
cpu_info = _run("lscpu | grep -E 'Model name|CPU\\(s\\)|MHz'")
|
||||||
|
cpu_usage = _run(
|
||||||
|
"top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4\"%\"}'"
|
||||||
|
)
|
||||||
|
sections.append(f"CPU :\n{cpu_info}\nUtilisation : {cpu_usage}")
|
||||||
|
|
||||||
|
if what in ("all", "mem"):
|
||||||
|
mem = _run("free -h")
|
||||||
|
sections.append(f"Mémoire :\n{mem}")
|
||||||
|
|
||||||
|
if what in ("all", "disk"):
|
||||||
|
disk = _run("df -h --output=source,size,used,avail,pcent,target | column -t")
|
||||||
|
sections.append(f"Disques :\n{disk}")
|
||||||
|
|
||||||
|
if what in ("all", "net"):
|
||||||
|
ifaces = _run("ip -br addr show")
|
||||||
|
sections.append(f"Interfaces réseau :\n{ifaces}")
|
||||||
|
|
||||||
|
if what == "os":
|
||||||
|
osinfo = _run("cat /etc/os-release | grep -E '^(NAME|VERSION)='")
|
||||||
|
kernel = _run("uname -r")
|
||||||
|
sections.append(f"OS :\n{osinfo}\nKernel : {kernel}")
|
||||||
|
|
||||||
|
if not sections:
|
||||||
|
return (
|
||||||
|
"Option inconnue. Utilise : all, cpu, mem, disk, uptime, load, net, os"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n\n".join(sections)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
Skill SYSTEMD — gestion des services systemd.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:systemd ARGS:status <service>
|
||||||
|
SKILL:systemd ARGS:start <service>
|
||||||
|
SKILL:systemd ARGS:stop <service>
|
||||||
|
SKILL:systemd ARGS:restart <service>
|
||||||
|
SKILL:systemd ARGS:reload <service>
|
||||||
|
SKILL:systemd ARGS:enable <service>
|
||||||
|
SKILL:systemd ARGS:disable <service>
|
||||||
|
SKILL:systemd ARGS:logs <service> [lignes]
|
||||||
|
SKILL:systemd ARGS:list [pattern]
|
||||||
|
SKILL:systemd ARGS:failed
|
||||||
|
SKILL:systemd ARGS:daemon-reload
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Gestion des services systemd (start/stop/restart/status/logs/enable/disable)"
|
||||||
|
USAGE = "SKILL:systemd ARGS:status <service> | start <service> | stop <service> | restart <service> | enable <service> | disable <service> | logs <service> [N] | list | failed"
|
||||||
|
|
||||||
|
|
||||||
|
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[: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()
|
||||||
|
action = parts[0].lower() if parts else ""
|
||||||
|
service = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if action == "status":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl status {service} --no-pager -l")
|
||||||
|
|
||||||
|
if action == "start":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
out = _run(f"systemctl start {service}")
|
||||||
|
status = _run(f"systemctl is-active {service}")
|
||||||
|
return f"Démarrage de {service}... Statut : {status}\n{out}"
|
||||||
|
|
||||||
|
if action == "stop":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
out = _run(f"systemctl stop {service}")
|
||||||
|
status = _run(f"systemctl is-active {service}")
|
||||||
|
return f"Arrêt de {service}... Statut : {status}\n{out}"
|
||||||
|
|
||||||
|
if action == "restart":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
out = _run(f"systemctl restart {service}")
|
||||||
|
status = _run(f"systemctl is-active {service}")
|
||||||
|
return f"Redémarrage de {service}... Statut : {status}\n{out}"
|
||||||
|
|
||||||
|
if action == "reload":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl reload {service}")
|
||||||
|
|
||||||
|
if action == "enable":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl enable {service}")
|
||||||
|
|
||||||
|
if action == "disable":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl disable {service}")
|
||||||
|
|
||||||
|
if action == "mask":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl mask {service}")
|
||||||
|
|
||||||
|
if action == "unmask":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl unmask {service}")
|
||||||
|
|
||||||
|
if action == "logs":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
n = parts[2] if len(parts) > 2 else "50"
|
||||||
|
try:
|
||||||
|
n = int(n)
|
||||||
|
except ValueError:
|
||||||
|
n = 50
|
||||||
|
return _run(f"journalctl -u {service} -n {n} --no-pager -o short-iso")
|
||||||
|
|
||||||
|
if action == "list":
|
||||||
|
pattern = parts[1] if len(parts) > 1 else ""
|
||||||
|
cmd = "systemctl list-units --type=service --no-pager"
|
||||||
|
if pattern:
|
||||||
|
cmd += f" | grep {pattern}"
|
||||||
|
return _run(cmd)
|
||||||
|
|
||||||
|
if action == "list-all":
|
||||||
|
return _run("systemctl list-units --type=service --all --no-pager")
|
||||||
|
|
||||||
|
if action == "failed":
|
||||||
|
return _run("systemctl list-units --state=failed --no-pager")
|
||||||
|
|
||||||
|
if action == "daemon-reload":
|
||||||
|
return _run("systemctl daemon-reload")
|
||||||
|
|
||||||
|
if action == "is-active":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl is-active {service}")
|
||||||
|
|
||||||
|
if action == "is-enabled":
|
||||||
|
if not service:
|
||||||
|
return "Précise le service."
|
||||||
|
return _run(f"systemctl is-enabled {service}")
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Action inconnue. Disponible : status, start, stop, restart, reload, "
|
||||||
|
"enable, disable, mask, unmask, logs, list, list-all, failed, "
|
||||||
|
"daemon-reload, is-active, is-enabled"
|
||||||
|
)
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
Skill USER — gestion des utilisateurs et groupes.
|
||||||
|
|
||||||
|
Usage LLM :
|
||||||
|
SKILL:user ARGS:list
|
||||||
|
SKILL:user ARGS:add <nom> [--sudo]
|
||||||
|
SKILL:user ARGS:delete <nom>
|
||||||
|
SKILL:user ARGS:passwd <nom>
|
||||||
|
SKILL:user ARGS:info <nom>
|
||||||
|
SKILL:user ARGS:groups <nom>
|
||||||
|
SKILL:user ARGS:addgroup <nom> <groupe>
|
||||||
|
SKILL:user ARGS:removegroup <nom> <groupe>
|
||||||
|
SKILL:user ARGS:lock <nom>
|
||||||
|
SKILL:user ARGS:unlock <nom>
|
||||||
|
SKILL:user ARGS:whoami
|
||||||
|
SKILL:user ARGS:logged
|
||||||
|
SKILL:user ARGS:sudoers
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DESCRIPTION = "Gestion utilisateurs et groupes : add, delete, passwd, groups, lock/unlock, sudoers"
|
||||||
|
USAGE = "SKILL:user ARGS:list | add <nom> [--sudo] | delete <nom> | passwd <nom> | info <nom> | groups <nom> | addgroup <nom> <groupe> | lock <nom> | unlock <nom> | logged"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: str, timeout: int = 15) -> 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 subprocess.TimeoutExpired:
|
||||||
|
return f"Timeout ({timeout}s)"
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: str, context) -> str:
|
||||||
|
parts = args.strip().split()
|
||||||
|
action = parts[0].lower() if parts else "list"
|
||||||
|
rest = parts[1:] if len(parts) > 1 else []
|
||||||
|
|
||||||
|
if action == "list":
|
||||||
|
return _run("getent passwd | awk -F: '$3>=1000 || $3==0 {print $1\" (uid=\"$3\", shell=\"$7\")\"}'")
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
name = rest[0]
|
||||||
|
sudo = "--sudo" in rest or "-s" in rest
|
||||||
|
cmds = [
|
||||||
|
f"adduser --gecos '' --disabled-password {name}",
|
||||||
|
]
|
||||||
|
if sudo:
|
||||||
|
cmds.append(f"usermod -aG sudo {name}")
|
||||||
|
return "\n".join(_run(c) for c in cmds)
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
return _run(f"deluser --remove-home {rest[0]}")
|
||||||
|
|
||||||
|
if action == "passwd":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
# Génère un mot de passe aléatoire
|
||||||
|
pwd = _run("openssl rand -base64 12").strip()
|
||||||
|
out = _run(f"echo '{rest[0]}:{pwd}' | chpasswd")
|
||||||
|
return f"{out}\nNouveauMDP : {pwd}"
|
||||||
|
|
||||||
|
if action == "info":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
name = rest[0]
|
||||||
|
return _run(f"id {name} && getent passwd {name}")
|
||||||
|
|
||||||
|
if action == "groups":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
return _run(f"groups {rest[0]}")
|
||||||
|
|
||||||
|
if action == "addgroup":
|
||||||
|
if len(rest) < 2:
|
||||||
|
return "Format : addgroup <utilisateur> <groupe>"
|
||||||
|
return _run(f"usermod -aG {rest[1]} {rest[0]}")
|
||||||
|
|
||||||
|
if action == "removegroup":
|
||||||
|
if len(rest) < 2:
|
||||||
|
return "Format : removegroup <utilisateur> <groupe>"
|
||||||
|
return _run(f"gpasswd -d {rest[0]} {rest[1]}")
|
||||||
|
|
||||||
|
if action == "lock":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
return _run(f"usermod -L {rest[0]}")
|
||||||
|
|
||||||
|
if action == "unlock":
|
||||||
|
if not rest:
|
||||||
|
return "Précise le nom d'utilisateur."
|
||||||
|
return _run(f"usermod -U {rest[0]}")
|
||||||
|
|
||||||
|
if action == "whoami":
|
||||||
|
return _run("whoami && id")
|
||||||
|
|
||||||
|
if action == "logged":
|
||||||
|
return _run("who && echo '---' && last -n 10")
|
||||||
|
|
||||||
|
if action == "sudoers":
|
||||||
|
return _run("getent group sudo | cut -d: -f4")
|
||||||
|
|
||||||
|
if action == "ssh-key":
|
||||||
|
# Ajouter une clé SSH pour un utilisateur
|
||||||
|
if len(rest) < 2:
|
||||||
|
return "Format : ssh-key <utilisateur> <clé_publique>"
|
||||||
|
name = rest[0]
|
||||||
|
key = " ".join(rest[1:])
|
||||||
|
return _run(
|
||||||
|
f"mkdir -p /home/{name}/.ssh && "
|
||||||
|
f"echo '{key}' >> /home/{name}/.ssh/authorized_keys && "
|
||||||
|
f"chmod 700 /home/{name}/.ssh && "
|
||||||
|
f"chmod 600 /home/{name}/.ssh/authorized_keys && "
|
||||||
|
f"chown -R {name}:{name} /home/{name}/.ssh && "
|
||||||
|
f"echo 'Clé ajoutée pour {name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "Action inconnue. Disponible : list, add, delete, passwd, info, groups, addgroup, removegroup, lock, unlock, whoami, logged, sudoers, ssh-key"
|
||||||
Reference in New Issue
Block a user