mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 04:34:41 +02:00
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:
parent
1544a2fc6d
commit
dca71144d6
17 changed files with 1979 additions and 4 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
20
admin/README.md
Normal 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
107
admin/ansible.md
Normal 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
120
admin/dnsmasq.md
Normal 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
376
admin/funk-cluster
Executable 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
200
admin/hermes.md
Normal 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
101
admin/incidents.md
Normal 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
168
admin/index_knowledge.py
Normal 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
128
admin/litellm.md
Normal 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
139
admin/llama_server.md
Normal 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
49
admin/lm_studio.md
Normal 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
117
admin/nfs.md
Normal 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
131
admin/reseau.md
Normal 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
116
admin/rocm.md
Normal 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
70
admin/ssh.md
Normal 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
136
admin/systeme.md
Normal 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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue