docs(admin): add ops documentation and fix hermes dashboard service

- Remove admin/ from .gitignore (repo is now private)
- Add --tui flag to hermes-dashboard.service template
- Document hermes-tui wrapper, dashboard access, TUI usage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-05-12 18:50:43 +02:00
parent 1544a2fc6d
commit dca71144d6
17 changed files with 1979 additions and 4 deletions

3
.gitignore vendored
View file

@ -24,9 +24,6 @@ progress/
# Documentation réseau domestique (topologie LAN privée)
Funk/
# Docs ops (IPs, architecture détaillée, procédures internes)
admin/
# Divers
.DS_Store
*.swp

20
admin/README.md Normal file
View file

@ -0,0 +1,20 @@
# Administration Funk — Index
Commandes utiles pour administrer, débugger et comprendre l'infrastructure.
| Fichier | Contenu |
|---|---|
| [ansible.md](ansible.md) | Commandes Ansible — ping, apply, vault, debug, facts |
| [ssh.md](ssh.md) | Connexions SSH — alias, jump host, transfert de fichiers |
| [reseau.md](reseau.md) | nftables, IP forwarding, routage, debug NAT |
| [dnsmasq.md](dnsmasq.md) | DNS local lab.local — résolution, logs, ajout d'entrées |
| [rocm.md](rocm.md) | GPU AMD RX 6700XT — ROCm, HSA_OVERRIDE, debug |
| [llama_server.md](llama_server.md) | llama-server — inférence ROCm 7.x, API OpenAI, build, modèles |
| [lm_studio.md](lm_studio.md) | LM Studio — gestion des modèles uniquement (lms get/ls) |
| [nfs.md](nfs.md) | NFS + RAID5 — exports, montages, automount, ajout de partages |
| [systeme.md](systeme.md) | Services, logs, ressources, NetworkManager, dnf |
| [litellm.md](litellm.md) | LiteLLM Proxy — routage Qwen local + Claude API, config, validation |
| [hermes.md](hermes.md) | Hermes Agent — service, config, modèle, API, debug |
| [incidents.md](incidents.md) | Incidents passés — causes, résolutions, fixes permanents |
| **Gestion cluster** | |
| `funk-start` / `funk-stop` / `funk-status` | Aliases locaux → `ssh storage-01 "sudo /usr/local/bin/funk-cluster [start\|stop\|status]"` |

107
admin/ansible.md Normal file
View file

@ -0,0 +1,107 @@
# Ansible — Commandes d'administration
Toutes les commandes se lancent depuis la racine du repo (`lab-infra/`).
Le venv est dans `.venv/`, le `Makefile` fournit les raccourcis.
---
## Raccourcis Makefile
```bash
make ping # Vérifie que toutes les machines répondent
make check # Dry-run complet (simule sans appliquer)
make apply # Applique tous les playbooks (storage-01 + gpu-01)
make apply-storage # Applique uniquement storage-01
make apply-gpu # Applique uniquement gpu-01
make install # Installe ansible-core + collections Galaxy
make vault-encrypt # Chiffre group_vars/all/vault.yml
make vault-edit # Édite le vault (déchiffre → édite → rechiffre)
make vault-view # Affiche le contenu du vault déchiffré
```
---
## Commandes Ansible directes
### Cibler un rôle spécifique
```bash
cd ansible/
# Via tag (si les rôles ont des tags)
../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags dnsmasq
# Via limite d'hôte
../.venv/bin/ansible-playbook playbooks/site.yml -l gpu-01
```
### Exécuter une commande ad-hoc sur une machine
```bash
cd ansible/
../.venv/bin/ansible storage-01 -m shell -a "systemctl status dnsmasq"
../.venv/bin/ansible all -m shell -a "uptime"
../.venv/bin/ansible gpu_hosts -m shell -a "free -h"
```
### Collecter les facts d'une machine (variables système détectées)
```bash
cd ansible/
../.venv/bin/ansible storage-01 -m setup
../.venv/bin/ansible storage-01 -m setup -a "filter=ansible_interfaces"
../.venv/bin/ansible storage-01 -m setup -a "filter=ansible_default_ipv4"
```
### Vérifier l'inventaire
```bash
cd ansible/
../.venv/bin/ansible-inventory --list # JSON complet
../.venv/bin/ansible-inventory --graph # Arbre des groupes/hôtes
```
### Dry-run avec diff (voir exactement ce qui changerait)
```bash
cd ansible/
../.venv/bin/ansible-playbook playbooks/storage-01.yml --check --diff
```
### Débugger une variable
```bash
cd ansible/
../.venv/bin/ansible storage-01 -m debug -a "var=lan_interface"
../.venv/bin/ansible storage-01 -m debug -a "var=hostvars['storage-01']"
```
### Augmenter la verbosité (debug complet)
```bash
cd ansible/
../.venv/bin/ansible-playbook playbooks/storage-01.yml -v # output étendu
../.venv/bin/ansible-playbook playbooks/storage-01.yml -vvv # connexions SSH incluses
```
---
## Vault Ansible
```bash
# Chiffrer un fichier
cd ansible/
../.venv/bin/ansible-vault encrypt group_vars/all/vault.yml
# Chiffrer une valeur à la volée (copier-coller dans vault.yml)
../.venv/bin/ansible-vault encrypt_string 'mon_mot_de_passe' --name 'vault_postgres_password'
# Déchiffrer temporairement pour voir
../.venv/bin/ansible-vault view group_vars/all/vault.yml
# Changer le mot de passe vault
../.venv/bin/ansible-vault rekey group_vars/all/vault.yml
```
---
## Groupes d'hôtes disponibles
| Groupe | Machines |
|---|---|
| `all` | storage-01, gpu-01 |
| `gateway` | storage-01 |
| `gpu_hosts` | gpu-01 |
| `almalinux` | storage-01, gpu-01 |

120
admin/dnsmasq.md Normal file
View file

@ -0,0 +1,120 @@
# dnsmasq — DNS local lab.local
dnsmasq tourne sur **storage-01** (`192.168.10.1:53`).
Il répond pour `*.lab.local` et forward le reste vers `8.8.8.8`.
---
## Statut et logs
```bash
# Statut du service
ssh s01 "sudo systemctl status dnsmasq"
# Logs en temps réel
ssh s01 "sudo tail -f /var/log/dnsmasq.log"
# Logs systemd
ssh s01 "sudo journalctl -u dnsmasq -f"
ssh s01 "sudo journalctl -u dnsmasq --since '10 minutes ago'"
```
---
## Tester la résolution DNS
```bash
# Depuis le poste perso — tester via storage-01 directement
dig @192.168.1.200 gpu-01.lab.local +short
dig @192.168.1.200 storage-01.lab.local +short
dig @192.168.1.200 google.com +short
# Depuis storage-01
ssh s01 "dig @192.168.10.1 gpu-01.lab.local +short"
ssh s01 "nslookup gpu-01.lab.local 192.168.10.1"
# Depuis gpu-01 (doit utiliser 192.168.10.1 automatiquement)
ssh g01 "dig gpu-01.lab.local +short"
ssh g01 "dig storage-01.lab.local +short"
ssh g01 "dig google.com +short"
# Tester un service k8s futur (ex: open-webui)
dig @192.168.10.1 open-webui.lab.local +short
```
---
## Config et rechargement
```bash
# Voir la config actuelle
ssh s01 "cat /etc/dnsmasq.conf"
# Tester la config sans redémarrer
ssh s01 "sudo dnsmasq --test"
# Recharger après modif manuelle
ssh s01 "sudo systemctl restart dnsmasq"
# Via Ansible (idempotent, recommandé)
cd /mnt/nvme0n1/nvme0n1p3/Projets/lab/ansible
../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags dnsmasq
```
---
## Ajouter une entrée DNS (ex: nouveau service k8s)
Modifier `ansible/roles/dnsmasq/defaults/main.yml` :
```yaml
dns_cluster_hosts:
- name: storage-01
ip: 192.168.10.1
# ... ajouter ici
- name: mon-service
ip: 192.168.10.200 # IP MetalLB
```
Puis : `make apply-storage`
Pour les services k8s exposés via Traefik/MetalLB, une seule entrée wildcard suffira :
```
address=/.lab.local/192.168.10.200
```
(à ajouter dans le template quand MetalLB sera en place)
---
## Problème connu — crash au boot (interface USB ethernet)
L'interface cluster `enp6s0f3u2c2` est un adaptateur USB ethernet qui prend plus
de temps à apparaître que `network.target`. dnsmasq peut démarrer avant qu'elle
soit disponible et crasher avec `unknown interface enp6s0f3u2c2`.
**Fix permanent** : override systemd déployé via Ansible :
```ini
# /etc/systemd/system/dnsmasq.service.d/wait-for-network.conf
[Unit]
After=network-online.target
Wants=network-online.target
```
**Fix immédiat** (si dnsmasq est en état `failed` après un reboot) :
```bash
sudo systemctl reset-failed dnsmasq
sudo systemctl start dnsmasq
```
**Symptôme** : les machines du LAN cluster (gpu-01...) ne peuvent plus résoudre
de noms DNS → `yum makecache` bloque, `nslookup` timeout.
---
## Vérifier le DNS configuré sur chaque machine
```bash
# storage-01
ssh s01 "cat /etc/resolv.conf && nmcli dev show enp6s0f3u2c2 | grep DNS"
# gpu-01 (doit pointer sur 192.168.10.1)
ssh g01 "cat /etc/resolv.conf && nmcli dev show enp4s0 | grep DNS"
```

376
admin/funk-cluster Executable file
View file

@ -0,0 +1,376 @@
#!/bin/bash
# Gestion du homelab Funk (storage-01 + gpu-01)
# Usage: funk-cluster [start|stop|status]
GPU01_IP="192.168.10.20"
GPU01_MAC="a8:a1:59:72:a6:11" # WOL — adresse MAC de gpu-01 (NIC principal)
GPU01_SSH="ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no gpu-01"
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BOLD='\033[1m'
NC='\033[0m'
ok() { echo -e " ${GREEN}✓${NC} $*"; }
fail() { echo -e " ${RED}✗${NC} $*"; }
warn() { echo -e " ${YELLOW}!${NC} $*"; }
info() { echo -e " → $*"; }
CHECKS_OK=0
CHECKS_FAIL=0
check_ok() { ok "$*"; CHECKS_OK=$((CHECKS_OK + 1)); }
check_fail() { fail "$*"; CHECKS_FAIL=$((CHECKS_FAIL + 1)); }
# --- Wake-on-LAN (python3, pas de dépendance externe) ---
wol() {
local mac="${1//:}"
python3 -c "
import socket
mac = '$mac'
payload = bytes.fromhex('ff' * 6 + mac * 16)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.sendto(payload, ('192.168.10.255', 9))
s.close()
"
echo -e " → Paquet WOL envoyé vers $1"
}
# --- Vérification service systemd ---
check_service() {
local svc=$1
if systemctl is-active --quiet "$svc" 2>/dev/null; then
check_ok "$svc"
return 0
else
check_fail "$svc (inactif)"
return 1
fi
}
# --- Vérification PostgreSQL (connexion réelle via postgres) ---
check_postgresql() {
if sudo -u postgres psql -c "SELECT 1" hermes &>/dev/null 2>&1; then
check_ok "PostgreSQL — base hermes accessible"
else
check_fail "PostgreSQL — base hermes inaccessible"
fi
}
# --- Vérification Qdrant ---
check_qdrant() {
local result
result=$(curl -sf --max-time 5 "http://127.0.0.1:6333/collections" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
cols = d.get('result', {}).get('collections', [])
print(len(cols))
" 2>/dev/null)
if [ -n "$result" ]; then
check_ok "Qdrant — $result collection(s)"
else
check_fail "Qdrant non joignable (port 6333)"
fi
}
# --- Vérification LiteLLM ---
check_litellm() {
local models
models=$(curl -sf --max-time 5 \
-H "Authorization: Bearer lm-studio" \
"http://127.0.0.1:4000/v1/models" \
| python3 -c "
import sys, json
d = json.load(sys.stdin).get('data', [])
print(', '.join(m['id'] for m in d))
" 2>/dev/null)
if [ -n "$models" ]; then
check_ok "LiteLLM — modèles : $models"
else
check_fail "LiteLLM non joignable (port 4000)"
fi
}
# --- Vérification Hermes HTTP ---
check_hermes() {
if curl -sf --max-time 5 "http://127.0.0.1:8080/health" &>/dev/null; then
check_ok "Hermes HTTP (port 8080)"
elif systemctl is-active --quiet hermes-agent 2>/dev/null; then
check_ok "Hermes service actif (pas d'endpoint HTTP)"
else
check_fail "Hermes inactif"
fi
}
# --- Vérification NFS export ---
check_nfs_export() {
if showmount -e localhost 2>/dev/null | grep -q "/srv/data/models"; then
check_ok "NFS export /srv/data/models OK"
else
check_fail "NFS export /srv/data/models manquant"
fi
}
# --- Vérification llama-server sur gpu-01 ---
check_llama_server() {
local model
model=$(curl -sf --max-time 5 "http://${GPU01_IP}:1234/v1/models" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data'][0]['id'])" 2>/dev/null)
if [ -n "$model" ]; then
check_ok "llama-server — modèle : $model"
return 0
else
check_fail "llama-server (non joignable)"
return 1
fi
}
# --- Vérification embeddings ---
check_embeddings() {
local dim
dim=$(curl -sf --max-time 15 -X POST "http://${GPU01_IP}:1234/v1/embeddings" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-8b","input":["test"]}' \
| python3 -c "import sys,json; print(len(json.load(sys.stdin)['data'][0]['embedding']))" 2>/dev/null)
if [ -n "$dim" ]; then
check_ok "Embeddings OK — dimension : $dim"
else
check_fail "Embeddings non disponibles"
fi
}
# --- Attendre qu'un hôte réponde au ping ---
wait_ping() {
local host=$1 timeout=${2:-120} elapsed=0
while ! ping -c1 -W1 "$host" &>/dev/null; do
sleep 3; elapsed=$((elapsed + 3))
[ $elapsed -ge $timeout ] && return 1
echo -ne " → Attente ping $host... ${elapsed}s\r"
done
echo ""
return 0
}
# --- Attendre llama-server HTTP ---
wait_llama() {
local timeout=${1:-240} elapsed=0
while ! curl -sf --max-time 3 "http://${GPU01_IP}:1234/v1/models" &>/dev/null; do
sleep 5; elapsed=$((elapsed + 5))
[ $elapsed -ge $timeout ] && return 1
echo -ne " → Attente llama-server... ${elapsed}s\r"
done
echo ""
return 0
}
# --- Afficher le score final ---
print_score() {
local total=$((CHECKS_OK + CHECKS_FAIL))
echo ""
if [ $CHECKS_FAIL -eq 0 ]; then
echo -e " ${GREEN}${BOLD}[$CHECKS_OK/$total] Tous les checks OK${NC}"
else
echo -e " ${YELLOW}${BOLD}[$CHECKS_OK/$total] — $CHECKS_FAIL check(s) en échec${NC}"
fi
}
# ============================================================
# STATUS
# ============================================================
cmd_status() {
CHECKS_OK=0; CHECKS_FAIL=0
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " FUNK — État du homelab"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "[ storage-01 — services ]"
check_service postgresql-16
check_service qdrant
check_service litellm
check_service hermes-agent
echo ""
echo "[ storage-01 — santé ]"
check_postgresql
check_qdrant
check_litellm
check_hermes
check_nfs_export
echo ""
echo "[ gpu-01 — 192.168.10.20 ]"
if ping -c1 -W2 "$GPU01_IP" &>/dev/null; then
check_ok "ping OK"
check_llama_server
check_embeddings
if $GPU01_SSH "mountpoint -q /mnt/models" 2>/dev/null; then
check_ok "NFS /mnt/models monté"
else
check_fail "NFS /mnt/models non monté"
fi
else
check_fail "gpu-01 non joignable (éteint ?)"
fi
print_score
echo ""
}
# ============================================================
# START
# ============================================================
cmd_start() {
CHECKS_OK=0; CHECKS_FAIL=0
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " FUNK — Démarrage"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 1. Services storage-01
echo ""
echo "[ storage-01 — démarrage des services ]"
for svc in postgresql-16 qdrant litellm hermes-agent; do
systemctl start "$svc" 2>/dev/null
sleep 1
if systemctl is-active --quiet "$svc"; then
ok "$svc démarré"
else
warn "$svc — problème au démarrage (voir: journalctl -u $svc -n 20)"
fi
done
# 2. Vérifications santé storage-01
echo ""
echo "[ storage-01 — vérifications ]"
sleep 2 # laisser les services s'initialiser
check_postgresql
check_qdrant
check_litellm
check_hermes
check_nfs_export
# 3. gpu-01 — Wake-on-LAN si éteint
echo ""
echo "[ gpu-01 — démarrage ]"
if ping -c1 -W2 "$GPU01_IP" &>/dev/null; then
check_ok "gpu-01 déjà allumé"
else
wol "$GPU01_MAC"
info "Attente du démarrage de gpu-01..."
if ! wait_ping "$GPU01_IP" 120; then
check_fail "gpu-01 n'a pas répondu au ping en 120s"
print_score
return 1
fi
check_ok "gpu-01 joignable"
# Attendre que SSH soit disponible
local ssh_tries=0
while ! $GPU01_SSH "true" 2>/dev/null; do
sleep 3; ssh_tries=$((ssh_tries + 1))
[ $ssh_tries -ge 10 ] && break
echo -ne " → Attente SSH gpu-01... $((ssh_tries * 3))s\r"
done
echo ""
fi
# 4. Vérifier NFS sur gpu-01
info "Vérification NFS sur gpu-01..."
local nfs_tries=0
while ! $GPU01_SSH "mountpoint -q /mnt/models" 2>/dev/null; do
sleep 3; nfs_tries=$((nfs_tries + 1))
[ $nfs_tries -ge 20 ] && break
echo -ne " → Attente montage NFS... $((nfs_tries * 3))s\r"
done
echo ""
if $GPU01_SSH "mountpoint -q /mnt/models" 2>/dev/null; then
check_ok "NFS /mnt/models monté sur gpu-01"
else
check_fail "NFS /mnt/models non monté (modèle non accessible)"
fi
# 5. Attendre llama-server
echo ""
echo "[ gpu-01 — llama-server ]"
info "Attente chargement du modèle LLM (jusqu'à 4 min)..."
if wait_llama 240; then
check_llama_server
check_embeddings
else
check_fail "llama-server n'a pas répondu en 4 minutes"
fi
# 6. Score final
print_score
if [ $CHECKS_FAIL -eq 0 ]; then
echo -e " ${GREEN}Homelab Funk opérationnel — bonne session !${NC}"
else
echo -e " ${YELLOW}Homelab démarré avec des avertissements${NC}"
fi
echo ""
}
# ============================================================
# STOP
# ============================================================
cmd_stop() {
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " FUNK — Arrêt"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 1. Arrêter Hermes et LiteLLM proprement
echo ""
echo "[ storage-01 — arrêt des services applicatifs ]"
for svc in hermes-agent litellm; do
systemctl stop "$svc" 2>/dev/null && ok "$svc arrêté" || warn "$svc déjà inactif"
done
# 2. Éteindre gpu-01
echo ""
echo "[ gpu-01 — arrêt ]"
if ping -c1 -W2 "$GPU01_IP" &>/dev/null; then
info "Arrêt de gpu-01..."
$GPU01_SSH "sudo shutdown -h now" 2>/dev/null
local tries=0
while ping -c1 -W1 "$GPU01_IP" &>/dev/null; do
sleep 2; tries=$((tries + 1))
[ $tries -ge 30 ] && break
echo -ne " → Extinction en cours... $((tries * 2))s\r"
done
echo ""
ok "gpu-01 éteint"
else
warn "gpu-01 déjà éteint"
fi
# 3. Arrêter Qdrant et PostgreSQL
echo ""
echo "[ storage-01 — arrêt des services données ]"
for svc in qdrant postgresql-16; do
systemctl stop "$svc" 2>/dev/null && ok "$svc arrêté" || warn "$svc déjà inactif"
done
echo ""
ok "Homelab Funk arrêté proprement"
echo ""
}
# ============================================================
case "${1:-status}" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
*)
echo "Usage: funk-cluster [start|stop|status]"
echo ""
echo " start Démarre gpu-01 (WOL) + vérifie tous les services"
echo " stop Arrêt propre gpu-01 + services storage-01"
echo " status État de tous les services"
exit 1
;;
esac

200
admin/hermes.md Normal file
View file

@ -0,0 +1,200 @@
# Hermes Agent — Commandes d'administration
Hermes Agent tourne en deux services systemd sur **storage-01** :
- `hermes-agent` — gateway (messagerie, API)
- `hermes-dashboard` — interface web (port 9119)
Il passe par **LiteLLM** (`:4000`) pour toute inférence — Qwen local ou Claude API.
Les données persistantes sont sur le RAID5 : `/srv/data/hermes`.
---
## Architecture
```
hermes chat / hermes -z / http://192.168.1.200:9119
▼ LM_BASE_URL=http://127.0.0.1:4000/v1
LiteLLM Proxy (:4000)
├── hermes-default → qwen3-8b → llama-server gpu-01 (défaut)
└── hermes-default → claude-sonnet-4-6 → api.anthropic.com (switch)
```
---
## Dashboard Web
Accessible depuis le poste admin uniquement (`192.168.1.10`) :
```
http://192.168.1.200:9119
```
Inclut : gestion des sessions, configuration, API keys.
> L'onglet Chat (TUI embarqué via WebSocket/PTY) ne s'affiche pas dans le dashboard malgré `--tui`.
> Utiliser `hermes-tui` en SSH à la place (voir ci-dessous).
```bash
# Service dashboard
sudo systemctl status hermes-dashboard
sudo systemctl restart hermes-dashboard
sudo journalctl -u hermes-dashboard -f
```
## TUI (chat interactif)
Le TUI nécessite un terminal interactif (PTY). Lancer depuis une session SSH :
```bash
# Depuis le poste perso
ssh storage-01
sudo hermes-tui # wrapper dans /usr/local/bin/hermes-tui
```
> Le wrapper gère automatiquement HERMES_HOME, PATH et le changement d'utilisateur vers hermes.
---
## Service systemd
```bash
sudo systemctl status hermes-agent
sudo systemctl restart hermes-agent
sudo journalctl -u hermes-agent -f
sudo journalctl -u hermes-agent -n 50
```
---
## Utilisation CLI
```bash
# Chat interactif (depuis storage-01)
cd /srv/data/hermes && hermes chat
# Question one-shot
cd /srv/data/hermes && hermes -z "ta question"
# Statut global
hermes status
# Diagnostics
hermes doctor
```
---
## Switcher le modèle
```bash
sudo /usr/local/bin/hermes-switch qwen # Qwen3-8B local (gratuit)
sudo /usr/local/bin/hermes-switch claude # Claude Sonnet 4.6 (API Anthropic ~1$/session)
sudo /usr/local/bin/hermes-switch status # Modèle actif
```
---
## Configuration
### config.yaml (`/srv/data/hermes/config.yaml`)
```yaml
model:
provider: lmstudio
base_url: "http://127.0.0.1:4000/v1"
default: "hermes-default"
context_length: 65536
gateway:
host: "0.0.0.0"
port: 8080
```
### .env (`/srv/data/hermes/.env`)
```bash
LM_BASE_URL=http://127.0.0.1:4000/v1
LM_API_KEY=lm-studio # valeur exacte obligatoire
```
---
## Variables d'environnement (service systemd)
| Variable | Valeur |
|---|---|
| `HOME` | `/opt/hermes` |
| `HERMES_HOME` | `/srv/data/hermes` |
| `LM_BASE_URL` | `http://127.0.0.1:4000/v1` |
| `LM_API_KEY` | `lm-studio` |
---
## Skill Qdrant Vector Search
```bash
# Vérifier le skill installé
cat /srv/data/hermes/skills/mlops/qdrant/SKILL.md
# Tester les embeddings (requis pour le skill)
curl -s -X POST http://192.168.10.20:1234/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-8b","input":["test"]}' | python3 -c \
"import sys,json; print('dim:', len(json.load(sys.stdin)['data'][0]['embedding']))"
# Installer qdrant-client si absent
sudo -u hermes /srv/data/hermes/hermes-agent/venv/bin/pip install "qdrant-client>=1.12.0"
```
> Toujours passer par le terminal (fichier /tmp/*.py) pour accéder à Qdrant depuis Hermes,
> jamais via execute_code (pas de qdrant-client dans cet environnement).
---
## Arborescence HERMES_HOME
```
/srv/data/hermes/
├── config.yaml → configuration principale (géré Ansible)
├── .env → LM_BASE_URL, LM_API_KEY (géré Ansible)
├── hermes-agent/ → installation (venv Python, binaire, frontend web)
│ └── web/ → source frontend dashboard (compilé via npm)
├── node/ → Node.js local (pour compiler le frontend)
├── memories/ → mémoire persistante (MEMORY.md auto-chargé)
├── sessions/ → historique des sessions
├── logs/ → logs
├── skills/ → skills installées
└── state.db → base SQLite état agent
```
---
## Recompiler le frontend dashboard
Si mise à jour de hermes-agent nécessite un rebuild :
```bash
sudo -u hermes bash -c \
'export PATH=/srv/data/hermes/node/bin:$PATH && \
cd /srv/data/hermes/hermes-agent/web && \
npm install && npm run build'
sudo systemctl restart hermes-dashboard
```
---
## Points d'attention
| Sujet | Détail |
|---|---|
| Données sur RAID5 | `HERMES_HOME=/srv/data/hermes` — jamais sur le SSD OS |
| LiteLLM requis | litellm.service doit tourner avant hermes-agent |
| `LM_API_KEY` | Doit valoir `lm-studio` exactement |
| Contexte | Qwen3-8B : 32768 tokens, system prompt ~15k → ~17k tokens dispo |
| path `hermes -z` | Toujours lancer depuis `/srv/data/hermes` (cherche .git en remontant) |
| Gemma 4 | Interdit — crash ROCm sur prompts >800 tokens |
| SELinux | Fichiers sur RAID5 ont contexte `unlabeled_t` — exécuter via binaire dans `/opt/hermes/.local/bin/` |
| Dashboard | Accessible uniquement depuis `192.168.1.10:9119` (restreint nftables) |
| TUI | `hermes --tui` nécessite un TTY — utiliser `sudo hermes-tui` en SSH interactif |
| `/opt/hermes` perms | Répertoire en `711` — traversable par tous mais non listable |

101
admin/incidents.md Normal file
View file

@ -0,0 +1,101 @@
# Incidents — Historique et résolutions
---
## 2026-05-11 — storage-01 : boucle fsck après coupure de courant
### Symptômes
- Boucle infinie de `fsck` au démarrage sur `/dev/md0`
- `You are in emergency mode. Give root password for maintenance` — mot de passe impossible à saisir (messages défilants)
- Après résolution : `hermes-agent` en boucle d'erreurs (cause : `/srv/data` non monté)
### Architecture concernée
| Composant | Détail |
|---|---|
| Disque système | NVMe `nvme0n1` — LVM (`almalinux-root` XFS 70G, swap 14.7G, home 845G) |
| RAID données | RAID5 software `md0` — 4 disques SATA 931.5G (`sda1`, `sdb2`, `sdc1`, `sdd1`) |
| Point de montage | `/srv/data` (ext4) |
### Cause racine
La coupure de courant a corrompu le système de fichiers **XFS** sur `almalinux-root`. Au redémarrage, systemd tentait de vérifier `/srv/data` avant que le RAID `md0` soit assemblé → boucle bloquante.
### Résolution étape par étape
**1. Shell de secours via GRUB**
Au démarrage, appuyer sur `Shift` pour afficher le menu GRUB. Appuyer sur `e` pour éditer la ligne `linux` :
- Remplacer `ro` par `rw`
- Ajouter `init=/bin/bash` en fin de ligne
Démarrer avec `Ctrl+X`.
**2. Activer le LVM**
```bash
lvm vgchange -ay
# 3 logical volume(s) in volume group "almalinux" now active
```
**3. Réparer le XFS (pas fsck)**
XFS nécessite `xfs_repair`, pas `fsck` :
```bash
umount /dev/mapper/almalinux-root
xfs_repair /dev/mapper/almalinux-root
# Phases 1 à 7 — succès
```
**4. Désactiver temporairement `/srv/data` dans fstab**
```bash
vi /etc/fstab
# Commenter la ligne /srv/data pour permettre le boot sans RAID
```
**5. Reboot** → système normal, RAID intact `[4/4] [UUUU]`.
### Fix permanent — fstab
Modifier la ligne `/srv/data` dans `/etc/fstab` :
```
# Avant
UUID=3add8360-fa01-47eb-0aa1-0e84bacbbc15 /srv/data ext4 defaults,noatime,nodiratime 0 2
# Après
UUID=3add8360-fa01-47eb-0aa1-0e84bacbbc15 /srv/data ext4 defaults,noatime,nodiratime,_netdev 0 0
```
| Changement | Effet |
|---|---|
| `0 2``0 0` | Désactive la vérification fsck automatique au boot |
| `_netdev` | Force systemd à attendre que tous les périphériques (RAID) soient prêts avant de monter |
### Surveillance post-incident
```bash
# État du RAID
cat /proc/mdstat
# Santé des disques (sdb a généré des erreurs I/O — à surveiller)
smartctl -a /dev/sda
smartctl -a /dev/sdb
smartctl -a /dev/sdc
smartctl -a /dev/sdd
# Monter /srv/data manuellement si besoin
mount /srv/data
# Logs de boot
journalctl -xb | grep -i "error\|fail"
```
### Points d'attention
- `sdb` a généré des erreurs I/O pendant l'incident — il est revenu opérationnel mais doit être surveillé régulièrement avec `smartctl`.
- Envisager un **onduleur (UPS)** pour prévenir ce type d'incident sur coupure de courant.
- Le fix fstab (`_netdev`, `0 0`) est permanent — ne pas revenir aux valeurs précédentes.

168
admin/index_knowledge.py Normal file
View file

@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
Index des connaissances Funk dans Qdrant (hermes_memory).
Exécuter avec : /srv/data/hermes/hermes-agent/venv/bin/python index_knowledge.py
"""
import requests
import uuid
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, Distance
EMBEDDINGS_URL = "http://192.168.10.20:1234/v1/embeddings"
EMBEDDINGS_MODEL = "qwen3-8b"
QDRANT_HOST = "localhost"
QDRANT_PORT = 6333
COLLECTION = "hermes_memory"
VECTOR_DIM = 4096
# --- Connaissances à indexer ---
KNOWLEDGE = [
{
"category": "architecture",
"tags": ["homelab", "funk", "réseau", "machines"],
"text": "Funk homelab: storage-01 (192.168.10.1 LAN cluster, 192.168.1.200 LAN domestique, AlmaLinux 9.7) est passerelle + bastion + données. gpu-01 (192.168.10.20, AlmaLinux 9.7) héberge llama-server avec RX 6700XT. Cluster Kubernetes Talos sur compute-01 (192.168.10.11), compute-02 (192.168.10.12), compute-03 (192.168.10.13). storage-01 et gpu-01 sont hors du cluster k8s.",
},
{
"category": "architecture",
"tags": ["réseau", "kubernetes", "metallb", "dns"],
"text": "Réseau Kubernetes: CNI Flannel pods 10.42.0.0/16, services 10.43.0.0/16, MetalLB pool 192.168.10.200-230. Domaine lab.local avec wildcard DNS via dnsmasq sur storage-01. Ingress: <service>.lab.local vers IP MetalLB de Traefik.",
},
{
"category": "litellm",
"tags": ["litellm", "proxy", "modèle", "switch"],
"text": "LiteLLM tourne sur storage-01 port 4000. master_key doit valoir exactement lm-studio. Hermes utilise l'alias hermes-default. Pour switcher: sudo hermes-switch qwen (Qwen2.5-7B local gratuit) ou sudo hermes-switch claude (Claude Sonnet 4.6 ~1-2$ par session). Le script modifie /etc/litellm/config.yaml et redémarre le service.",
},
{
"category": "litellm",
"tags": ["litellm", "config", "modèles", "anthropic"],
"text": "LiteLLM config /etc/litellm/config.yaml expose: hermes-default (alias switchable), qwen2.5-7b-instruct (gpu-01:1234), claude-sonnet-4-6 (API Anthropic), claude-opus-4-7 (API Anthropic). ANTHROPIC_API_KEY dans l'environnement systemd via vault Ansible.",
},
{
"category": "hermes",
"tags": ["hermes", "config", "LM_API_KEY", "provider"],
"text": "LM_API_KEY doit valoir exactement lm-studio pour activer le provider LM Studio dans Hermes. Toute autre valeur échoue silencieusement. Le fichier /srv/data/hermes/.env peut écraser les variables systemd — Ansible gère les deux via lineinfile.",
},
{
"category": "hermes",
"tags": ["hermes", "config", "config.yaml", "provider"],
"text": "config.yaml Hermes doit contenir model.provider: lmstudio ET model.base_url: http://127.0.0.1:4000/v1. Sans provider, l'inférence échoue avec AuthError même si base_url est correct. model.default: hermes-default pointe sur l'alias LiteLLM.",
},
{
"category": "hermes",
"tags": ["hermes", "cli", "path", "git"],
"text": "Toujours lancer hermes -z depuis /srv/data/hermes pour éviter l'erreur PermissionError sur .git. Hermes remonte l'arborescence à la recherche d'un .git et échoue s'il tombe sur /home/ansible/.git sans permission. HERMES_HOME=/srv/data/hermes, données sur RAID5.",
},
{
"category": "hermes",
"tags": ["hermes", "mémoire", "sessions", "qdrant"],
"text": "Hermes a deux systèmes de mémoire: mémoire native dans /srv/data/hermes/memories/ (texte, automatique, recherche par mots-clés) et Qdrant (vectoriel, sémantique, skill mlops/qdrant installé). Le skill qdrant-vector-search est actif. qdrant-client v1.18 dans le venv /srv/data/hermes/hermes-agent/venv/.",
},
{
"category": "hermes",
"tags": ["hermes", "coût", "tokens", "claude"],
"text": "Coût Hermes avec Claude Sonnet 4.6: system prompt ~15000 tokens par échange, contexte s'accumule. Une session de debug ~375k tokens input = ~1.20$. Utiliser Qwen local (gratuit) pour les tâches courantes, Claude uniquement pour le debug complexe ponctuel.",
},
{
"category": "gpu",
"tags": ["rocm", "amd", "gfx1031", "HSA", "llama-server"],
"text": "gpu-01 RX 6700XT (gfx1031) non officiellement supportée par ROCm. Toujours définir HSA_OVERRIDE_GFX_VERSION=10.3.0 dans l'environnement llama-server et tout service ROCm. Sans cette variable ROCm échoue. Défini dans le service systemd llama-server via Ansible.",
},
{
"category": "gpu",
"tags": ["llama-server", "embeddings", "pooling", "vecteurs"],
"text": "llama-server doit être démarré avec --embeddings --pooling mean pour exposer /v1/embeddings compatible OpenAI. Sans --pooling mean: erreur 'Pooling type none is not OAI compatible'. Qwen2.5-7B produit des vecteurs de dimension 3584. Endpoint: http://192.168.10.20:1234/v1/embeddings.",
},
{
"category": "gpu",
"tags": ["modèles", "qwen", "gemma", "performance"],
"text": "Modèle actif gpu-01: Qwen2.5-14B-Instruct-Q4_K_M (~35 tok/s génération, ~200 tok/s prefill, ROCm natif). Contexte limité à 16384 tokens (VRAM: 8GB modèle + 3GB KV cache = 11GB sur 12GB). Alias API: qwen2.5-14b-instruct. Ne pas utiliser Gemma 4: crash ROCm >800 tokens. Dimension embeddings: 5120.",
},
{
"category": "postgresql",
"tags": ["postgresql", "pgdg", "PGDATA", "RAID5"],
"text": "PostgreSQL 16 installé depuis repo officiel pgdg sur storage-01. PGDATA sur RAID5: /srv/data/postgres. Override systemd dans /etc/systemd/system/postgresql-16.service.d/pgdata.conf. Auth: peer en local, scram-sha-256 pour connexions réseau. Bases: hermes et litellm avec utilisateurs dédiés.",
},
{
"category": "qdrant",
"tags": ["qdrant", "api", "client", "python"],
"text": "Qdrant v1.13.4 sur storage-01 port 6333 (HTTP) et 6334 (gRPC). Données: /srv/data/qdrant. qdrant-client v1.18 installé dans /srv/data/hermes/hermes-agent/venv/. Utiliser client.query_points() et non client.search() (supprimé en v1.14+). Collection hermes_memory: dim=3584, distance=COSINE.",
},
{
"category": "firewall",
"tags": ["nftables", "firewall", "ports", "ansible"],
"text": "storage-01 utilise nftables géré centralement par le rôle gateway Ansible (nftables.conf.j2). Ne jamais ajouter de tâches firewalld dans les autres rôles. Ports ouverts cluster (192.168.10.0/24): 5432 PostgreSQL, 6333-6334 Qdrant, 8080 Hermes gateway, 9000-9001 MinIO. SSH ouvert depuis LAN domestique et cluster.",
},
{
"category": "ansible",
"tags": ["ansible", "vault", "secrets", "variables"],
"text": "Secrets Ansible dans group_vars/all/vault.yml: vault_anthropic_api_key, vault_pg_hermes_password, vault_pg_litellm_password. Rôles idempotents: common, gateway, dnsmasq, nfs_server, postgresql, qdrant, minio, litellm, hermes_agent, llama_server, rocm. Playbooks: storage-01.yml et gpu-01.yml.",
},
{
"category": "ansible",
"tags": ["ansible", "tags", "déploiement", "commandes"],
"text": "Commandes Ansible: cd ansible/ && ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags postgresql. Pour gpu-01: ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server. Dry-run: ajouter --check. Collections requises: ansible.posix, community.general, community.postgresql.",
},
]
def get_embedding(text: str) -> list[float]:
response = requests.post(
EMBEDDINGS_URL,
json={"model": EMBEDDINGS_MODEL, "input": [text]},
timeout=30,
)
response.raise_for_status()
return response.json()["data"][0]["embedding"]
def main():
client = QdrantClient(QDRANT_HOST, port=QDRANT_PORT)
# Recréer la collection proprement
existing = [c.name for c in client.get_collections().collections]
if COLLECTION in existing:
client.delete_collection(COLLECTION)
print(f"Collection '{COLLECTION}' supprimée (recréation propre)")
client.create_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=VECTOR_DIM, distance=Distance.COSINE),
)
print(f"Collection '{COLLECTION}' créée (dim={VECTOR_DIM}, cosine)")
points = []
for i, item in enumerate(KNOWLEDGE):
print(f"[{i+1}/{len(KNOWLEDGE)}] Embedding: {item['text'][:60]}...")
embedding = get_embedding(item["text"])
points.append(
PointStruct(
id=i + 1,
vector=embedding,
payload={
"text": item["text"],
"category": item["category"],
"tags": item["tags"],
"source": "funk-sessions-2026-05",
},
)
)
client.upsert(collection_name=COLLECTION, points=points)
print(f"\nOK — {len(points)} chunks indexés dans '{COLLECTION}'")
# Test rapide
print("\n--- Test recherche: 'erreur hermes modele provider' ---")
query_vec = get_embedding("erreur hermes modele provider")
results = client.query_points(
collection_name=COLLECTION,
query=query_vec,
limit=3,
)
for r in results.points:
print(f" Score={r.score:.4f} [{r.payload['category']}] {r.payload['text'][:80]}...")
if __name__ == "__main__":
main()

128
admin/litellm.md Normal file
View file

@ -0,0 +1,128 @@
# LiteLLM — Proxy LLM unifié (storage-01)
LiteLLM tourne en service systemd sur **storage-01** (`127.0.0.1:4000`).
Il route les requêtes OpenAI-compatibles vers le bon backend selon le modèle demandé.
---
## Flux
```
Hermes / Open WebUI / n8n
▼ http://127.0.0.1:4000/v1 (Authorization: Bearer lm-studio)
LiteLLM Proxy
├── hermes-default → qwen2.5-14b-instruct (défaut Hermes)
├── qwen2.5-14b-instruct → llama-server gpu-01 :1234 (local, gratuit)
├── claude-sonnet-4-6 → api.anthropic.com (cloud, payant)
└── claude-opus-4-7 → api.anthropic.com (cloud, payant)
```
---
## Service systemd
```bash
# Depuis storage-01
sudo systemctl status litellm
sudo systemctl restart litellm
sudo journalctl -u litellm -f
sudo journalctl -u litellm -n 50
```
---
## Modèles disponibles
| Nom dans l'API | Backend | Coût |
|---|---|---|
| `hermes-default` | Alias switchable (Qwen par défaut) | Selon backend actif |
| `qwen2.5-14b-instruct` | llama-server gpu-01 (ROCm 7.x) | Gratuit |
| `claude-sonnet-4-6` | Anthropic API | ~$3/$15 par million tokens in/out |
| `claude-opus-4-7` | Anthropic API | Plus cher, meilleur raisonnement |
---
## Switch rapide avec hermes-switch
Script déployé sur storage-01 :
```bash
sudo hermes-switch status # voir le modèle actuel de hermes-default
sudo hermes-switch qwen # Qwen2.5-14B local (gratuit, ~35 tok/s)
sudo hermes-switch claude # Claude Sonnet 4.6 (payant, ~1-2$ par session debug)
```
Le script modifie `/etc/litellm/config.yaml` et redémarre litellm automatiquement.
Source : `roles/litellm/files/hermes-switch`.
### Coût Claude
| Session type | Tokens input | Coût estimé |
|---|---|---|
| Debug simple (5 échanges) | ~80k | ~$0.24 |
| Debug complexe (10 échanges) | ~375k | ~$1.12 |
| Utilisation courante | — | Utiliser Qwen |
---
## Validation API
```bash
# Depuis storage-01 — master_key = lm-studio
curl http://127.0.0.1:4000/v1/models \
-H "Authorization: Bearer lm-studio" | python3 -m json.tool
# Test inférence Qwen (local)
curl http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer lm-studio" \
-H "Content-Type: application/json" \
-d '{"model": "qwen2.5-14b-instruct", "messages": [{"role": "user", "content": "Dis bonjour"}], "max_tokens": 30}'
# Test inférence Claude
curl http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer lm-studio" \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-4-6", "messages": [{"role": "user", "content": "Dis bonjour"}], "max_tokens": 30}'
```
---
## Configuration
Fichier : `/etc/litellm/config.yaml` (géré par Ansible, propriété `litellm:litellm`, mode `0640`)
La clé API Anthropic est injectée via `ANTHROPIC_API_KEY` dans l'unit systemd —
elle vient du vault Ansible (`vault_anthropic_api_key`). Jamais en clair dans config.yaml.
**master_key** : `lm-studio` — doit correspondre à `LM_API_KEY` dans le `.env` Hermes.
---
## Ajouter un modèle
1. Modifier `roles/litellm/templates/config.yaml.j2` :
```yaml
- model_name: claude-haiku-4-5
litellm_params:
model: anthropic/claude-haiku-4-5-20251001
api_key: os.environ/ANTHROPIC_API_KEY
```
2. Redéployer :
```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm
```
---
## Points d'attention
| Sujet | Détail |
|---|---|
| Accès réseau | `127.0.0.1` uniquement — pour exposer au cluster, changer `litellm_host: 0.0.0.0` + ouvrir firewall |
| master_key | `lm-studio` — doit correspondre exactement à `LM_API_KEY` dans le `.env` Hermes |
| Clé Anthropic | `ANTHROPIC_API_KEY` dans l'env systemd — vault Ansible |
| Coût Claude | Surveiller la consommation sur console.anthropic.com |
| 404 /api/v1/models | Hermes appelle `/api/v1/models` à l'init (retourne 404) — normal, ne bloque pas |
| hermes-switch status | Utilise `grep -A3` — si le bloc hermes-default a une structure différente, ajuster |

139
admin/llama_server.md Normal file
View file

@ -0,0 +1,139 @@
# llama-server — Inférence LLM (gpu-01)
llama-server (llama.cpp compilé with ROCm 7.x) tourne en service systemd sur **gpu-01** (`192.168.10.20:1234`).
API 100% compatible OpenAI — remplace LM Studio pour l'inférence.
---
## Service systemd
```bash
# Depuis gpu-01
sudo systemctl status llama-server
sudo systemctl restart llama-server
sudo journalctl -u llama-server -f
sudo journalctl -u llama-server -n 50
```
---
## Modèle actuel
| Paramètre | Valeur |
|---|---|
| Modèle | Qwen2.5-14B-Instruct Q4_K_M |
| Alias API | `qwen2.5-14b-instruct` |
| Chemin | `/mnt/models/bartowski/Qwen2.5-14B-Instruct-GGUF/Qwen2.5-14B-Instruct-Q4_K_M.gguf` |
| Contexte | 16384 tokens (limité par VRAM — voir ci-dessous) |
| GPU offload | 99 layers (tout sur GPU) |
| Parallélisme | 1 slot (requis pour Hermes — voir bug parallel) |
| Embeddings | activés (`--embeddings --pooling mean`) |
### Contrainte VRAM (12 GB RX 6700XT)
| Élément | VRAM |
|---|---|
| Modèle Q4_K_M 14B | ~8 GB |
| KV cache à 16384 ctx | ~3 GB |
| **Total** | **~11 GB** ✅ |
À 32768 tokens de contexte, le KV cache monte à ~6 GB → total ~14 GB → segfault.
**Ne pas augmenter `llama_ctx_size` au-delà de 16384 pour le 14B.**
---
## Embeddings
llama-server expose `/v1/embeddings` avec `--embeddings --pooling mean`.
Le flag `--pooling mean` est obligatoire pour les LLM causaux (sinon : erreur 400
`Pooling type 'none' is not OAI compatible`).
```bash
# Test embeddings depuis storage-01
curl -s -X POST http://192.168.10.20:1234/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"model":"qwen2.5-14b-instruct","input":["test"]}' | jq '.data[0].embedding | length'
# → 5120 (dimension Qwen2.5-14B)
```
---
## Performances (RX 6700XT, ROCm 7.x)
| Métrique | Qwen2.5-7B | Qwen2.5-14B |
|---|---|---|
| Prefill | ~400 tok/s | ~200 tok/s |
| Génération | ~70 tok/s | ~35 tok/s |
| TTFT Hermes (15k tokens) | ~35s | ~75s |
| VRAM modèle | ~4 GB | ~8 GB |
---
## Validation API
```bash
# Depuis storage-01 ou n'importe quelle machine du LAN cluster
curl -s http://192.168.10.20:1234/v1/models | jq '.data[].id'
# Test inférence rapide
curl http://192.168.10.20:1234/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5-14b-instruct",
"messages": [{"role": "user", "content": "Dis bonjour"}],
"max_tokens": 50
}'
```
---
## Changer de modèle
1. Modifier `host_vars/gpu-01/vars.yml` :
```yaml
llama_model_path: "/mnt/models/bartowski/NouveauModele.gguf"
llama_model_alias: "nouveau-alias"
llama_ctx_size: 16384 # ajuster selon VRAM disponible
```
2. Redéployer :
```bash
ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server
```
3. Mettre à jour LiteLLM (alias `hermes-default`) :
```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm
```
---
## Build (source)
```bash
# Sur gpu-01 — rebuild si besoin
cd /opt/llama.cpp
git pull
cmake -B build \
-DGGML_HIP=ON \
-DAMDGPU_TARGETS=gfx1030 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_HIP_COMPILER=/opt/rocm/llvm/bin/clang++ \
-DCMAKE_PREFIX_PATH=/opt/rocm
cmake --build build --target llama-server -j$(nproc)
sudo systemctl restart llama-server
```
---
## Points d'attention
| Sujet | Détail |
|---|---|
| GPU non officiel | RX 6700XT (gfx1031) — `HSA_OVERRIDE_GFX_VERSION=10.3.0` dans le service |
| ROCm version | 7.2.3 — libs dans `/opt/rocm` |
| VRAM limite | 14B + 16k ctx = ~11 GB — ne pas dépasser 16384 tokens |
| parallel=1 obligatoire | `--parallel 4` divise le ctx en slots → contexte insuffisant pour Hermes |
| pooling mean obligatoire | sans `--pooling mean` l'endpoint `/v1/embeddings` retourne erreur 400 |
| Gemma 4 interdit | Crash ROCm sur prompts >800 tokens |
| Modèles sur NFS | `/mnt/models` monté depuis storage-01 (NFS) |

49
admin/lm_studio.md Normal file
View file

@ -0,0 +1,49 @@
# LM Studio — Commandes d'administration
> **Note** : L'inférence GPU est désormais assurée par **llama-server** (voir `admin/llama_server.md`).
> LM Studio reste installé sur gpu-01 **uniquement pour la gestion des modèles** (`lms get`, `lms ls`).
> Le service `lm-studio` est désactivé.
---
## Gestion des modèles (lms CLI)
```bash
# Lister les modèles installés
lms ls
# Télécharger un modèle depuis HuggingFace
lms get https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF -y
# Supprimer un modèle
lms remove bartowski/Qwen2.5-7B-Instruct-GGUF
# Les modèles sont stockés sur le NFS partagé :
# storage-01:/srv/data/models → gpu-01:/mnt/models
# Symlink : /opt/lmstudio/.lmstudio/models → /mnt/models
```
---
## Pourquoi llama-server remplace LM Studio pour l'inférence
| Critère | LM Studio (Vulkan) | llama-server (ROCm) |
|---|---|---|
| Prefill | ~100 tok/s | ~400 tok/s |
| Génération | ~15 tok/s | ~70 tok/s |
| Backend | Vulkan (ROCm 6.x uniquement) | ROCm 7.x natif |
| API | OpenAI-compatible | OpenAI-compatible |
Le backend ROCm de LM Studio est compilé pour ROCm 6.x et incompatible avec ROCm 7.x installé sur gpu-01.
llama.cpp compilé from source avec ROCm 7.x donne un gain de ~5x sur la vitesse d'inférence.
---
## Points d'attention
| Sujet | Détail |
|---|---|
| GPU non officiel | RX 6700XT (gfx1031) — `HSA_OVERRIDE_GFX_VERSION=10.3.0` dans le service llama-server |
| Gemma 4 interdit | Gemma 4 E2B/E4B crash Vulkan et ROCm sur prompts >800 tokens — utiliser Qwen |
| Modèles sur NFS | `/opt/lmstudio/.lmstudio/models` est un symlink vers `/mnt/models` (NFS automount) |
| lms get | Utiliser `lms get <url-hf>` pour télécharger, puis pointer llama-server sur le GGUF |

117
admin/nfs.md Normal file
View file

@ -0,0 +1,117 @@
# NFS — Commandes d'administration
Le RAID5 `md127` (4×1 TB = 2.7 TB utiles) est monté sur **storage-01** à `/srv/data`
et exporté en NFS vers le LAN cluster (`192.168.10.0/24`).
---
## Côté serveur (storage-01)
```bash
# État du RAID
sudo mdadm --detail /dev/md127
cat /proc/mdstat
# Exports actifs
sudo exportfs -v
# Recharger les exports après modification de /etc/exports
sudo exportfs -ra
# Statut NFS
sudo systemctl status nfs-server
```
### Arborescence du RAID
```
/srv/data/
├── nfs/
│ └── k8s/ → export pour le NFS provisioner Kubernetes
├── models/ → modèles LM Studio partagés (gpu-01 monte ce répertoire)
├── hermes/ → données persistantes Hermes Agent (à venir)
├── postgres/ → données PostgreSQL (à venir)
├── qdrant/ → données Qdrant (à venir)
└── minio/ → données MinIO (à venir)
```
### Exports (`/etc/exports`)
```
/srv/data/nfs/k8s 192.168.10.0/24(rw,sync,no_subtree_check,no_root_squash)
/srv/data/models 192.168.10.0/24(rw,sync,no_subtree_check,no_root_squash)
```
---
## Côté client (gpu-01)
```bash
# État des automounts
sudo systemctl status mnt-nfs.automount
sudo systemctl status mnt-models.automount
# Monter manuellement (debug)
sudo mount -t nfs -o nfsvers=4 192.168.10.1:/srv/data/nfs/k8s /mnt/nfs
# Vérifier les montages actifs
mount | grep nfs
df -h /mnt/nfs /mnt/models
```
### Comportement automount (extinctions fréquentes)
Les montages NFS sur gpu-01 utilisent `x-systemd.automount` :
- **Pas montés au boot** — montés uniquement au premier accès
- Si storage-01 est éteint au moment du boot → gpu-01 démarre normalement
- Au premier accès → montage déclenché (timeout 30 s)
- `soft` + `timeo=30` + `retrans=3` → jamais bloquant si le serveur est injoignable
---
## Modèles LM Studio sur NFS
```bash
# Sur gpu-01 — symlink transparent pour LM Studio
ls -la /opt/lmstudio/.lmstudio/models
# → lrwxrwxrwx ... models -> /mnt/models
# Le contenu réel est sur storage-01
ls /mnt/models/
# → lmstudio-community/gemma-4-E2B-it-GGUF/
# → lmstudio-community/gemma-4-E4B-it-GGUF/
# Ajouter un modèle (depuis gpu-01 — sera stocké directement sur NFS)
lms get <modele>
```
---
## Ajouter un nouveau répertoire partagé
1. Créer le répertoire sur storage-01 :
```bash
sudo mkdir -p /srv/data/<nom>
sudo chown <uid>:<gid> /srv/data/<nom>
```
2. Ajouter dans `host_vars/storage-01/vars.yml` :
```yaml
nfs_exports:
- path: /srv/data/<nom>
network: "{{ cluster_network }}"
options: "rw,sync,no_subtree_check,no_root_squash"
```
3. Ajouter le montage client dans `host_vars/<host>/vars.yml` :
```yaml
nfs_client_mounts:
- local_path: /mnt/<nom>
remote_export: /srv/data/<nom>
```
4. Appliquer :
```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml
ansible-playbook -i inventory.yml playbooks/<host>.yml
```

131
admin/reseau.md Normal file
View file

@ -0,0 +1,131 @@
# Réseau — nftables, routage, NAT
Tout ce qui suit s'exécute sur **storage-01** sauf mention contraire.
---
## Interfaces réseau storage-01
| Interface | IP | Rôle |
|---|---|---|
| `enp4s0` | `192.168.1.200` | WAN — vers Freebox (LAN domestique) |
| `enp6s0f3u2c2` | `192.168.10.1` | LAN cluster — vers machines |
> Noms contre-intuitifs : `enp6s0f3u2c2` est une carte USB-Ethernet (cluster),
> `enp4s0` est la carte PCIe (WAN).
---
## Route statique poste perso
Pour atteindre le LAN cluster depuis le poste (`192.168.1.10`) :
```bash
# Route permanente (NetworkManager)
nmcli connection modify enp12s0 +ipv4.routes "192.168.10.0/24 192.168.1.200"
nmcli connection up enp12s0
# Vérifier
ip route | grep 192.168.10
ping 192.168.10.20 # gpu-01
```
La route statique Freebox (`192.168.10.0/24 → 192.168.1.200`) est aussi configurée
dans l'interface Freebox pour que tous les équipements du LAN domestique puissent
atteindre le cluster.
---
## nftables
Géré par Ansible — rôle `gateway`, template `nftables.conf.j2`.
**Ne jamais modifier manuellement** — les changements seront écrasés au prochain `ansible-playbook`.
```bash
# Voir toutes les règles actives
sudo nft list ruleset
# Voir les paquets bloqués (logs kernel)
sudo journalctl -k | grep nft-drop
# Recharger après modification manuelle d'urgence
sudo systemctl reload nftables
# Appliquer via Ansible (méthode correcte)
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags gateway
```
### Ports ouverts
| Port | Proto | Source | Service |
|---|---|---|---|
| 22 | TCP | 192.168.1.0/24, 192.168.10.0/24 | SSH |
| 53 | TCP/UDP | 192.168.10.0/24 | DNS (dnsmasq) |
| 111, 2049 | TCP/UDP | 192.168.10.0/24 | NFS |
| 5432 | TCP | 192.168.10.0/24 | PostgreSQL |
| 6333, 6334 | TCP | 192.168.10.0/24 | Qdrant |
| 8080 | TCP | 192.168.1.0/24, 192.168.10.0/24 | Hermes gateway |
| 9000, 9001 | TCP | 192.168.10.0/24 | MinIO |
| 9119 | TCP | 192.168.1.10 uniquement | Hermes dashboard |
### Forwarding
```
LAN domestique (192.168.1.0/24) → LAN cluster (192.168.10.0/24) : new+established (admin)
LAN cluster → WAN : new+established (NAT sortant pour les nœuds)
WAN → LAN cluster : established+related uniquement (réponses NAT)
```
---
## IP Forwarding
```bash
# Vérifier (doit être = 1)
sysctl net.ipv4.ip_forward
```
---
## DNS — /etc/hosts storage-01
```bash
cat /etc/hosts
# 192.168.10.20 gpu-01 ← ajouté manuellement (dnsmasq non utilisé par storage-01 lui-même)
```
> storage-01 utilise 1.1.1.1 / 9.9.9.9 comme resolvers, pas son propre dnsmasq.
> Les entrées critiques doivent être dans `/etc/hosts`.
---
## Debug NAT / forwarding
```bash
# Depuis gpu-01 — vérifier que le trafic sort par storage-01
ssh gpu-01 "curl -s https://ifconfig.me" # doit retourner l'IP publique Freebox
ssh gpu-01 "traceroute 8.8.8.8"
# Paquets passant par le NAT
sudo nft list table ip nat
# Tester le routage depuis le poste perso
traceroute 192.168.10.20
ping 192.168.10.11 # compute-01
```
---
## Commandes utiles
```bash
# Table de routage storage-01
ip route show
# Stats trafic interfaces
ip -s link show enp4s0
ip -s link show enp6s0f3u2c2
# Connexions actives
ss -tnp
```

116
admin/rocm.md Normal file
View file

@ -0,0 +1,116 @@
# ROCm — GPU AMD RX 6700XT (gpu-01)
ROCm tourne directement sur l'hôte AlmaLinux (pas dans des conteneurs).
La RX 6700XT (gfx1031) n'est pas officiellement supportée → `HSA_OVERRIDE_GFX_VERSION=10.3.0` obligatoire.
---
## Vérification de base
```bash
# Le GPU est-il détecté par le kernel ?
ssh g01 "lspci | grep -i amd | grep -i vga"
# Le module amdgpu est-il chargé ?
ssh g01 "lsmod | grep amdgpu"
# ROCm voit-il le GPU ? (doit lister la RX 6700XT)
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo | grep -A5 'Agent 2'"
# Version ROCm installée
ssh g01 "rocminfo | grep 'ROCk'"
# GPU info complet
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo"
```
---
## Variable HSA_OVERRIDE
```bash
# Vérifier qu'elle est bien définie system-wide
ssh g01 "cat /etc/environment | grep HSA"
# Vérifier dans profile.d (sessions interactives)
ssh g01 "cat /etc/profile.d/rocm.sh"
# Tester manuellement (si la variable n'est pas dans l'env courant)
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo | grep gfx"
```
---
## Modules kernel
```bash
# Statut du module amdgpu
ssh g01 "lsmod | grep amdgpu"
# Recharger le module (si nécessaire)
ssh g01 "sudo modprobe -r amdgpu && sudo modprobe amdgpu"
# Logs kernel liés au GPU
ssh g01 "sudo dmesg | grep -i amdgpu | tail -20"
ssh g01 "sudo dmesg | grep -i 'drm\|gpu' | tail -20"
```
---
## Repos AMD
```bash
# Vérifier les repos installés
ssh g01 "sudo dnf repolist | grep -i 'amd\|rocm'"
# Voir les packages ROCm installés
ssh g01 "sudo dnf list installed | grep -i rocm"
ssh g01 "sudo rpm -qa | grep rocm"
```
---
## Groupes utilisateur (accès GPU)
```bash
# L'user doit être dans video et render
ssh g01 "groups ansible"
# Si absent — ajouter manuellement
ssh g01 "sudo usermod -aG video,render ansible"
```
---
## Debug ROCm / LM Studio
```bash
# Tester OpenCL
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 clinfo | head -30"
# Voir l'utilisation GPU en temps réel
ssh g01 "watch -n 1 'cat /sys/class/drm/card*/device/gpu_busy_percent'"
# Température GPU
ssh g01 "cat /sys/class/drm/card*/device/hwmon/hwmon*/temp1_input"
# (valeur en millidegrés → diviser par 1000)
# VRAM utilisée
ssh g01 "cat /sys/class/drm/card*/device/mem_info_vram_used"
ssh g01 "cat /sys/class/drm/card*/device/mem_info_vram_total"
```
---
## Fallback Vulkan (si ROCm ne fonctionne pas)
Si `rocminfo` ne liste pas le GPU malgré `HSA_OVERRIDE`, LM Studio peut utiliser Vulkan :
```bash
# Vérifier que Vulkan fonctionne
ssh g01 "vulkaninfo 2>/dev/null | grep 'GPU id'"
# Installer les outils Vulkan si absents
ssh g01 "sudo dnf install -y vulkan-tools"
```
Dans LM Studio : Settings → GPU → Backend → Vulkan

70
admin/ssh.md Normal file
View file

@ -0,0 +1,70 @@
# SSH — Connexions et transferts
Les alias sont définis dans `~/.ssh/config`.
---
## Connexions rapides
```bash
ssh s01 # storage-01 (192.168.1.200) — user ansible
ssh g01 # gpu-01 (192.168.10.20) — via ProxyJump s01 automatique
ssh c01 # compute-01 — via ProxyJump s01 (quand installé)
ssh c02 # compute-02 — idem
ssh c03 # compute-03 — idem
```
## Connexion en root (uniquement si nécessaire, avant durcissement)
```bash
ssh -o PasswordAuthentication=yes root@192.168.1.200
```
## Connexion manuelle avec jump host (sans alias)
```bash
ssh -J ansible@192.168.1.200 ansible@192.168.10.20
```
---
## Transferts de fichiers
```bash
# Copier un fichier vers storage-01
scp fichier.txt s01:/tmp/
# Copier depuis gpu-01
scp g01:/var/log/dnsmasq.log /tmp/
# Copier via jump host (sans alias SSH)
scp -o ProxyJump=s01 fichier.txt ansible@192.168.10.20:/tmp/
```
---
## Debug SSH
```bash
# Connexion avec verbosité max (voir pourquoi ça échoue)
ssh -vvv s01
# Tester sans utiliser ~/.ssh/config
ssh -F /dev/null -i ~/.ssh/id_ed25519 ansible@192.168.1.200
# Vérifier quelle clé est utilisée
ssh -v s01 2>&1 | grep "Offering"
```
---
## Clés SSH
```bash
# Voir la clé publique utilisée
cat ~/.ssh/id_ed25519.pub
# Vérifier les clés autorisées sur storage-01
ssh s01 "cat ~/.ssh/authorized_keys"
# Vérifier les clés autorisées sur gpu-01
ssh g01 "cat ~/.ssh/authorized_keys"
```

136
admin/systeme.md Normal file
View file

@ -0,0 +1,136 @@
# Système — AlmaLinux 9
Commandes générales applicables sur storage-01 et gpu-01.
---
## Services systemd
```bash
# Statut d'un service
sudo systemctl status <service>
# Démarrer / arrêter / redémarrer
sudo systemctl start <service>
sudo systemctl stop <service>
sudo systemctl restart <service>
sudo systemctl reload <service> # rechargement config sans coupure
# Activer / désactiver au démarrage
sudo systemctl enable <service>
sudo systemctl disable <service>
# Lister tous les services actifs
sudo systemctl list-units --type=service --state=running
```
### Services clés par machine
| Machine | Service | Rôle |
|---|---|---|
| storage-01 | `nftables` | Firewall + NAT masquerade cluster → WAN |
| storage-01 | `dnsmasq` | DNS local lab.local |
| storage-01 | `chronyd` | NTP |
| storage-01 | `sshd` | SSH |
| storage-01 | `hermes-agent` | Agent IA (gateway + cron) |
| gpu-01 | `chronyd` | NTP |
| gpu-01 | `sshd` | SSH |
| gpu-01 | `llama-server` | Inférence LLM — Qwen2.5-7B via ROCm 7.x |
---
## Logs
```bash
# Logs d'un service en temps réel
sudo journalctl -u <service> -f
# Logs depuis X minutes
sudo journalctl -u <service> --since '30 minutes ago'
# Logs kernel (erreurs matériel, OOM killer...)
sudo journalctl -k
# Logs du démarrage
sudo journalctl -b
```
---
## Ressources système
```bash
# CPU / RAM / swap
free -h
top
htop # plus lisible (installé via EPEL)
# Disques
df -h
lsblk
# Processus gourmands
ps aux --sort=-%mem | head -10
ps aux --sort=-%cpu | head -10
```
---
## Réseau (NetworkManager)
```bash
# Lister les connexions
nmcli con show
# Détails d'une interface (IP, DNS, gateway)
nmcli dev show enp4s0
# Modifier le DNS d'une interface
sudo nmcli con mod enp4s0 ipv4.dns '192.168.10.1'
sudo nmcli con up enp4s0
# Voir le resolv.conf généré
cat /etc/resolv.conf
```
---
## Mises à jour
```bash
# Voir les mises à jour disponibles
sudo dnf check-update
# Appliquer les mises à jour via Ansible (recommandé — reboot automatique si nouveau kernel)
cd /mnt/nvme0n1/nvme0n1p3/Projets/lab/ansible
../.venv/bin/ansible-playbook -i inventory.yml playbooks/site.yml --tags update
# Manuellement sur une machine
sudo dnf update -y
```
> **Attention post-reboot RAID5** : vérifier que dnsmasq est démarré
> (`systemctl status dnsmasq`). Si `failed`, voir `admin/dnsmasq.md`.
> Les `.pyc` Python peuvent être corrompus sur reboot non-propre —
> symptôme : `bad marshal data` dans les logs d'une app Python.
---
## Infos système rapides
```bash
# Version OS
cat /etc/os-release
# Hostname
hostname
# Uptime
uptime
# Infos CPU
lscpu | grep -E "Model name|Socket|Core|Thread"
# RAM totale
free -h | head -2
```

View file

@ -11,7 +11,7 @@ Environment=HOME={{ hermes_home }}
Environment=HERMES_HOME={{ hermes_data_dir }}
Environment=PATH={{ hermes_data_dir }}/node/bin:{{ hermes_data_dir }}/hermes-agent/venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart={{ hermes_home }}/.local/bin/hermes dashboard --no-open --tui --insecure --host {{ ansible_host }} --port {{ hermes_dashboard_port }}
ExecStart={{ hermes_home }}/.local/bin/hermes dashboard --no-open --insecure --tui --host {{ ansible_host }} --port {{ hermes_dashboard_port }}
Restart=on-failure
RestartSec=10s