docs: réorganisation install/ — guides séparés par machine

- admin/install/README.md        — index + ordre d'installation recommandé
- admin/install/storage-01.md   — AlmaLinux + RAID5 + Ansible + stack IA (Hermes/LiteLLM/Qdrant/PostgreSQL)
- admin/install/gpu-01.md       — AlmaLinux + ROCm + llama-server (RX 6700XT, HSA_OVERRIDE)
- admin/install/kubernetes.md   — ex admin/installe.md (Talos + MetalLB + Traefik)
- admin/README.md               — séparation install/ vs référence opérationnelle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-05-12 23:03:05 +02:00
parent 0bdfe49d75
commit 0ce6be04bb
5 changed files with 1125 additions and 1 deletions

View file

@ -1,6 +1,14 @@
# Administration Funk — Index
Commandes utiles pour administrer, débugger et comprendre l'infrastructure.
## Guides d'installation (première mise en place)
| Guide | Cible |
|---|---|
| [install/storage-01.md](install/storage-01.md) | AlmaLinux + RAID5 + Ansible + stack IA complète |
| [install/gpu-01.md](install/gpu-01.md) | AlmaLinux + ROCm + llama-server (RX 6700XT) |
| [install/kubernetes.md](install/kubernetes.md) | Talos + MetalLB + Traefik + DNS wildcard |
## Référence opérationnelle (administration courante)
| Fichier | Contenu |
|---|---|
@ -15,6 +23,7 @@ Commandes utiles pour administrer, débugger et comprendre l'infrastructure.
| [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 |
| [talos.md](talos.md) | Talos Linux — administration quotidienne du cluster k8s |
| [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]"` |

22
admin/install/README.md Normal file
View file

@ -0,0 +1,22 @@
# Guides d'installation Funk
Guides d'installation complets, écrits après avoir réalisé chaque étape en réel.
Chaque fichier couvre une machine ou une couche du projet de A à Z.
| Guide | Machine | OS | Rôle |
|---|---|---|---|
| [storage-01.md](storage-01.md) | storage-01 | AlmaLinux 9.7 | Passerelle + RAID5 + services IA (Hermes, LiteLLM, Qdrant, PostgreSQL) |
| [gpu-01.md](gpu-01.md) | gpu-01 | AlmaLinux 9.7 | Inférence LLM (ROCm + llama-server, RX 6700XT) |
| [kubernetes.md](kubernetes.md) | compute-01/02/03 | Talos v1.13 | Cluster Kubernetes (MetalLB, Traefik, DNS wildcard) |
## Ordre d'installation recommandé
```
1. storage-01 ← indépendant, point d'entrée de tout le lab
2. gpu-01 ← nécessite storage-01 (DNS, NAT, NFS)
3. kubernetes ← nécessite storage-01 (DHCP, DNS, NFS, NAT)
```
storage-01 est la pièce centrale : sans lui, rien d'autre ne fonctionne
(pas de DHCP pour les nœuds Talos, pas de NAT pour sortir sur internet,
pas de DNS `lab.local`, pas de NFS pour les modèles LLM).

365
admin/install/gpu-01.md Normal file
View file

@ -0,0 +1,365 @@
# Installation gpu-01
Guide d'installation complet de gpu-01 — de l'ISO AlmaLinux 9.7 à llama-server
en production avec ROCm sur RX 6700XT. Écrit après installation réelle.
---
## Sommaire
1. [Rôle et architecture](#1-rôle-et-architecture)
2. [Installation AlmaLinux 9.7 (base OS)](#2-installation-almalinux-97-base-os)
3. [Configuration réseau initiale (manuelle)](#3-configuration-réseau-initiale-manuelle)
4. [Prérequis : storage-01 opérationnel](#4-prérequis--storage-01-opérationnel)
5. [Jouer le playbook Ansible](#5-jouer-le-playbook-ansible)
6. [Vérifications post-install](#6-vérifications-post-install)
7. [Pièges et incidents rencontrés](#7-pièges-et-incidents-rencontrés)
---
## 1. Rôle et architecture
gpu-01 est le serveur d'inférence LLM du lab. Il n'est pas dans le cluster Kubernetes.
Il est consommé par storage-01 (LiteLLM le pointe directement via HTTP).
### Ce qu'il fait
| Fonction | Détail |
|---|---|
| Inférence LLM | llama-server (llama.cpp) via ROCm 7.x sur RX 6700XT 12 GB VRAM |
| Embeddings | `/v1/embeddings` OpenAI-compatible (`--embeddings --pooling mean`) |
| NFS client | Monte `/srv/data/models` depuis storage-01 → symlink pour llama-server |
### Matériel
| Composant | Détail |
|---|---|
| CPU | AMD Ryzen 5 3600 (6c/12t) |
| RAM | 32 GB DDR4 |
| GPU | AMD RX 6700XT — 12 GB VRAM, architecture RDNA2 (gfx1031) |
| OS | AlmaLinux 9.7 |
| Disque | SSD SATA 500 GB (OS + binaires) |
| Stockage modèles | NFS via storage-01 `/srv/data/models` → monté sur `/mnt/models` |
### Contrainte critique — RX 6700XT (gfx1031)
La RX 6700XT n'est **pas officiellement supportée** par ROCm. Son GFX ID est `gfx1031`.
ROCm ne reconnaît que jusqu'à `gfx1030` dans la plupart des versions.
**Workaround permanent** dans tout service qui utilise le GPU :
```
HSA_OVERRIDE_GFX_VERSION=10.3.0
```
Sans cette variable, ROCm ne voit pas le GPU et l'inférence tombe sur le CPU.
---
## 2. Installation AlmaLinux 9.7 (base OS)
Même ISO que storage-01 : AlmaLinux 9.7 minimal.
```bash
sudo dd if=AlmaLinux-9.7-x86_64-minimal.iso of=/dev/sdX bs=4M status=progress conv=fsync
```
### Paramètres d'installation (Anaconda)
**Réseau** : gpu-01 est dans le LAN cluster — il n'a pas d'accès direct au LAN domestique.
Pour accéder à internet pendant l'install (télécharger les packages ROCm), il faut que
storage-01 soit déjà en ligne et fasse le NAT.
Configurer l'interface réseau :
- IP statique : `192.168.10.20/24`
- Gateway : `192.168.10.1` (storage-01)
- DNS : `192.168.10.1`
- Hostname : `gpu-01`
**Disque** : sélectionner le SSD SATA (pas le NVMe si présent — celui-ci peut être
réservé à autre chose). Partitionnement automatique LVM convient.
**Utilisateur** : root avec mot de passe temporaire.
---
## 3. Configuration réseau initiale (manuelle)
Avant Ansible, vérifier que gpu-01 est joignable via storage-01 :
```bash
# Depuis le poste perso via jump
ssh -J ansible@192.168.1.200 root@192.168.10.20
```
Depuis storage-01 :
```bash
ping 192.168.10.20 # doit répondre
```
Si storage-01 n'a pas encore `192.168.10.20 gpu-01` dans `/etc/hosts` :
```bash
ssh s01 "echo '192.168.10.20 gpu-01' | sudo tee -a /etc/hosts"
```
Vérifier que gpu-01 a accès à internet via storage-01 (NAT) :
```bash
ssh -J s01 root@192.168.10.20 "ping -c3 8.8.8.8"
```
---
## 4. Prérequis : storage-01 opérationnel
gpu-01 dépend de storage-01 pour :
- **DNS** : `192.168.10.1` doit résoudre `lab.local`
- **NAT** : pour télécharger les packages ROCm (~10 GB de téléchargements)
- **NFS** : `/srv/data/models` doit être exporté pour monter les modèles
Vérifier avant de continuer :
```bash
ssh s01 "sudo systemctl is-active dnsmasq nftables nfs-server"
# → active / active / active
```
---
## 5. Jouer le playbook Ansible
Tout se lance depuis le poste perso à la racine du repo.
### Test de connectivité
```bash
make ping
# gpu-01 doit maintenant répondre
```
Si gpu-01 n'est pas encore dans l'inventaire, il l'est déjà (`ansible/inventory.yml`) :
```yaml
gpu_hosts:
hosts:
gpu-01:
ansible_host: 192.168.10.20
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ProxyJump=root@192.168.1.200'
```
La première exécution se fait en root. Après le rôle `common`, ce sera le compte `ansible`.
### Phase 1 — Base OS
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags common
```
Même chose que storage-01 : hostname, timezone, EPEL, user ansible, SSH durci.
### Phase 2 — ROCm
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags rocm
```
Le rôle `rocm` fait :
- Ajoute le repo AMD officiel (`amdgpu-install`)
- Installe ROCm 7.x (`rocm-hip-sdk`, `rocm-opencl-runtime`, `rocminfo`, `clinfo`)
- Ajoute l'user `ansible` aux groupes `video` et `render` (accès GPU sans root)
- Déploie `/etc/environment` et `/etc/profile.d/rocm.sh` avec `HSA_OVERRIDE_GFX_VERSION=10.3.0`
- Reload udev pour les permissions GPU
> L'installation ROCm pèse environ 8-10 GB — prévoir 15-20 minutes selon la connexion.
> gpu-01 télécharge via le NAT de storage-01.
Vérifier :
```bash
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo | grep -A3 'Agent 2'"
# doit lister : AMD Radeon RX 6700 XT, gfx1031
```
### Phase 3 — NFS client (modèles)
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags nfs_client
```
Le rôle `nfs_client` fait :
- Installe `nfs-utils`, `autofs`
- Configure les montages dans `/etc/fstab` (ou automount) :
- `/mnt/nfs``192.168.10.1:/srv/data/nfs/k8s`
- `/mnt/models``192.168.10.1:/srv/data/models`
- Options : `nfsvers=4,soft,timeo=30,retrans=3,x-systemd.automount`
- Crée le symlink `/opt/lmstudio/.lmstudio/models``/mnt/models`
> Les montages sont `soft` avec timeout : si storage-01 est éteint, gpu-01 boot
> normalement et ne reste pas bloqué à attendre le NFS.
Vérifier :
```bash
ssh g01 "df -h /mnt/models"
ssh g01 "ls /mnt/models/"
# → bartowski/Qwen3-8B-GGUF/ (ou autre modèle présent sur le RAID)
```
### Phase 4 — llama-server (inférence ROCm)
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags llama_server
```
Le rôle `llama_server` fait :
- Télécharge le binaire `llama-server` pré-compilé pour ROCm (release llama.cpp GitHub)
- Déploie `/etc/systemd/system/llama-server.service` :
- `HSA_OVERRIDE_GFX_VERSION=10.3.0` dans l'environnement
- `--model /mnt/models/bartowski/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf`
- `--alias qwen3-8b`
- `--ctx-size 32768`
- `--host 0.0.0.0 --port 1234`
- `--embeddings --pooling mean` (pour `/v1/embeddings` OAI-compatible)
- Active et démarre `llama-server.service`
Variables dans `host_vars/gpu-01/vars.yml` :
```yaml
llama_model_path: "/mnt/models/bartowski/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf"
llama_model_alias: "qwen3-8b"
llama_ctx_size: 32768
```
> Avec un contexte de 32768 tokens, l'empreinte mémoire est :
> ~5 GB modèle + ~3 GB KV cache = ~8 GB sur 12 GB VRAM.
> Ajuster `llama_ctx_size` si un modèle plus lourd est utilisé.
---
## 6. Vérifications post-install
### Checklist
```bash
# 1. GPU détecté par ROCm
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo | grep -E 'Agent|gfx'"
# 2. NFS monté
ssh g01 "df -h /mnt/models && ls /mnt/models/"
# 3. llama-server actif
ssh g01 "sudo systemctl is-active llama-server"
ssh g01 "curl -s http://localhost:1234/v1/models | python3 -m json.tool"
# 4. Inférence fonctionnelle (test depuis storage-01)
ssh s01 "curl -s http://192.168.10.20:1234/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer lm-studio' \
-d '{\"model\":\"qwen3-8b\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":10}'"
# 5. Embeddings fonctionnels
ssh s01 "curl -s http://192.168.10.20:1234/v1/embeddings \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer lm-studio' \
-d '{\"model\":\"qwen3-8b\",\"input\":\"test\"}' | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d[\"data\"][0][\"embedding\"]), \"dimensions\")'"
# → 4096 dimensions
```
### VRAM en cours d'inférence
```bash
ssh g01 "watch -n 2 'cat /sys/class/drm/card*/device/mem_info_vram_used'"
# Affiche la VRAM utilisée en octets — diviser par 1073741824 pour avoir des GB
```
---
## 7. Pièges et incidents rencontrés
---
### ❌ ROCm ne voit pas le GPU — HSA_OVERRIDE absent
**Symptôme** : `rocminfo` liste uniquement le CPU, pas le GPU RX 6700XT.
llama-server démarre mais tourne sur le CPU → ~2 tok/s au lieu de ~35 tok/s.
**Cause** : La RX 6700XT est `gfx1031`, non officiel dans ROCm. Sans `HSA_OVERRIDE_GFX_VERSION=10.3.0`,
ROCm ignore ce GPU.
**Fix** : La variable doit être présente dans :
1. `/etc/environment` (sessions interactives)
2. Le `[Service]` de `llama-server.service` (service systemd)
```ini
[Service]
Environment=HSA_OVERRIDE_GFX_VERSION=10.3.0
```
Géré par le rôle Ansible `llama_server`. Vérifier :
```bash
ssh g01 "sudo systemctl cat llama-server | grep HSA"
```
---
### ❌ Gemma 4 — crash ROCm au-delà de 800 tokens
**Symptôme** : llama-server crash ou freeze dès que le contexte dépasse ~800 tokens
avec les modèles Gemma 4.
**Cause** : Bug de compatibilité entre l'architecture Gemma 4 et le backend ROCm
sur cette combinaison ROCm 7.x + gfx1031 (non officiel). Non reproduit avec Qwen3.
**Fix** : Utiliser Qwen3-8B ou Qwen2.5-14B. Ne pas charger de modèles Gemma 4 sur gpu-01.
---
### ❌ Embeddings — erreur si --pooling absent
**Symptôme** : Requêtes vers `/v1/embeddings` retournent une erreur 500 ou des vecteurs vides.
**Cause** : llama-server nécessite `--embeddings` ET `--pooling mean` pour activer
les embeddings OAI-compatible. `--embeddings` seul active le endpoint mais sans
pooling les résultats sont incorrects.
**Fix** : Le service doit avoir les deux flags :
```
--embeddings --pooling mean
```
Géré par le rôle Ansible `llama_server`.
---
### ❌ NFS bloquant au boot si storage-01 est éteint
**Symptôme** : gpu-01 boot très lentement (plusieurs minutes) ou reste bloqué en
attendant les montages NFS si storage-01 n'est pas allumé.
**Cause** : Montages NFS en `hard` (comportement par défaut) — le client attend
indéfiniment que le serveur réponde.
**Fix** : Options `soft,timeo=30,retrans=3` dans fstab/automount + `x-systemd.automount`.
Avec `soft`, si storage-01 ne répond pas en 30 × timeo ms, le montage échoue proprement
et gpu-01 continue à démarrer. Le montage sera réessayé au premier accès.
Géré par le rôle Ansible `nfs_client`.
---
### ❌ llama-server redémarre pas si modèle indisponible au boot
**Symptôme** : `llama-server.service` en `failed` au démarrage car `/mnt/models/...`
n'est pas encore monté (NFS automount).
**Cause** : Conflit d'ordre de démarrage — llama-server démarre avant que
le montage NFS soit déclenché.
**Fix** : `After=mnt-models.automount` dans le `.service`. Géré par Ansible.
Ou simplement `sudo systemctl start llama-server` après boot.
---
### Résumé des points d'attention
| Sujet | Règle |
|---|---|
| HSA_OVERRIDE | `HSA_OVERRIDE_GFX_VERSION=10.3.0` dans tout service GPU et dans `/etc/environment` |
| Gemma 4 | Interdit sur gpu-01 — crash ROCm >800 tokens |
| Embeddings | `--embeddings --pooling mean` obligatoires ensemble |
| Contexte 32k | ~8 GB VRAM sur 12 GB — ajuster si modèle plus lourd |
| NFS boot | Options `soft,timeo=30` — jamais `hard` |
| ROCm install | ~10 GB de téléchargement — prévoir le NAT storage-01 opérationnel |

728
admin/install/storage-01.md Normal file
View file

@ -0,0 +1,728 @@
# Installation storage-01
Guide d'installation complet de storage-01 — de l'ISO AlmaLinux à la stack IA
complète (Hermes + LiteLLM + Qdrant + PostgreSQL). Écrit après installation réelle.
---
## Sommaire
1. [Rôle et architecture](#1-rôle-et-architecture)
2. [Installation AlmaLinux 9.7 (base OS)](#2-installation-almalinux-97-base-os)
3. [Configuration réseau initiale (manuelle)](#3-configuration-réseau-initiale-manuelle)
4. [Préparer le poste perso pour Ansible](#4-préparer-le-poste-perso-pour-ansible)
5. [Cloner le repo et initialiser Ansible](#5-cloner-le-repo-et-initialiser-ansible)
6. [Configurer le vault Ansible (secrets)](#6-configurer-le-vault-ansible-secrets)
7. [Jouer le playbook — phase par phase](#7-jouer-le-playbook--phase-par-phase)
8. [Vérifications post-install](#8-vérifications-post-install)
9. [Pièges et incidents rencontrés](#9-pièges-et-incidents-rencontrés)
---
## 1. Rôle et architecture
storage-01 est la machine centrale du lab. Elle n'est **pas dans le cluster Kubernetes**
— elle est la fondation sur laquelle tout le reste repose.
### Ce qu'elle fait
| Fonction | Détail |
|---|---|
| Passerelle NAT | Forward LAN cluster → internet, LAN domestique → LAN cluster |
| DHCP + DNS | dnsmasq pour le LAN cluster (`192.168.10.0/24`) et le wildcard `*.lab.local` |
| Stockage RAID5 | 4 disques SATA 1 TB → 2,7 TB utiles, montés sur `/srv/data` |
| NFS | Exports vers gpu-01 et le cluster k8s (`/srv/data/nfs/k8s`, `/srv/data/models`) |
| Hermes Agent | Agent IA autonome avec mémoire vectorielle |
| LiteLLM Proxy | Routage LLM : Qwen local (gpu-01) ↔ Claude API (Anthropic) |
| Qdrant | Base vectorielle pour la mémoire de Hermes (`hermes_memory`) |
| PostgreSQL 16 | Bases pour Hermes et LiteLLM |
### Interfaces réseau
| Interface | IP | Rôle |
|---|---|---|
| `enp4s0` | `192.168.1.200` | WAN — vers Freebox (LAN domestique `192.168.1.0/24`) |
| `enp6s0f3u2c2` | `192.168.10.1` | LAN cluster (`192.168.10.0/24`) — carte USB-Ethernet |
> `enp6s0f3u2c2` est une carte USB-Ethernet branchée en permanence — les noms d'interface sont contre-intuitifs.
### RAID5
4 disques SATA 1 TB (Seagate/WD) assemblés en RAID5 software Linux (`md0` ou `md127`).
- Capacité utile : ~2,7 TB
- Tolérance : 1 disque mort
- Point de montage : `/srv/data`
- ⚠️ `sdb` a généré des erreurs I/O après une coupure de courant — surveiller avec `smartctl`
---
## 2. Installation AlmaLinux 9.7 (base OS)
### Télécharger l'ISO
AlmaLinux 9.7 minimal (pas de desktop) :
`https://repo.almalinux.org/almalinux/9/isos/x86_64/`
Flasher sur clé USB :
```bash
sudo dd if=AlmaLinux-9.7-x86_64-minimal.iso of=/dev/sdX bs=4M status=progress conv=fsync
```
### Paramètres d'installation (Anaconda)
Dans l'installeur graphique Anaconda :
**Langue / clavier** : Français ou English — peu importe, Ansible configure le reste.
**Disque système** : sélectionner le NVMe interne uniquement.
- Ne pas sélectionner les disques SATA (RAID5) — ils sont gérés manuellement.
- Partitionnement automatique convient (LVM sur NVMe).
**Réseau** : configurer uniquement l'interface WAN (`enp4s0`) pendant l'install :
- IP statique : `192.168.1.200/24`
- Gateway : `192.168.1.254` (Freebox)
- DNS : `1.1.1.1`
- Hostname : `storage-01`
L'interface LAN (`enp6s0f3u2c2`) sera configurée par Ansible.
**Utilisateurs** :
- Root : définir un mot de passe temporaire (sera désactivé par Ansible)
- Créer un premier utilisateur `ansible` (ou laisser Ansible le créer — dans ce cas se connecter en root pour la première exécution)
### Premier boot
Après l'installation, vérifier que la machine est joignable depuis le poste perso :
```bash
ping 192.168.1.200
ssh root@192.168.1.200
```
---
## 3. Configuration réseau initiale (manuelle)
Avant de lancer Ansible, il faut que storage-01 soit SSH-accessible et que les deux
interfaces réseau soient opérationnelles. Ces opérations sont faites une seule fois
depuis la console ou root SSH.
### Configurer l'interface LAN (USB-Ethernet)
```bash
# Se connecter en root
ssh root@192.168.1.200
# Identifier la carte USB-Ethernet (généralement enp6s0f3u2c2 ou similaire)
nmcli device status
ip link show
# Créer la connexion NetworkManager
nmcli connection add type ethernet \
ifname enp6s0f3u2c2 \
con-name lan-cluster \
ipv4.method manual \
ipv4.addresses 192.168.10.1/24 \
ipv4.never-default yes \
connection.autoconnect yes
nmcli connection up lan-cluster
```
Vérifier :
```bash
ip addr show enp6s0f3u2c2
# → inet 192.168.10.1/24
```
### Activer l'IP forwarding
```bash
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/99-forwarding.conf
sysctl -p /etc/sysctl.d/99-forwarding.conf
```
> Ansible gère aussi l'IP forwarding, mais l'activer maintenant permet aux nœuds
> Talos de sortir sur internet dès que dnsmasq et nftables seront en place.
### Ajouter gpu-01 dans /etc/hosts (si gpu-01 sera installé avant dnsmasq)
```bash
echo '192.168.10.20 gpu-01' >> /etc/hosts
```
---
## 4. Préparer le poste perso pour Ansible
Tout Ansible se lance depuis le **poste perso** (ou storage-01 une fois qu'il est
opérationnel — voir section suivante).
### Installer Python et créer le venv
```bash
# Python 3 doit être disponible
python3 --version
# Créer le venv à la racine du repo
cd /mnt/nvme0n1/nvme0n1p3/Projets/lab
python3 -m venv .venv
source .venv/bin/activate
# Installer ansible-core
pip install -r requirements.txt
# ansible-core>=2.17,<2.18
```
### Installer les collections Ansible
```bash
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
```
### Générer une clé SSH pour Ansible
```bash
# Si pas encore de clé ed25519
ssh-keygen -t ed25519 -C "ansible@poste-perso" -f ~/.ssh/id_ed25519
# Copier la clé publique vers storage-01
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@192.168.1.200
```
Ansible utilisera le compte `ansible` (créé par le rôle `common`) — mais pour la
première exécution, il se connecte en root.
### Configurer ~/.ssh/config
```
Host s01
HostName 192.168.1.200
User ansible
IdentityFile ~/.ssh/id_ed25519
Host g01
HostName 192.168.10.20
User ansible
ProxyJump s01
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking no
```
---
## 5. Cloner le repo et initialiser Ansible
```bash
git clone git@github.com:Alkatrazz24/Funk-lab.git
cd Funk-lab
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
```
### Structure Ansible du repo
```
ansible/
├── inventory.yml # hosts + groupes
├── playbooks/
│ ├── site.yml # import storage-01.yml + gpu-01.yml
│ ├── storage-01.yml # playbook storage-01
│ └── gpu-01.yml # playbook gpu-01
├── group_vars/
│ ├── all/
│ │ ├── vars.yml # variables globales (dns_domain, cluster_network...)
│ │ └── vault.yml # secrets chiffrés (Ansible Vault)
│ └── gateway/
│ └── vars.yml # interfaces réseau storage-01
├── host_vars/
│ ├── storage-01/
│ │ └── vars.yml # RAID uuid, NFS exports, chemins services
│ └── gpu-01/
│ └── vars.yml # modèle llama-server, montages NFS
└── roles/
├── common/ # hostname, timezone, EPEL, user ansible, SSH hardenin
├── gateway/ # nftables + NAT + IP forwarding
├── dnsmasq/ # DNS lab.local + DHCP cluster
├── nfs_server/ # RAID5 fstab + exports NFS
├── postgresql/ # PostgreSQL 16 sur RAID5
├── qdrant/ # Qdrant vectordb sur RAID5
├── litellm/ # LiteLLM proxy + hermes-switch
├── hermes_agent/ # Hermes Agent + Dashboard
├── rocm/ # ROCm AMD pour gpu-01
├── llama_server/ # llama-server sur gpu-01
└── nfs_client/ # montages NFS automatiques (gpu-01)
```
### Variables globales (`group_vars/all/vars.yml`)
```yaml
dns_domain: lab.local
ntp_servers:
- 0.fr.pool.ntp.org
- 1.fr.pool.ntp.org
cluster_network: 192.168.10.0/24
wan_gateway: 192.168.1.254
```
### Variables gateway (`group_vars/gateway/vars.yml`)
```yaml
wan_interface: enp4s0 # NIC PCIe — vers Freebox
lan_interface: enp6s0f3u2c2 # NIC USB — vers LAN cluster
wan_ip: 192.168.1.200
lan_ip: 192.168.10.1
```
### Variables storage-01 (`host_vars/storage-01/vars.yml`)
```yaml
ansible_host: 192.168.1.200
raid_uuid: "3add8360-fa01-47eb-8aa1-0e84bacbbc15"
raid_mount: /srv/data
nfs_exports:
- path: /srv/data/nfs/k8s
network: "{{ cluster_network }}"
options: "rw,sync,no_subtree_check,no_root_squash"
- path: /srv/data/models
network: "{{ cluster_network }}"
options: "rw,sync,no_subtree_check,no_root_squash"
hermes_data_dir: /srv/data/hermes
postgres_data_dir: /srv/data/postgres
qdrant_data_dir: /srv/data/qdrant
```
---
## 6. Configurer le vault Ansible (secrets)
Le vault contient les mots de passe et l'API key Anthropic. Il est chiffré dans git.
### Fichier mot de passe vault
```bash
# Créer le fichier de mot de passe (ne jamais le committer — dans .gitignore)
echo "ton-mot-de-passe-vault" > .vault_pass
chmod 600 .vault_pass
```
`ansible.cfg` pointe automatiquement dessus :
```ini
[defaults]
vault_password_file = .vault_pass
```
### Créer le vault avec les secrets
```bash
cd ansible/
../.venv/bin/ansible-vault create group_vars/all/vault.yml
```
Contenu du vault :
```yaml
vault_anthropic_api_key: "sk-ant-..."
vault_pg_hermes_password: "mot-de-passe-fort"
vault_pg_litellm_password: "mot-de-passe-fort"
```
Pour modifier plus tard :
```bash
make vault-edit
# ou
cd ansible && ../.venv/bin/ansible-vault edit group_vars/all/vault.yml
```
---
## 7. Jouer le playbook — phase par phase
Il est recommandé d'appliquer les rôles dans l'ordre ci-dessous, par petits
blocs, pour valider chaque couche avant de passer à la suivante.
### Test de connectivité
```bash
make ping
# ou
cd ansible && ../.venv/bin/ansible all -m ping
```
Sortie attendue :
```
storage-01 | SUCCESS => {"ping": "pong"}
```
> gpu-01 échouera si pas encore installé — normal.
### Phase 1 — Base OS
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags common
```
Le rôle `common` fait :
- Hostname → `storage-01`
- Timezone → Europe/Paris
- EPEL activé
- `dnf update -y` (tous les packages à jour)
- Packages de base : `vim`, `curl`, `wget`, `git`, `htop`, `tmux`, `rsync`, `lsof`, `net-tools`, `bind-utils`
- Création user `ansible` avec sudo NOPASSWD
- Clé SSH publique du poste perso déposée dans `~/.ssh/authorized_keys`
- SSH durci : `PermitRootLogin no`, `PasswordAuthentication no`, `X11Forwarding no`
Après cette étape, les connexions root ne fonctionnent plus — utiliser `ssh s01`.
### Phase 2 — Passerelle (nftables + NAT)
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags gateway
```
Le rôle `gateway` fait :
- Déploie `/etc/nftables.conf` depuis le template (flush + rebuild complet)
- Active IP forwarding (`sysctl net.ipv4.ip_forward=1`)
- Active et redémarre `nftables`
Vérifier que le NAT fonctionne depuis le LAN cluster :
```bash
# Depuis une machine du cluster (si déjà installée)
ssh 192.168.10.11 "curl -s https://ifconfig.me"
```
### Phase 3 — DNS + DHCP (dnsmasq)
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags dnsmasq
```
Le rôle `dnsmasq` fait :
- Installe `dnsmasq`
- Déploie la config depuis template : DNS `lab.local`, DHCP `192.168.10.50-99`, baux statiques MACs
- Wildcard `*.lab.local → 192.168.10.200` (Traefik)
- Override systemd : dnsmasq attend que l'interface USB-Ethernet soit prête avant de démarrer
Tester depuis le poste perso :
```bash
dig @192.168.10.1 storage-01.lab.local +short
# → 192.168.10.1
dig @192.168.10.1 test.lab.local +short
# → 192.168.10.200 (wildcard)
```
> Sans l'override systemd, dnsmasq démarre avant que `enp6s0f3u2c2` (USB-Ethernet)
> soit disponible → `bind(): Address not available` → service en échec au boot.
### Phase 4 — NFS + RAID5
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags nfs_server
```
Le rôle `nfs_server` fait :
- Installe `nfs-utils` et `mdadm`
- Persiste la config RAID dans `/etc/mdadm.conf` (pour survie aux reboots)
- Ajoute `/srv/data` dans `/etc/fstab` avec `_netdev 0 0`
- Crée les répertoires d'export (`/srv/data/nfs/k8s`, `/srv/data/models`)
- Déploie `/etc/exports` et recharge `nfs-server`
Vérifier :
```bash
ssh s01 "cat /proc/mdstat" # RAID doit être [4/4] [UUUU]
ssh s01 "sudo exportfs -v" # doit lister les deux exports
ssh s01 "df -h /srv/data" # doit afficher ~2.7 TB
```
> **UUID du RAID** : récupérer avec `sudo blkid | grep md` sur storage-01 et
> mettre à jour `host_vars/storage-01/vars.yml` avant de jouer ce rôle.
### Phase 5 — PostgreSQL 16
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags postgresql
```
Le rôle `postgresql` fait :
- Installe le repo officiel PGDG (EL9)
- Désactive le module AppStream par défaut (`dnf module disable postgresql`)
- Installe `postgresql16-server` et `python3-psycopg2`
- Crée `/srv/data/postgres` sur le RAID5 et le configure comme `PGDATA`
- Override systemd pour pointer sur le bon `PGDATA`
- `initdb` si la base n'existe pas encore
- Crée les bases `hermes` et `litellm` et leurs users avec les mots de passe du vault
Vérifier :
```bash
ssh s01 "sudo -u postgres psql -c '\l'"
# doit lister : hermes, litellm, postgres, template0, template1
```
### Phase 6 — Qdrant
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags qdrant
```
Le rôle `qdrant` fait :
- Télécharge le binaire Qdrant (release GitHub)
- Crée un user système `qdrant`
- Configure le stockage dans `/srv/data/qdrant`
- Déploie `qdrant.service` (bind `0.0.0.0:6333`, gRPC `6334`)
- Active et démarre le service
Vérifier :
```bash
ssh s01 "curl -s http://localhost:6333/healthz"
# → {"result":"ok","status":"ok","time":0.00...}
```
### Phase 7 — LiteLLM Proxy
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags litellm
```
Le rôle `litellm` fait :
- Installe `litellm` via pip dans un venv dédié
- Déploie `/etc/litellm/config.yaml` avec les modèles (Qwen local + Claude)
- Configure `master_key: lm-studio` (aligné avec `LM_API_KEY=lm-studio` dans Hermes)
- Déploie `litellm.service` (bind `127.0.0.1:4000` — jamais exposé sur le réseau)
- Déploie le script `hermes-switch` dans `/usr/local/bin/`
Config LiteLLM (résumé) :
```yaml
model_list:
- model_name: hermes-default
litellm_params:
model: openai/qwen3-8b
api_base: http://192.168.10.20:1234/v1
api_key: lm-studio
- model_name: claude-sonnet-4-6
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: "{{ vault_anthropic_api_key }}"
general_settings:
master_key: lm-studio
```
Vérifier :
```bash
ssh s01 "curl -s http://localhost:4000/health -H 'Authorization: Bearer lm-studio'"
```
### Phase 8 — Hermes Agent
```bash
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags hermes_agent
```
Le rôle `hermes_agent` fait :
- Crée un user système `hermes` avec home sur le RAID5 (`/srv/data/hermes`)
- Installe Hermes via son installer officiel dans `/opt/hermes`
- Crée le venv Python (`/srv/data/hermes/hermes-agent/venv/`) avec `qdrant-client`
- Déploie `~hermes/.config/hermes/config.yaml` :
- `model.provider: lmstudio`
- `model.base_url: http://127.0.0.1:4000/v1`
- `model.name: hermes-default`
- Déploie `hermes-agent.service` et `hermes-dashboard.service`
- Déploie `/usr/local/bin/hermes-tui` (wrapper pour TUI interactif)
- Permissions `/opt/hermes``711` (traversable, non listable)
Vérifier :
```bash
ssh s01 "sudo systemctl status hermes-agent hermes-dashboard"
```
Dashboard accessible depuis le poste perso (restreint à `192.168.1.10`) :
```
http://192.168.1.200:9119
```
TUI interactif (doit être lancé sous le compte hermes) :
```bash
ssh storage-01
sudo -i -u hermes
cd /srv/data/hermes
hermes --tui
```
---
## 8. Vérifications post-install
### Checklist complète
```bash
# 1. Réseau
ping 192.168.10.20 # gpu-01 joignable
ssh g01 "curl -s https://ifconfig.me" # NAT fonctionnel
dig @192.168.10.1 storage-01.lab.local +short # DNS lab.local
# 2. RAID
ssh s01 "cat /proc/mdstat" # [4/4] [UUUU]
ssh s01 "df -h /srv/data" # ~2.7 TB monté
# 3. NFS
ssh s01 "sudo exportfs -v" # exports actifs
# 4. PostgreSQL
ssh s01 "sudo -u postgres psql -c '\l'" # bases hermes + litellm présentes
# 5. Qdrant
ssh s01 "curl -s http://localhost:6333/healthz" | python3 -m json.tool
# 6. LiteLLM
ssh s01 "curl -s http://localhost:4000/health -H 'Authorization: Bearer lm-studio'"
# 7. Hermes
ssh s01 "sudo systemctl is-active hermes-agent hermes-dashboard"
# 8. funk-cluster (script de gestion global)
ssh s01 "sudo /usr/local/bin/funk-cluster status"
```
### Services actifs attendus
```bash
ssh s01 "sudo systemctl list-units --type=service --state=running | grep -E 'dnsmasq|nfs|postgres|qdrant|litellm|hermes|nftables'"
```
---
## 9. Pièges et incidents rencontrés
---
### ❌ dnsmasq ne démarre pas au boot — "Address not available"
**Symptôme** : `systemctl status dnsmasq``bind(): Address not available`.
Storage-01 rebooté → dnsmasq en `failed`, plus de DHCP pour le cluster.
**Cause** : dnsmasq tentait de binder sur `192.168.10.1` (`enp6s0f3u2c2`, USB-Ethernet)
avant que cette interface soit disponible. L'USB-Ethernet est détectée plus tard que
les NIC PCIe dans la séquence de boot.
**Fix** : Un override systemd dans `/etc/systemd/system/dnsmasq.service.d/wait-lan.conf` :
```ini
[Unit]
After=network-online.target sys-subsystem-net-devices-enp6s0f3u2c2.device
Wants=network-online.target sys-subsystem-net-devices-enp6s0f3u2c2.device
```
Géré par le rôle Ansible `dnsmasq` — ne pas modifier manuellement.
---
### ❌ Boucle fsck au boot après coupure de courant
**Symptôme** : Boot bloqué sur `emergency mode`, impossible de saisir le mot de passe root
(messages d'erreur défilants), le RAID `/dev/md0` ne monte plus.
**Cause** : Coupure de courant → corruption du filesystem XFS sur `almalinux-root` (NVMe).
systemd tentait de monter `/srv/data` (RAID5) avant que le RAID soit assemblé, créant
une boucle bloquante.
**Résolution** (procédure complète dans `admin/incidents.md`) :
1. Shell via GRUB (`init=/bin/bash` dans la ligne kernel)
2. `lvm vgchange -ay` pour activer le LVM
3. `xfs_repair /dev/mapper/almalinux-root` (pas `fsck` — XFS a son propre outil)
4. Commenter `/srv/data` dans `/etc/fstab` temporairement pour booter
5. Après boot : corriger fstab → ajouter `_netdev 0 0` pour `/srv/data`
**Fix permanent dans fstab** :
```
UUID=3add8360-... /srv/data ext4 defaults,noatime,nodiratime,_netdev 0 0
```
- `_netdev` : attend que les périphériques soient prêts (RAID assemblé) avant de monter
- `0 0` : désactive le `fsck` automatique au boot (le RAID a sa propre intégrité)
Géré par le rôle Ansible `nfs_server`. Vérifier que ces options sont bien présentes.
---
### ❌ hermes --tui "gateway error" si lancé sous le mauvais compte
**Symptôme** : `hermes --tui` lancé depuis le compte `ansible` retourne des "gateway error"
en boucle. TUI inutilisable.
**Cause** : Hermes charge sa configuration depuis `$HERMES_HOME`, qui doit pointer vers
`/srv/data/hermes`. Lancé sous `ansible`, les variables d'environnement ne sont pas
correctes et `hermes` cherche sa config dans `~ansible/.config/hermes/` qui n'existe pas.
**Fix** : Toujours lancer le TUI sous le compte `hermes` :
```bash
ssh storage-01
sudo -i -u hermes
cd /srv/data/hermes
hermes --tui
```
Ou via le wrapper `/usr/local/bin/hermes-tui` qui gère automatiquement l'environnement.
---
### ❌ qdrant-client : méthode search() supprimée en v1.14+
**Symptôme** : Scripts Python Hermes qui appellent Qdrant échouent avec
`AttributeError: 'AsyncQdrantClient' object has no attribute 'search'`.
**Cause** : `qdrant-client` v1.14+ a supprimé la méthode `search()`.
Elle est remplacée par `query_points()`.
**Fix** : Mettre à jour les scripts pour utiliser `query_points()`.
Le rôle Ansible `hermes_agent` installe la version compatible — ne pas faire
`pip install qdrant-client` manuellement sans vérifier la version.
---
### ❌ LM_API_KEY absent → Hermes ne peut pas appeler LiteLLM
**Symptôme** : Hermes démarre mais toutes les requêtes vers LiteLLM échouent avec 401.
**Cause** : Hermes utilise `LM_API_KEY` comme Bearer token. LiteLLM attend `master_key`.
Les deux doivent avoir la même valeur. Si `LM_API_KEY` n'est pas défini dans
l'environnement du service systemd, Hermes envoie un token vide ou incorrect.
**Fix** : Le service `hermes-agent.service` doit avoir dans sa section `[Service]` :
```ini
Environment=LM_API_KEY=lm-studio
```
La valeur `lm-studio` est une valeur magique alignée avec `master_key: lm-studio`
dans la config LiteLLM. Géré par le rôle Ansible.
---
### ❌ hermes -z ne fonctionne pas depuis n'importe où
**Symptôme** : `hermes -z "question"` depuis `/home/ansible` → erreur de chemin,
Hermes ne trouve pas son `.git`.
**Cause** : Hermes remonte l'arborescence à la recherche d'un `.git` pour localiser
sa config de projet. Si lancé depuis un répertoire sans `.git` en amont, il échoue.
**Fix** : Toujours lancer depuis `/srv/data/hermes` :
```bash
cd /srv/data/hermes && hermes -z "ta question"
```
---
### Résumé des points d'attention
| Sujet | Règle |
|---|---|
| dnsmasq au boot | Override systemd obligatoire pour USB-Ethernet |
| fstab `/srv/data` | `_netdev 0 0` — jamais `0 2` |
| TUI Hermes | Toujours sous le compte `hermes`, depuis `/srv/data/hermes` |
| `hermes -z` | Toujours depuis `/srv/data/hermes` |
| LM_API_KEY | Doit valoir `lm-studio` (= `master_key` LiteLLM) |
| qdrant-client | Utiliser `query_points()` pas `search()` (supprimé en v1.14+) |
| RAID sdb | Surveiller régulièrement avec `smartctl -a /dev/sdb` |
| UUID RAID | Vérifier `host_vars/storage-01/vars.yml` avant de jouer `nfs_server` |
| Vault | `.vault_pass` ne doit jamais être commité — dans `.gitignore` |