From 0ce6be04bbfa20781a58fe5aadde95eefc237df1 Mon Sep 17 00:00:00 2001 From: alkatrazz Date: Tue, 12 May 2026 23:03:05 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20r=C3=A9organisation=20install/=20?= =?UTF-8?q?=E2=80=94=20guides=20s=C3=A9par=C3=A9s=20par=20machine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- admin/README.md | 11 +- admin/install/README.md | 22 + admin/install/gpu-01.md | 365 ++++++++++ admin/{installe.md => install/kubernetes.md} | 0 admin/install/storage-01.md | 728 +++++++++++++++++++ 5 files changed, 1125 insertions(+), 1 deletion(-) create mode 100644 admin/install/README.md create mode 100644 admin/install/gpu-01.md rename admin/{installe.md => install/kubernetes.md} (100%) create mode 100644 admin/install/storage-01.md diff --git a/admin/README.md b/admin/README.md index 0e63ef3..44b4849 100644 --- a/admin/README.md +++ b/admin/README.md @@ -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]"` | diff --git a/admin/install/README.md b/admin/install/README.md new file mode 100644 index 0000000..ea82722 --- /dev/null +++ b/admin/install/README.md @@ -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). diff --git a/admin/install/gpu-01.md b/admin/install/gpu-01.md new file mode 100644 index 0000000..a1949ca --- /dev/null +++ b/admin/install/gpu-01.md @@ -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 | diff --git a/admin/installe.md b/admin/install/kubernetes.md similarity index 100% rename from admin/installe.md rename to admin/install/kubernetes.md diff --git a/admin/install/storage-01.md b/admin/install/storage-01.md new file mode 100644 index 0000000..1f9a300 --- /dev/null +++ b/admin/install/storage-01.md @@ -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` |