- 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>
22 KiB
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
- Rôle et architecture
- Installation AlmaLinux 9.7 (base OS)
- Configuration réseau initiale (manuelle)
- Préparer le poste perso pour Ansible
- Cloner le repo et initialiser Ansible
- Configurer le vault Ansible (secrets)
- Jouer le playbook — phase par phase
- Vérifications post-install
- 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 |
enp6s0f3u2c2est 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 - ⚠️
sdba généré des erreurs I/O après une coupure de courant — surveiller avecsmartctl
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 :
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 :
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)
# 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 :
ip addr show enp6s0f3u2c2
# → inet 192.168.10.1/24
Activer l'IP forwarding
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)
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
# 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
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
Générer une clé SSH pour Ansible
# 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
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)
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)
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)
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
# 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 :
[defaults]
vault_password_file = .vault_pass
Créer le vault avec les secrets
cd ansible/
../.venv/bin/ansible-vault create group_vars/all/vault.yml
Contenu du vault :
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 :
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é
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
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
ansibleavec 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)
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags gateway
Le rôle gateway fait :
- Déploie
/etc/nftables.confdepuis 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 :
# 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)
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, DHCP192.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 :
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
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags nfs_server
Le rôle nfs_server fait :
- Installe
nfs-utilsetmdadm - Persiste la config RAID dans
/etc/mdadm.conf(pour survie aux reboots) - Ajoute
/srv/datadans/etc/fstabavec_netdev 0 0 - Crée les répertoires d'export (
/srv/data/nfs/k8s,/srv/data/models) - Déploie
/etc/exportset rechargenfs-server
Vérifier :
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 mdsur storage-01 et mettre à jourhost_vars/storage-01/vars.ymlavant de jouer ce rôle.
Phase 5 — PostgreSQL 16
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-serveretpython3-psycopg2 - Crée
/srv/data/postgressur le RAID5 et le configure commePGDATA - Override systemd pour pointer sur le bon
PGDATA initdbsi la base n'existe pas encore- Crée les bases
hermesetlitellmet leurs users avec les mots de passe du vault
Vérifier :
ssh s01 "sudo -u postgres psql -c '\l'"
# doit lister : hermes, litellm, postgres, template0, template1
Phase 6 — Qdrant
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(bind0.0.0.0:6333, gRPC6334) - Active et démarre le service
Vérifier :
ssh s01 "curl -s http://localhost:6333/healthz"
# → {"result":"ok","status":"ok","time":0.00...}
Phase 7 — LiteLLM Proxy
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags litellm
Le rôle litellm fait :
- Installe
litellmvia pip dans un venv dédié - Déploie
/etc/litellm/config.yamlavec les modèles (Qwen local + Claude) - Configure
master_key: lm-studio(aligné avecLM_API_KEY=lm-studiodans Hermes) - Déploie
litellm.service(bind127.0.0.1:4000— jamais exposé sur le réseau) - Déploie le script
hermes-switchdans/usr/local/bin/
Config LiteLLM (résumé) :
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 :
ssh s01 "curl -s http://localhost:4000/health -H 'Authorization: Bearer lm-studio'"
Phase 8 — Hermes Agent
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
hermesavec 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/) avecqdrant-client - Déploie
~hermes/.config/hermes/config.yaml:model.provider: lmstudiomodel.base_url: http://127.0.0.1:4000/v1model.name: hermes-default
- Déploie
hermes-agent.serviceethermes-dashboard.service - Déploie
/usr/local/bin/hermes-tui(wrapper pour TUI interactif) - Permissions
/opt/hermes→711(traversable, non listable)
Vérifier :
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) :
ssh storage-01
sudo -i -u hermes
cd /srv/data/hermes
hermes --tui
8. Vérifications post-install
Checklist complète
# 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
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 :
[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) :
- Shell via GRUB (
init=/bin/bashdans la ligne kernel) lvm vgchange -aypour activer le LVMxfs_repair /dev/mapper/almalinux-root(pasfsck— XFS a son propre outil)- Commenter
/srv/datadans/etc/fstabtemporairement pour booter - Après boot : corriger fstab → ajouter
_netdev 0 0pour/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 monter0 0: désactive lefsckautomatique 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 :
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] :
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 :
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 |