mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 06:34:43 +02:00
feat: initial commit — Ansible roles storage-01/gpu-01 opérationnels
This commit is contained in:
commit
b3fce8af5d
70 changed files with 2688 additions and 0 deletions
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Python / Ansible env
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Ansible
|
||||||
|
.vault_pass
|
||||||
|
*.retry
|
||||||
|
ansible/.vault_pass
|
||||||
|
|
||||||
|
# Talos (généré par talhelper — ne pas committer)
|
||||||
|
talos/clusterconfig/
|
||||||
|
|
||||||
|
# SOPS / age (clés privées — jamais dans git)
|
||||||
|
*.age
|
||||||
|
keys.txt
|
||||||
|
|
||||||
|
# Claude Code (settings locaux à la machine)
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Journaux de session (notes de debug, IPs, infos locales)
|
||||||
|
progress/
|
||||||
|
|
||||||
|
# Documentation réseau domestique (topologie LAN privée)
|
||||||
|
Funk/
|
||||||
|
|
||||||
|
# Docs ops (IPs, architecture détaillée, procédures internes)
|
||||||
|
admin/
|
||||||
|
|
||||||
|
# Divers
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
207
CLAUDE.md
Normal file
207
CLAUDE.md
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Présentation du projet
|
||||||
|
|
||||||
|
**Funk** — homelab IA composé d'un cluster Kubernetes (Talos) pour les services applicatifs, d'un hôte d'inférence LLM dédié (gpu-01 / LM Studio), et d'un agent autonome (Hermes Agent) sur storage-01. Stack IaC complète : Ansible + talhelper + ArgoCD.
|
||||||
|
|
||||||
|
Référence complète : `Funk/Funk-infra/HOMELAB.md`
|
||||||
|
|
||||||
|
**État actuel (2026-05-09)** :
|
||||||
|
- ✅ Hermes Agent opérationnel sur le poste perso (LM Studio local `127.0.0.1:1234`)
|
||||||
|
- ✅ storage-01 installé — `192.168.10.1` (LAN cluster), `192.168.1.200` (WAN)
|
||||||
|
- ✅ gpu-01 installé — ping OK depuis storage-01 (`192.168.10.20`)
|
||||||
|
- ⏳ Phase 1/2 en cours — Ansible, ROCm, LM Studio, dnsmasq, NAT restent à faire
|
||||||
|
- ❌ Repo `lab-infra/` à initialiser, aucun cluster k8s monté
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture des machines
|
||||||
|
|
||||||
|
| Machine | IP | OS | Rôle |
|
||||||
|
|---|---|---|---|
|
||||||
|
| storage-01 | 192.168.10.1 | AlmaLinux 9.7 | Passerelle + bastion admin + données + Hermes Agent — **hors cluster** |
|
||||||
|
| compute-01 | 192.168.10.11 | Talos v1.13+ | k8s control-plane |
|
||||||
|
| compute-02 | 192.168.10.12 | Talos v1.13+ | k8s worker |
|
||||||
|
| compute-03 | 192.168.10.13 | Talos v1.13+ | k8s worker |
|
||||||
|
| gpu-01 | 192.168.10.20 | AlmaLinux 9.7 | LM Studio dédié (RX 6700XT) — **hors cluster** |
|
||||||
|
|
||||||
|
**Points clés d'architecture :**
|
||||||
|
- `storage-01` et `gpu-01` sont **hors du cluster k8s** — ils sont consommés via des `ExternalName` services ou `Endpoints` manuels
|
||||||
|
- Le cluster n'a que 3 nœuds (compute-01/02/03), pas de GPU dans k8s
|
||||||
|
- Single control-plane assumé : si compute-01 tombe, `kubectl apply` ne fonctionne plus mais les workloads continuent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure du repo lab-infra (cible)
|
||||||
|
|
||||||
|
```
|
||||||
|
lab-infra/
|
||||||
|
├── ansible/ # gère storage-01 + gpu-01 (AlmaLinux)
|
||||||
|
│ ├── inventory.yml
|
||||||
|
│ ├── playbooks/site.yml
|
||||||
|
│ ├── group_vars/
|
||||||
|
│ └── roles/ # common, gateway, dnsmasq, nfs_server, postgresql,
|
||||||
|
│ # qdrant, minio, rocm, lm_studio, hermes_agent
|
||||||
|
├── talos/ # nœuds Talos via talhelper
|
||||||
|
│ ├── talconfig.yaml # source de vérité (lue par talhelper)
|
||||||
|
│ ├── talsecret.sops.yaml
|
||||||
|
│ ├── talenv.yaml
|
||||||
|
│ ├── patches/
|
||||||
|
│ └── clusterconfig/ # généré → au .gitignore, ne pas committer
|
||||||
|
└── k8s/ # workloads k8s (ArgoCD)
|
||||||
|
├── argocd-bootstrap/
|
||||||
|
├── apps-of-apps/root.yaml
|
||||||
|
├── infra/ # metallb, traefik, cert-manager, longhorn, nfs-provisioner
|
||||||
|
└── apps/ # litellm, open-webui, n8n, monitoring,
|
||||||
|
# external-services (ExternalName vers storage-01/gpu-01)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack IaC — trois outils, trois cibles
|
||||||
|
|
||||||
|
| Outil | Cible | Déclenchement |
|
||||||
|
|---|---|---|
|
||||||
|
| **Ansible** | storage-01, gpu-01 (AlmaLinux) | Push manuel : `ansible-playbook` |
|
||||||
|
| **talhelper** | compute-01/02/03 (Talos) | Push manuel : `talosctl apply-config` |
|
||||||
|
| **ArgoCD** | Workloads k8s | Pull automatique depuis Git |
|
||||||
|
|
||||||
|
### Ansible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ansible/
|
||||||
|
ansible-playbook -i inventory.yml playbooks/site.yml --check # dry-run
|
||||||
|
ansible-playbook -i inventory.yml playbooks/site.yml # apply
|
||||||
|
ansible-playbook -i inventory.yml playbooks/site.yml --tags rocm # un rôle
|
||||||
|
```
|
||||||
|
|
||||||
|
Rôles prévus : `common`, `gateway`, `dnsmasq`, `nfs_server`, `postgresql`, `qdrant`, `minio`, `rocm`, `lm_studio`, `hermes_agent`. Tous idempotents. Secrets via Ansible Vault.
|
||||||
|
|
||||||
|
### talhelper (nœuds Talos)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd talos/
|
||||||
|
talhelper genconfig # régénérer configs nœuds
|
||||||
|
talhelper gencommand apply --node compute-02 | bash # appliquer sur un nœud
|
||||||
|
talhelper gencommand bootstrap --node compute-01 | bash # bootstrap (1ère fois seulement)
|
||||||
|
talhelper gencommand kubeconfig | bash # récupérer kubeconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
`talos/clusterconfig/` est généré — ne pas committer (mettre au `.gitignore`).
|
||||||
|
|
||||||
|
### ArgoCD (workloads k8s)
|
||||||
|
|
||||||
|
Pattern "App of Apps" : modifier un manifest → `git commit` + `git push` → ArgoCD applique automatiquement (poll toutes les ~3 min).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vim k8s/apps/open-webui/deployment.yaml
|
||||||
|
git add k8s/apps/open-webui/
|
||||||
|
git commit -m "open-webui: bump to v0.4.0"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Réseaux internes Kubernetes
|
||||||
|
|
||||||
|
- CNI (Flannel) — Pods : `10.42.0.0/16`
|
||||||
|
- Services : `10.43.0.0/16`
|
||||||
|
- MetalLB pool (IP virtuelles Traefik/services) : `192.168.10.200-230`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contrainte GPU critique
|
||||||
|
|
||||||
|
La RX 6700XT (gfx1031) n'est **pas officiellement supportée** par ROCm. Sur gpu-01, toujours s'assurer que la variable est présente dans l'environnement LM Studio / le service systemd :
|
||||||
|
|
||||||
|
```
|
||||||
|
HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
ROCm tourne directement sur l'hôte AlmaLinux (pas dans des conteneurs). Fallback : Vulkan dans LM Studio si ROCm coince.
|
||||||
|
|
||||||
|
LM Studio bind sur `127.0.0.1` par défaut → pour l'exposer au réseau :
|
||||||
|
```bash
|
||||||
|
lms server start --port 1234 --bind 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
CLI LM Studio utile : `lms ls`, `lms load <model>`, `lms unload`, `lms server status`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deux setups IA
|
||||||
|
|
||||||
|
### Setup 1 — Poste perso (9950X3D) — dev local
|
||||||
|
|
||||||
|
| Composant | Endpoint |
|
||||||
|
|---|---|
|
||||||
|
| LM Studio | `http://127.0.0.1:1234/v1` |
|
||||||
|
| Ollama | `http://127.0.0.1:11434` |
|
||||||
|
| Hermes Agent | `base_url: http://127.0.0.1:1234/v1` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prérequis : modèle chargé avec context suffisant (system prompt Hermes ≈ 15 000 tokens)
|
||||||
|
lms load <modele> --context-length 16384
|
||||||
|
lms server start --port 1234
|
||||||
|
```
|
||||||
|
|
||||||
|
Agents Goose dans `Funk/Funk-infra/agents/` — pointent tous sur `http://localhost:1234/v1` :
|
||||||
|
```bash
|
||||||
|
cd Funk/Funk-infra/agents/<nom-agent>
|
||||||
|
goose session --config goose-config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup 2 — Cluster Funk (production)
|
||||||
|
|
||||||
|
| Composant | Endpoint |
|
||||||
|
|---|---|
|
||||||
|
| LM Studio (gpu-01) | `http://192.168.10.20:1234/v1` |
|
||||||
|
| Hermes Agent | service systemd sur storage-01, pointe sur gpu-01 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sur gpu-01 — HSA_OVERRIDE requis pour RX 6700XT (gfx1031)
|
||||||
|
HSA_OVERRIDE_GFX_VERSION=10.3.0 lms load <modele> --context-length 16384
|
||||||
|
lms server start --port 1234 --bind 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Détail complet des deux setups : `Funk/Funk-infra/HOMELAB.md` § 7.4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Domaine** : `lab.local` — wildcard DNS via dnsmasq sur storage-01
|
||||||
|
- **Ingress** : `<service>.lab.local` → IP MetalLB de Traefik (`192.168.10.200-230`)
|
||||||
|
- **LM Studio endpoint** : `http://192.168.10.20:1234/v1`
|
||||||
|
- **Namespaces k8s** : `argocd`, `infra`, `storage`, `external`, `ai`, `workflows`
|
||||||
|
- **Git** : branches `main` (prod) / `feature/*` — commits en conventional commits (`feat:`, `fix:`, `chore:`, `docs:`)
|
||||||
|
- **Secrets** : Ansible Vault (Ansible), SOPS+age (Talos via talhelper), Sealed Secrets (k8s)
|
||||||
|
|
||||||
|
## Opérations cluster importantes
|
||||||
|
|
||||||
|
**etcd backup** (single control-plane → critique) :
|
||||||
|
```bash
|
||||||
|
talosctl etcd snapshot /path/to/backup.db --nodes compute-01
|
||||||
|
```
|
||||||
|
À planifier en cron Ansible vers le RAID5 de storage-01.
|
||||||
|
|
||||||
|
**Hermes Agent** — données persistantes dans `~/.hermes/` → doit résider sur le RAID5, pas sur le SSD OS.
|
||||||
|
|
||||||
|
**Ce qui reste toujours manuel** (GitOps ne peut pas bootstrapper lui-même) :
|
||||||
|
- Installation OS depuis USB (AlmaLinux ou Talos)
|
||||||
|
- Configuration IP/SSH minimale pour le premier Ansible
|
||||||
|
- Premier déploiement ArgoCD via Helm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RAM serrée sur compute-02/03 (8 GB)
|
||||||
|
|
||||||
|
Toujours définir `resources.requests` et `resources.limits` sur les pods déployés sur ces nœuds — sans quoi l'OOM-killer peut tuer kubelet.
|
||||||
|
|
||||||
|
Réservations à configurer dans talhelper :
|
||||||
|
```
|
||||||
|
system-reserved: cpu=500m,memory=1Gi
|
||||||
|
kube-reserved: cpu=500m,memory=1Gi
|
||||||
|
```
|
||||||
35
Makefile
Normal file
35
Makefile
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
VENV := .venv
|
||||||
|
ANSIBLE := $(VENV)/bin/ansible
|
||||||
|
PLAYBOOK := $(VENV)/bin/ansible-playbook
|
||||||
|
GALAXY := $(VENV)/bin/ansible-galaxy
|
||||||
|
INV := ansible/inventory.yml
|
||||||
|
|
||||||
|
.PHONY: install ping check apply apply-storage apply-gpu vault-init
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(VENV)/bin/pip install -q -r requirements.txt
|
||||||
|
$(GALAXY) collection install -r ansible/requirements.yml
|
||||||
|
|
||||||
|
ping:
|
||||||
|
cd ansible && ../$(ANSIBLE) all -m ping
|
||||||
|
|
||||||
|
check:
|
||||||
|
cd ansible && ../$(PLAYBOOK) playbooks/site.yml --check
|
||||||
|
|
||||||
|
apply:
|
||||||
|
cd ansible && ../$(PLAYBOOK) playbooks/site.yml
|
||||||
|
|
||||||
|
apply-storage:
|
||||||
|
cd ansible && ../$(PLAYBOOK) playbooks/storage-01.yml
|
||||||
|
|
||||||
|
apply-gpu:
|
||||||
|
cd ansible && ../$(PLAYBOOK) playbooks/gpu-01.yml
|
||||||
|
|
||||||
|
vault-encrypt:
|
||||||
|
cd ansible && ../$(VENV)/bin/ansible-vault encrypt group_vars/all/vault.yml
|
||||||
|
|
||||||
|
vault-edit:
|
||||||
|
cd ansible && ../$(VENV)/bin/ansible-vault edit group_vars/all/vault.yml
|
||||||
|
|
||||||
|
vault-view:
|
||||||
|
cd ansible && ../$(VENV)/bin/ansible-vault view group_vars/all/vault.yml
|
||||||
8
PROGRESS.md
Normal file
8
PROGRESS.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Funk — Journal de progression
|
||||||
|
|
||||||
|
| Date | Résumé |
|
||||||
|
|---|---|
|
||||||
|
| [2026-05-09](progress/2026-05-09.md) | Init repo, rôles common + gateway, storage-01 et gpu-01 opérationnels |
|
||||||
|
| [2026-05-10](progress/2026-05-10.md) | dnsmasq, ROCm, LM Studio (GLIBCXX fix), NFS + RAID5, modèles partagés sur NFS |
|
||||||
|
| [2026-05-10b](progress/2026-05-10b.md) | Rôle hermes_agent — installation, config, service systemd sur storage-01 |
|
||||||
|
| [2026-05-11](progress/2026-05-11.md) | Hermes opérationnel (matin) — llama-server ROCm 7.x ×4 perf (après-midi) — LiteLLM proxy + Claude API, switch modèle sans restart Hermes (soir) |
|
||||||
15
ansible/ansible.cfg
Normal file
15
ansible/ansible.cfg
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[defaults]
|
||||||
|
inventory = inventory.yml
|
||||||
|
roles_path = roles
|
||||||
|
remote_user = ansible
|
||||||
|
private_key_file = ~/.ssh/id_ed25519
|
||||||
|
host_key_checking = False
|
||||||
|
retry_files_enabled = False
|
||||||
|
stdout_callback = ansible.builtin.default
|
||||||
|
result_format = yaml
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
vault_password_file = ../.vault_pass
|
||||||
|
|
||||||
|
[privilege_escalation]
|
||||||
|
become = True
|
||||||
|
become_method = sudo
|
||||||
8
ansible/group_vars/all/vars.yml
Normal file
8
ansible/group_vars/all/vars.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
30
ansible/group_vars/all/vault.yml
Normal file
30
ansible/group_vars/all/vault.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
33306234383266653631633563663630353937323132323336356161626533373832313530393064
|
||||||
|
3130383235333530336531636665623737663261316332370a313739353336306130643461653635
|
||||||
|
35353961333236636263613761393935373762336633396337346661373062636131373632366339
|
||||||
|
6562383163316162390a393865383736623132653531643064663338393164366166303532633362
|
||||||
|
38343131656238653562323632303362303566363635316132383335326462316432336330366230
|
||||||
|
63663732356366356234646664646539316531613131313932313237663365643230343063613763
|
||||||
|
63633061663833353337626433666363363336646136376334303166613764323634323463393464
|
||||||
|
37393533333437386666653663323331666130656161646437663734363161653534666661383339
|
||||||
|
38623934643636643361643766356664393335353034373532313262666537373330353762336361
|
||||||
|
64343465333534313239323432656135626237333434653034333364663061383965653838663763
|
||||||
|
63653832313934326163623831653839613330346238643630663539623436373738323837386162
|
||||||
|
66326466623764346666613266663530363664343037643766653562613034363239346639326664
|
||||||
|
30346337663530623732333264383839396534383762623338313437633266656665366538633564
|
||||||
|
63363633343731636234623330393838313237633064333939663236616365326130323735623339
|
||||||
|
34386635623033636166633536633661643133653263646333326363613362303461376432373466
|
||||||
|
36613538393334626266396531616366383931636665633862373331343365623962643362306133
|
||||||
|
66363730626639336232626430636239643261666661396664396661393635306466363338306662
|
||||||
|
35343432346461306330623536363330653262323863343162623965633164653035373432316232
|
||||||
|
36303630306465393837373033656130636261316535323464613563656235643136663038366536
|
||||||
|
36343062623932346161616134653562346666333266393563623938376535323637376265353164
|
||||||
|
65346663373331393561653030356163633038376234663231336332653161636662323537306633
|
||||||
|
38636661613535336463616662363033313961613434323033316230653865356233636138343336
|
||||||
|
35323438656564613931656538646561663432363234653134323933626236646237326636353833
|
||||||
|
36383838666263626632613462646561393035663435373736653861383063623862613131653332
|
||||||
|
66393731623630376463623533336133353036386234633361653237626335323432643332313036
|
||||||
|
31363032323763656530313230346466343537663664623931623933616166333135326437666231
|
||||||
|
30363135643632633962613631656339616665356463653162653832366336663165646264636638
|
||||||
|
66663866303733633037313131353033393263346138643139626539326431383762646565613437
|
||||||
|
32333462353765653931613431366639323434356365376636646237303166323762
|
||||||
5
ansible/group_vars/gateway/vars.yml
Normal file
5
ansible/group_vars/gateway/vars.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
wan_interface: enp4s0 # NIC1 — port 2 switch — VLAN 1 (192.168.1.200)
|
||||||
|
lan_interface: enp6s0f3u2c2 # NIC2 — port 7 switch — VLAN 10 (192.168.10.1)
|
||||||
|
wan_ip: 192.168.1.200
|
||||||
|
lan_ip: 192.168.10.1
|
||||||
2
ansible/group_vars/gpu_hosts/vars.yml
Normal file
2
ansible/group_vars/gpu_hosts/vars.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
hsa_override_gfx_version: "10.3.0"
|
||||||
14
ansible/host_vars/gpu-01/vars.yml
Normal file
14
ansible/host_vars/gpu-01/vars.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
ansible_host: 192.168.10.20
|
||||||
|
|
||||||
|
# llama-server — modèle servi au démarrage
|
||||||
|
llama_model_path: "/mnt/models/bartowski/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf"
|
||||||
|
llama_model_alias: "qwen3-8b"
|
||||||
|
llama_ctx_size: 32768
|
||||||
|
|
||||||
|
# Montages NFS (surcharge les defaults du rôle nfs_client)
|
||||||
|
nfs_client_mounts:
|
||||||
|
- local_path: /mnt/nfs
|
||||||
|
remote_export: /srv/data/nfs/k8s
|
||||||
|
- local_path: /mnt/models
|
||||||
|
remote_export: /srv/data/models
|
||||||
21
ansible/host_vars/storage-01/vars.yml
Normal file
21
ansible/host_vars/storage-01/vars.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
ansible_host: 192.168.1.200
|
||||||
|
|
||||||
|
# RAID5
|
||||||
|
raid_uuid: "3add8360-fa01-47eb-8aa1-0e84bacbbc15"
|
||||||
|
raid_mount: /srv/data
|
||||||
|
|
||||||
|
# NFS exports
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Chemins services (sous le RAID)
|
||||||
|
hermes_data_dir: /srv/data/hermes
|
||||||
|
postgres_data_dir: /srv/data/postgres
|
||||||
|
qdrant_data_dir: /srv/data/qdrant
|
||||||
|
minio_data_dir: /srv/data/minio
|
||||||
18
ansible/inventory.yml
Normal file
18
ansible/inventory.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
all:
|
||||||
|
vars:
|
||||||
|
ansible_user: ansible
|
||||||
|
|
||||||
|
children:
|
||||||
|
gateway:
|
||||||
|
hosts:
|
||||||
|
storage-01:
|
||||||
|
ansible_host: 192.168.1.200
|
||||||
|
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'
|
||||||
|
almalinux:
|
||||||
|
children:
|
||||||
|
gateway:
|
||||||
|
gpu_hosts:
|
||||||
8
ansible/playbooks/gpu-01.yml
Normal file
8
ansible/playbooks/gpu-01.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
- name: Configure gpu-01
|
||||||
|
hosts: gpu_hosts
|
||||||
|
roles:
|
||||||
|
- { role: common, tags: [common] }
|
||||||
|
- { role: rocm, tags: [rocm] }
|
||||||
|
- { role: nfs_client, tags: [nfs_client] }
|
||||||
|
- { role: llama_server, tags: [llama_server] }
|
||||||
3
ansible/playbooks/site.yml
Normal file
3
ansible/playbooks/site.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
- import_playbook: storage-01.yml
|
||||||
|
- import_playbook: gpu-01.yml
|
||||||
13
ansible/playbooks/storage-01.yml
Normal file
13
ansible/playbooks/storage-01.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
- name: Configure storage-01
|
||||||
|
hosts: gateway
|
||||||
|
roles:
|
||||||
|
- { role: common, tags: [common] }
|
||||||
|
- { role: gateway, tags: [gateway] }
|
||||||
|
- { role: dnsmasq, tags: [dnsmasq] }
|
||||||
|
- { role: nfs_server, tags: [nfs_server] }
|
||||||
|
- { role: qdrant, tags: [qdrant] }
|
||||||
|
- { role: postgresql, tags: [postgresql] }
|
||||||
|
- { role: minio, tags: [minio] }
|
||||||
|
- { role: litellm, tags: [litellm] }
|
||||||
|
- { role: hermes_agent, tags: [hermes_agent] }
|
||||||
4
ansible/requirements.yml
Normal file
4
ansible/requirements.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
collections:
|
||||||
|
- name: ansible.posix
|
||||||
|
- name: community.general
|
||||||
|
- name: community.postgresql
|
||||||
19
ansible/roles/common/defaults/main.yml
Normal file
19
ansible/roles/common/defaults/main.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
timezone: Europe/Paris
|
||||||
|
|
||||||
|
base_packages:
|
||||||
|
- vim
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- jq
|
||||||
|
- git
|
||||||
|
- htop
|
||||||
|
- chrony
|
||||||
|
- sudo
|
||||||
|
- bash-completion
|
||||||
|
- tar
|
||||||
|
- unzip
|
||||||
|
- net-tools
|
||||||
|
- bind-utils
|
||||||
|
|
||||||
|
ansible_service_user: ansible
|
||||||
9
ansible/roles/common/handlers/main.yml
Normal file
9
ansible/roles/common/handlers/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Restart sshd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Reboot if kernel updated
|
||||||
|
ansible.builtin.reboot:
|
||||||
|
reboot_timeout: 300
|
||||||
71
ansible/roles/common/tasks/main.yml
Normal file
71
ansible/roles/common/tasks/main.yml
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
- name: Set hostname
|
||||||
|
ansible.builtin.hostname:
|
||||||
|
name: "{{ inventory_hostname }}"
|
||||||
|
|
||||||
|
- name: Set timezone
|
||||||
|
community.general.timezone:
|
||||||
|
name: "{{ timezone }}"
|
||||||
|
|
||||||
|
- name: Enable EPEL repository
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: epel-release
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Upgrade all packages
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "*"
|
||||||
|
state: latest
|
||||||
|
update_cache: true
|
||||||
|
tags: update
|
||||||
|
notify: Reboot if kernel updated
|
||||||
|
|
||||||
|
- name: Install base packages
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "{{ base_packages }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Enable and start chrony
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: chronyd
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Create ansible group
|
||||||
|
ansible.builtin.group:
|
||||||
|
name: "{{ ansible_service_user }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create ansible user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ ansible_service_user }}"
|
||||||
|
group: "{{ ansible_service_user }}"
|
||||||
|
groups: wheel
|
||||||
|
shell: /bin/bash
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Allow ansible user passwordless sudo
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/sudoers.d/ansible
|
||||||
|
content: "{{ ansible_service_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
||||||
|
mode: '0440'
|
||||||
|
validate: /usr/sbin/visudo -cf %s
|
||||||
|
|
||||||
|
- name: Set up SSH authorized key for ansible user
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: "{{ ansible_service_user }}"
|
||||||
|
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Harden sshd
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: "{{ item.regexp }}"
|
||||||
|
line: "{{ item.line }}"
|
||||||
|
state: present
|
||||||
|
loop:
|
||||||
|
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
|
||||||
|
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
|
||||||
|
- { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
|
||||||
|
notify: Restart sshd
|
||||||
16
ansible/roles/dnsmasq/defaults/main.yml
Normal file
16
ansible/roles/dnsmasq/defaults/main.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
dns_upstream:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
|
||||||
|
dns_cluster_hosts:
|
||||||
|
- name: storage-01
|
||||||
|
ip: 192.168.10.1
|
||||||
|
- name: gpu-01
|
||||||
|
ip: 192.168.10.20
|
||||||
|
- name: compute-01
|
||||||
|
ip: 192.168.10.11
|
||||||
|
- name: compute-02
|
||||||
|
ip: 192.168.10.12
|
||||||
|
- name: compute-03
|
||||||
|
ip: 192.168.10.13
|
||||||
9
ansible/roles/dnsmasq/handlers/main.yml
Normal file
9
ansible/roles/dnsmasq/handlers/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Restart dnsmasq
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: dnsmasq
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
29
ansible/roles/dnsmasq/tasks/main.yml
Normal file
29
ansible/roles/dnsmasq/tasks/main.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
- name: Install dnsmasq
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: dnsmasq
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Deploy dnsmasq config
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: dnsmasq.conf.j2
|
||||||
|
dest: /etc/dnsmasq.conf
|
||||||
|
mode: '0644'
|
||||||
|
notify: Restart dnsmasq
|
||||||
|
|
||||||
|
- name: Deploy dnsmasq systemd override (wait for USB ethernet)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/systemd/system/dnsmasq.service.d/wait-for-network.conf
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
mode: '0644'
|
||||||
|
notify: Reload systemd
|
||||||
|
|
||||||
|
- name: Enable and start dnsmasq
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: dnsmasq
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
31
ansible/roles/dnsmasq/templates/dnsmasq.conf.j2
Normal file
31
ansible/roles/dnsmasq/templates/dnsmasq.conf.j2
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Géré par Ansible — ne pas modifier manuellement
|
||||||
|
|
||||||
|
# Écoute uniquement sur le LAN cluster
|
||||||
|
interface={{ lan_interface }}
|
||||||
|
bind-interfaces
|
||||||
|
|
||||||
|
# Domaine local
|
||||||
|
domain={{ dns_domain }}
|
||||||
|
local=/{{ dns_domain }}/
|
||||||
|
expand-hosts
|
||||||
|
|
||||||
|
# Ne pas lire /etc/resolv.conf pour les upstream
|
||||||
|
no-resolv
|
||||||
|
|
||||||
|
# Upstream DNS pour tout ce qui n'est pas lab.local
|
||||||
|
{% for server in dns_upstream %}
|
||||||
|
server={{ server }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
# Enregistrements statiques des machines du cluster
|
||||||
|
{% for host in dns_cluster_hosts %}
|
||||||
|
address=/{{ host.name }}.{{ dns_domain }}/{{ host.ip }}
|
||||||
|
address=/{{ host.name }}/{{ host.ip }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache-size=1000
|
||||||
|
|
||||||
|
# Logs (désactiver en prod si trop verbeux)
|
||||||
|
log-queries
|
||||||
|
log-facility=/var/log/dnsmasq.log
|
||||||
5
ansible/roles/gateway/handlers/main.yml
Normal file
5
ansible/roles/gateway/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: Reload nftables
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: nftables
|
||||||
|
state: reloaded
|
||||||
32
ansible/roles/gateway/tasks/main.yml
Normal file
32
ansible/roles/gateway/tasks/main.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
- name: Stop and disable firewalld
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: firewalld
|
||||||
|
enabled: false
|
||||||
|
state: stopped
|
||||||
|
|
||||||
|
- name: Enable IP forwarding
|
||||||
|
ansible.posix.sysctl:
|
||||||
|
name: net.ipv4.ip_forward
|
||||||
|
value: '1'
|
||||||
|
sysctl_set: true
|
||||||
|
state: present
|
||||||
|
reload: true
|
||||||
|
|
||||||
|
- name: Install nftables
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: nftables
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Deploy nftables config
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: nftables.conf.j2
|
||||||
|
dest: /etc/sysconfig/nftables.conf
|
||||||
|
mode: '0640'
|
||||||
|
notify: Reload nftables
|
||||||
|
|
||||||
|
- name: Enable and start nftables
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: nftables
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
63
ansible/roles/gateway/templates/nftables.conf.j2
Normal file
63
ansible/roles/gateway/templates/nftables.conf.j2
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
# Géré par Ansible — ne pas modifier manuellement
|
||||||
|
|
||||||
|
flush ruleset
|
||||||
|
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0; policy drop;
|
||||||
|
|
||||||
|
# Loopback
|
||||||
|
iif lo accept
|
||||||
|
|
||||||
|
# Connexions établies
|
||||||
|
ct state established,related accept
|
||||||
|
|
||||||
|
# ICMP
|
||||||
|
ip protocol icmp accept
|
||||||
|
ip6 nexthdr icmpv6 accept
|
||||||
|
|
||||||
|
# SSH depuis le LAN domestique et le LAN cluster
|
||||||
|
tcp dport 22 ip saddr { 192.168.1.0/24, 192.168.10.0/24 } accept
|
||||||
|
|
||||||
|
# DNS (dnsmasq)
|
||||||
|
udp dport 53 ip saddr 192.168.10.0/24 accept
|
||||||
|
tcp dport 53 ip saddr 192.168.10.0/24 accept
|
||||||
|
|
||||||
|
# NFS
|
||||||
|
tcp dport { 111, 2049 } ip saddr 192.168.10.0/24 accept
|
||||||
|
udp dport { 111, 2049 } ip saddr 192.168.10.0/24 accept
|
||||||
|
|
||||||
|
# Services données (accès cluster uniquement)
|
||||||
|
tcp dport { 5432, 6333, 6334, 9000, 9001 } ip saddr 192.168.10.0/24 accept
|
||||||
|
|
||||||
|
# Hermes gateway (accès LAN domestique + cluster)
|
||||||
|
tcp dport 8080 ip saddr { 192.168.1.0/24, 192.168.10.0/24 } accept
|
||||||
|
|
||||||
|
# LiteLLM (localhost uniquement — pas d'accès réseau direct)
|
||||||
|
# Port 4000 non exposé ici intentionnellement
|
||||||
|
|
||||||
|
log prefix "nft-drop: " drop
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority 0; policy drop;
|
||||||
|
|
||||||
|
# Forwarding LAN cluster → WAN
|
||||||
|
iif {{ lan_interface }} oif {{ wan_interface }} ct state new,established,related accept
|
||||||
|
iif {{ wan_interface }} oif {{ lan_interface }} ct state established,related accept
|
||||||
|
}
|
||||||
|
|
||||||
|
chain output {
|
||||||
|
type filter hook output priority 0; policy accept;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table ip nat {
|
||||||
|
chain postrouting {
|
||||||
|
type nat hook postrouting priority 100;
|
||||||
|
|
||||||
|
# NAT masquerade pour le LAN cluster
|
||||||
|
oif {{ wan_interface }} ip saddr 192.168.10.0/24 masquerade
|
||||||
|
}
|
||||||
|
}
|
||||||
12
ansible/roles/hermes_agent/defaults/main.yml
Normal file
12
ansible/roles/hermes_agent/defaults/main.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
hermes_user: hermes
|
||||||
|
hermes_home: /opt/hermes
|
||||||
|
hermes_data_dir: /srv/data/hermes
|
||||||
|
|
||||||
|
hermes_lm_base_url: "http://127.0.0.1:4000/v1"
|
||||||
|
hermes_lm_api_key: "lm-studio"
|
||||||
|
hermes_model: "hermes-default"
|
||||||
|
hermes_context_length: 65536
|
||||||
|
|
||||||
|
hermes_gateway_port: 8080
|
||||||
|
hermes_gateway_host: "0.0.0.0"
|
||||||
9
ansible/roles/hermes_agent/handlers/main.yml
Normal file
9
ansible/roles/hermes_agent/handlers/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Restart hermes-agent
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: hermes-agent
|
||||||
|
state: restarted
|
||||||
157
ansible/roles/hermes_agent/tasks/main.yml
Normal file
157
ansible/roles/hermes_agent/tasks/main.yml
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
---
|
||||||
|
# --- Dépendances système ---
|
||||||
|
|
||||||
|
- name: Install Hermes Agent dependencies
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- curl
|
||||||
|
- git
|
||||||
|
- tar
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# --- Utilisateur système ---
|
||||||
|
|
||||||
|
- name: Create hermes system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ hermes_user }}"
|
||||||
|
home: "{{ hermes_home }}"
|
||||||
|
shell: /bin/bash
|
||||||
|
system: false
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# --- Environnement shell utilisateur ---
|
||||||
|
|
||||||
|
- name: Set HERMES_HOME in hermes user bashrc
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ hermes_home }}/.bashrc"
|
||||||
|
line: "export HERMES_HOME={{ hermes_data_dir }}"
|
||||||
|
create: true
|
||||||
|
owner: "{{ hermes_user }}"
|
||||||
|
group: "{{ hermes_user }}"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Add hermes venv to PATH in bashrc
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ hermes_home }}/.bashrc"
|
||||||
|
line: "export PATH={{ hermes_data_dir }}/hermes-agent/venv/bin:$PATH"
|
||||||
|
create: true
|
||||||
|
owner: "{{ hermes_user }}"
|
||||||
|
group: "{{ hermes_user }}"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
# --- Répertoire données (RAID5) ---
|
||||||
|
|
||||||
|
- name: Create HERMES_HOME on RAID5
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ hermes_data_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ hermes_user }}"
|
||||||
|
group: "{{ hermes_user }}"
|
||||||
|
mode: '0750'
|
||||||
|
|
||||||
|
# --- Installation ---
|
||||||
|
|
||||||
|
- name: Check if hermes binary is already installed
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ hermes_home }}/.local/bin/hermes"
|
||||||
|
register: hermes_binary
|
||||||
|
|
||||||
|
- name: Install Hermes Agent via installer script
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
||||||
|
executable: /bin/bash
|
||||||
|
become: true
|
||||||
|
become_user: "{{ hermes_user }}"
|
||||||
|
environment:
|
||||||
|
HOME: "{{ hermes_home }}"
|
||||||
|
HERMES_HOME: "{{ hermes_data_dir }}"
|
||||||
|
when: not hermes_binary.stat.exists
|
||||||
|
register: hermes_install_result
|
||||||
|
|
||||||
|
- name: Show installer output
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: hermes_install_result.stdout_lines
|
||||||
|
when: hermes_install_result is changed
|
||||||
|
|
||||||
|
# --- Packages Python pour les skills ---
|
||||||
|
|
||||||
|
- name: Install Python packages for Hermes skills
|
||||||
|
ansible.builtin.pip:
|
||||||
|
name: "qdrant-client>=1.12.0"
|
||||||
|
executable: "{{ hermes_data_dir }}/hermes-agent/venv/bin/pip"
|
||||||
|
state: present
|
||||||
|
become: true
|
||||||
|
become_user: "{{ hermes_user }}"
|
||||||
|
when: hermes_binary.stat.exists
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
|
||||||
|
- name: Ensure LM_BASE_URL in .env points to LiteLLM
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ hermes_data_dir }}/.env"
|
||||||
|
regexp: '^LM_BASE_URL='
|
||||||
|
line: "LM_BASE_URL={{ hermes_lm_base_url }}"
|
||||||
|
create: false
|
||||||
|
notify: Restart hermes-agent
|
||||||
|
when: hermes_binary.stat.exists
|
||||||
|
|
||||||
|
- name: Ensure LM_API_KEY in .env is correct
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ hermes_data_dir }}/.env"
|
||||||
|
regexp: '^LM_API_KEY='
|
||||||
|
line: "LM_API_KEY={{ hermes_lm_api_key }}"
|
||||||
|
create: false
|
||||||
|
notify: Restart hermes-agent
|
||||||
|
when: hermes_binary.stat.exists
|
||||||
|
|
||||||
|
- name: Deploy Hermes config.yaml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: config.yaml.j2
|
||||||
|
dest: "{{ hermes_data_dir }}/config.yaml"
|
||||||
|
owner: "{{ hermes_user }}"
|
||||||
|
group: "{{ hermes_user }}"
|
||||||
|
mode: '0640'
|
||||||
|
notify: Restart hermes-agent
|
||||||
|
|
||||||
|
# --- Service systemd ---
|
||||||
|
|
||||||
|
- name: Deploy hermes-agent systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: hermes-agent.service.j2
|
||||||
|
dest: /etc/systemd/system/hermes-agent.service
|
||||||
|
mode: '0644'
|
||||||
|
notify:
|
||||||
|
- Reload systemd
|
||||||
|
- Restart hermes-agent
|
||||||
|
|
||||||
|
- name: Enable and start hermes-agent
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: hermes-agent
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# --- Wrapper global ---
|
||||||
|
|
||||||
|
- name: Deploy hermes global wrapper (/usr/local/bin/hermes)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /usr/local/bin/hermes
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
exec sudo -u {{ hermes_user }} \
|
||||||
|
HOME={{ hermes_home }} \
|
||||||
|
HERMES_HOME={{ hermes_data_dir }} \
|
||||||
|
{{ hermes_data_dir }}/hermes-agent/venv/bin/hermes "$@"
|
||||||
|
|
||||||
|
# --- Firewall ---
|
||||||
|
|
||||||
|
- name: Open Hermes gateway port in firewall
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ hermes_gateway_port }}/tcp"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
|
when: ansible_facts.services['firewalld.service'] is defined
|
||||||
|
and ansible_facts.services['firewalld.service']['state'] == 'running'
|
||||||
13
ansible/roles/hermes_agent/templates/config.yaml.j2
Normal file
13
ansible/roles/hermes_agent/templates/config.yaml.j2
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
model:
|
||||||
|
provider: lmstudio
|
||||||
|
base_url: "{{ hermes_lm_base_url }}"
|
||||||
|
default: "{{ hermes_model }}"
|
||||||
|
context_length: {{ hermes_context_length }}
|
||||||
|
|
||||||
|
auxiliary:
|
||||||
|
compression:
|
||||||
|
context_length: {{ hermes_context_length }}
|
||||||
|
|
||||||
|
gateway:
|
||||||
|
host: "{{ hermes_gateway_host }}"
|
||||||
|
port: {{ hermes_gateway_port }}
|
||||||
26
ansible/roles/hermes_agent/templates/hermes-agent.service.j2
Normal file
26
ansible/roles/hermes_agent/templates/hermes-agent.service.j2
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Hermes Agent Gateway
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User={{ hermes_user }}
|
||||||
|
Group={{ hermes_user }}
|
||||||
|
|
||||||
|
Environment=HOME={{ hermes_home }}
|
||||||
|
Environment=HERMES_HOME={{ hermes_data_dir }}
|
||||||
|
Environment=LM_BASE_URL={{ hermes_lm_base_url }}
|
||||||
|
Environment=LM_API_KEY={{ hermes_lm_api_key }}
|
||||||
|
Environment=PATH={{ hermes_data_dir }}/hermes-agent/venv/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
ExecStart={{ hermes_home }}/.local/bin/hermes gateway run
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=hermes-agent
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
15
ansible/roles/litellm/defaults/main.yml
Normal file
15
ansible/roles/litellm/defaults/main.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
litellm_user: litellm
|
||||||
|
litellm_venv: /opt/litellm/venv
|
||||||
|
litellm_config_dir: /etc/litellm
|
||||||
|
|
||||||
|
litellm_host: "127.0.0.1" # loopback — Hermes est sur la même machine
|
||||||
|
litellm_port: 4000
|
||||||
|
litellm_master_key: "lm-studio"
|
||||||
|
|
||||||
|
# Clé Anthropic — vient du vault
|
||||||
|
litellm_anthropic_api_key: "{{ vault_anthropic_api_key }}"
|
||||||
|
|
||||||
|
# Endpoint llama-server sur gpu-01
|
||||||
|
litellm_local_llm_base_url: "http://192.168.10.20:1234/v1"
|
||||||
|
litellm_local_llm_api_key: "lm-studio"
|
||||||
76
ansible/roles/litellm/files/hermes-switch
Normal file
76
ansible/roles/litellm/files/hermes-switch
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Switch le modèle de Hermes via LiteLLM (hermes-default alias)
|
||||||
|
# Usage: hermes-switch [qwen|claude|status]
|
||||||
|
|
||||||
|
CONFIG=/etc/litellm/config.yaml
|
||||||
|
|
||||||
|
qwen_block=' litellm_params:\n model: openai/qwen2.5-7b-instruct\n api_base: http://192.168.10.20:1234/v1\n api_key: lm-studio'
|
||||||
|
claude_block=' litellm_params:\n model: anthropic/claude-sonnet-4-6\n api_key: os.environ/ANTHROPIC_API_KEY'
|
||||||
|
|
||||||
|
current() {
|
||||||
|
grep -A3 "model_name: hermes-default" "$CONFIG" | grep "model:" | awk '{print $2}'
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
local cur=$(current)
|
||||||
|
echo "hermes-default → $cur"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_qwen() {
|
||||||
|
python3 - "$CONFIG" <<'EOF'
|
||||||
|
import sys, re
|
||||||
|
path = sys.argv[1]
|
||||||
|
text = open(path).read()
|
||||||
|
block = """ - model_name: hermes-default
|
||||||
|
litellm_params:
|
||||||
|
model: openai/qwen3-8b
|
||||||
|
api_base: http://192.168.10.20:1234/v1
|
||||||
|
api_key: lm-studio"""
|
||||||
|
text = re.sub(
|
||||||
|
r' - model_name: hermes-default\n litellm_params:.*?(?=\n -|\nlitellm_settings)',
|
||||||
|
block + '\n',
|
||||||
|
text, flags=re.DOTALL
|
||||||
|
)
|
||||||
|
open(path, 'w').write(text)
|
||||||
|
print("OK")
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_claude() {
|
||||||
|
python3 - "$CONFIG" <<'EOF'
|
||||||
|
import sys, re
|
||||||
|
path = sys.argv[1]
|
||||||
|
text = open(path).read()
|
||||||
|
block = """ - model_name: hermes-default
|
||||||
|
litellm_params:
|
||||||
|
model: anthropic/claude-sonnet-4-6
|
||||||
|
api_key: os.environ/ANTHROPIC_API_KEY"""
|
||||||
|
text = re.sub(
|
||||||
|
r' - model_name: hermes-default\n litellm_params:.*?(?=\n -|\nlitellm_settings)',
|
||||||
|
block + '\n',
|
||||||
|
text, flags=re.DOTALL
|
||||||
|
)
|
||||||
|
open(path, 'w').write(text)
|
||||||
|
print("OK")
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
qwen)
|
||||||
|
switch_qwen
|
||||||
|
systemctl restart litellm
|
||||||
|
echo "Hermes → Qwen3-8B (gpu-01)"
|
||||||
|
;;
|
||||||
|
claude)
|
||||||
|
switch_claude
|
||||||
|
systemctl restart litellm
|
||||||
|
echo "Hermes → Claude Sonnet 4.6 (API Anthropic)"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: hermes-switch [qwen|claude|status]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
9
ansible/roles/litellm/handlers/main.yml
Normal file
9
ansible/roles/litellm/handlers/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Restart litellm
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: litellm
|
||||||
|
state: restarted
|
||||||
62
ansible/roles/litellm/tasks/main.yml
Normal file
62
ansible/roles/litellm/tasks/main.yml
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
- name: Install Python pip and devel
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- python3-pip
|
||||||
|
- python3-devel
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create litellm system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ litellm_user }}"
|
||||||
|
system: true
|
||||||
|
shell: /sbin/nologin
|
||||||
|
home: /opt/litellm
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install litellm[proxy] in venv
|
||||||
|
ansible.builtin.pip:
|
||||||
|
name: "litellm[proxy]"
|
||||||
|
virtualenv: "{{ litellm_venv }}"
|
||||||
|
virtualenv_command: python3 -m venv
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create config directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ litellm_config_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ litellm_user }}"
|
||||||
|
group: "{{ litellm_user }}"
|
||||||
|
mode: '0750'
|
||||||
|
|
||||||
|
- name: Deploy LiteLLM config
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: config.yaml.j2
|
||||||
|
dest: "{{ litellm_config_dir }}/config.yaml"
|
||||||
|
owner: "{{ litellm_user }}"
|
||||||
|
group: "{{ litellm_user }}"
|
||||||
|
mode: '0640'
|
||||||
|
notify: Restart litellm
|
||||||
|
|
||||||
|
- name: Deploy systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: litellm.service.j2
|
||||||
|
dest: /etc/systemd/system/litellm.service
|
||||||
|
mode: '0644'
|
||||||
|
notify:
|
||||||
|
- Reload systemd
|
||||||
|
- Restart litellm
|
||||||
|
|
||||||
|
- name: Deploy hermes-switch script
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: hermes-switch
|
||||||
|
dest: /usr/local/bin/hermes-switch
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Enable and start litellm
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: litellm
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
36
ansible/roles/litellm/templates/config.yaml.j2
Normal file
36
ansible/roles/litellm/templates/config.yaml.j2
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
model_list:
|
||||||
|
|
||||||
|
# Alias Hermes — changer uniquement ce bloc pour switcher le modèle de Hermes
|
||||||
|
# sans toucher à la configuration de Hermes lui-même
|
||||||
|
- model_name: hermes-default
|
||||||
|
litellm_params:
|
||||||
|
model: openai/qwen3-8b
|
||||||
|
api_base: {{ litellm_local_llm_base_url }}
|
||||||
|
api_key: {{ litellm_local_llm_api_key }}
|
||||||
|
# Pour passer Hermes sur Claude : remplacer les 3 lignes ci-dessus par :
|
||||||
|
# model: anthropic/claude-sonnet-4-6
|
||||||
|
# api_key: os.environ/ANTHROPIC_API_KEY
|
||||||
|
|
||||||
|
# Modèles accessibles directement par nom
|
||||||
|
- model_name: qwen3-8b
|
||||||
|
litellm_params:
|
||||||
|
model: openai/qwen3-8b
|
||||||
|
api_base: {{ litellm_local_llm_base_url }}
|
||||||
|
api_key: {{ litellm_local_llm_api_key }}
|
||||||
|
|
||||||
|
- model_name: claude-sonnet-4-6
|
||||||
|
litellm_params:
|
||||||
|
model: anthropic/claude-sonnet-4-6
|
||||||
|
api_key: os.environ/ANTHROPIC_API_KEY
|
||||||
|
|
||||||
|
- model_name: claude-opus-4-7
|
||||||
|
litellm_params:
|
||||||
|
model: anthropic/claude-opus-4-7
|
||||||
|
api_key: os.environ/ANTHROPIC_API_KEY
|
||||||
|
|
||||||
|
litellm_settings:
|
||||||
|
drop_params: true
|
||||||
|
request_timeout: 600
|
||||||
|
|
||||||
|
general_settings:
|
||||||
|
master_key: {{ litellm_master_key }}
|
||||||
25
ansible/roles/litellm/templates/litellm.service.j2
Normal file
25
ansible/roles/litellm/templates/litellm.service.j2
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[Unit]
|
||||||
|
Description=LiteLLM Proxy — unified LLM gateway (Qwen local + Claude API)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User={{ litellm_user }}
|
||||||
|
Group={{ litellm_user }}
|
||||||
|
|
||||||
|
Environment=ANTHROPIC_API_KEY={{ litellm_anthropic_api_key }}
|
||||||
|
|
||||||
|
ExecStart={{ litellm_venv }}/bin/litellm \
|
||||||
|
--config {{ litellm_config_dir }}/config.yaml \
|
||||||
|
--host {{ litellm_host }} \
|
||||||
|
--port {{ litellm_port }}
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=litellm
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
15
ansible/roles/llama_server/defaults/main.yml
Normal file
15
ansible/roles/llama_server/defaults/main.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
llama_server_port: 1234
|
||||||
|
llama_server_host: "0.0.0.0"
|
||||||
|
llama_amdgpu_targets: "gfx1030"
|
||||||
|
rocm_path: "/opt/rocm"
|
||||||
|
hsa_override_gfx_version: "10.3.0"
|
||||||
|
|
||||||
|
# Modèle à charger au démarrage
|
||||||
|
llama_model_path: ""
|
||||||
|
llama_model_alias: ""
|
||||||
|
llama_ctx_size: 32768
|
||||||
|
llama_n_gpu_layers: 99
|
||||||
|
llama_parallel: 1
|
||||||
|
llama_embeddings: true
|
||||||
|
llama_pooling: "mean"
|
||||||
9
ansible/roles/llama_server/handlers/main.yml
Normal file
9
ansible/roles/llama_server/handlers/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Restart llama-server
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: llama-server
|
||||||
|
state: restarted
|
||||||
79
ansible/roles/llama_server/tasks/main.yml
Normal file
79
ansible/roles/llama_server/tasks/main.yml
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
---
|
||||||
|
- name: Install build dependencies
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- cmake
|
||||||
|
- gcc-c++
|
||||||
|
- git
|
||||||
|
- patchelf
|
||||||
|
- hipblas-devel
|
||||||
|
- rocblas-devel
|
||||||
|
- hip-devel
|
||||||
|
- hipcc
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Clone llama.cpp
|
||||||
|
ansible.builtin.git:
|
||||||
|
repo: https://github.com/ggerganov/llama.cpp
|
||||||
|
dest: /opt/llama.cpp
|
||||||
|
depth: 1
|
||||||
|
version: "{{ llama_server_commit | default('HEAD') }}"
|
||||||
|
register: llama_clone
|
||||||
|
|
||||||
|
- name: Check if llama-server binary exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /opt/llama.cpp/build/bin/llama-server
|
||||||
|
register: llama_server_binary_stat
|
||||||
|
|
||||||
|
- name: Configure cmake build (ROCm HIP)
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: >
|
||||||
|
cmake -B build
|
||||||
|
-DGGML_HIP=ON
|
||||||
|
-DAMDGPU_TARGETS={{ llama_amdgpu_targets }}
|
||||||
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
-DROCM_PATH={{ rocm_path }}
|
||||||
|
-DCMAKE_PREFIX_PATH={{ rocm_path }}
|
||||||
|
-DCMAKE_HIP_COMPILER={{ rocm_path }}/llvm/bin/clang++
|
||||||
|
chdir: /opt/llama.cpp
|
||||||
|
environment:
|
||||||
|
PATH: "{{ rocm_path }}/bin:{{ ansible_env.PATH }}"
|
||||||
|
when: llama_clone is changed or not llama_server_binary_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Build llama-server
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: cmake --build build --target llama-server -j{{ ansible_processor_nproc }}
|
||||||
|
chdir: /opt/llama.cpp
|
||||||
|
environment:
|
||||||
|
PATH: "{{ rocm_path }}/bin:{{ ansible_env.PATH }}"
|
||||||
|
when: llama_clone is changed or not llama_server_binary_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Deploy systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: llama-server.service.j2
|
||||||
|
dest: /etc/systemd/system/llama-server.service
|
||||||
|
mode: '0644'
|
||||||
|
notify:
|
||||||
|
- Reload systemd
|
||||||
|
- Restart llama-server
|
||||||
|
|
||||||
|
- name: Enable llama-server service
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: llama-server
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Open port in firewall
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ llama_server_port }}/tcp"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
|
|
||||||
|
- name: Disable lm-studio service (replaced by llama-server)
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: lm-studio
|
||||||
|
enabled: false
|
||||||
|
state: stopped
|
||||||
|
ignore_errors: true
|
||||||
28
ansible/roles/llama_server/templates/llama-server.service.j2
Normal file
28
ansible/roles/llama_server/templates/llama-server.service.j2
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[Unit]
|
||||||
|
Description=llama-server (llama.cpp OpenAI-compatible API)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Environment=HSA_OVERRIDE_GFX_VERSION={{ hsa_override_gfx_version }}
|
||||||
|
Environment=LD_LIBRARY_PATH={{ rocm_path }}/lib
|
||||||
|
ExecStart=/opt/llama.cpp/build/bin/llama-server \
|
||||||
|
--model {{ llama_model_path }} \
|
||||||
|
--host {{ llama_server_host }} \
|
||||||
|
--port {{ llama_server_port }} \
|
||||||
|
--ctx-size {{ llama_ctx_size }} \
|
||||||
|
--n-gpu-layers {{ llama_n_gpu_layers }} \
|
||||||
|
--parallel {{ llama_parallel }} \
|
||||||
|
--alias {{ llama_model_alias }} \
|
||||||
|
{% if llama_embeddings %}
|
||||||
|
--embeddings \
|
||||||
|
--pooling {{ llama_pooling }} \
|
||||||
|
{% endif %}
|
||||||
|
--log-disable
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
369
ansible/roles/lm_studio/TROUBLESHOOTING.md
Normal file
369
ansible/roles/lm_studio/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
# LM Studio sur AlmaLinux 9 — Déblocages et solutions
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
- **Machine** : gpu-01, AlmaLinux 9.7, GCC 11, libstdc++ → max `GLIBCXX_3.4.29`
|
||||||
|
- **Problème initial** : LM Studio refuse de démarrer avec `GLIBCXX_3.4.30 not found`
|
||||||
|
- **Cause racine** : `watcher.node` (module Node natif livré avec LM Studio) a été compilé avec GCC 12 et déclare une dépendance ELF vers `GLIBCXX_3.4.30`. AlmaLinux 9 ne fournit que GCC 11 → `GLIBCXX_3.4.29` maximum.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 1 — LD_PRELOAD ne peut pas satisfaire DT_VERNEED
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Construire un shim `.so` qui exporte `_ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE` sous le tag de version `GLIBCXX_3.4.30`, puis l'injecter via `LD_PRELOAD`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// glibcxx_compat.c
|
||||||
|
__asm__(".symver _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE_compat,"
|
||||||
|
"_ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@GLIBCXX_3.4.30");
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
gcc -shared -fPIC -Wl,--version-script=glibcxx_compat.map \
|
||||||
|
-o libglibcxx_compat.so glibcxx_compat.c /usr/lib64/libstdc++.so.6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pourquoi ça échoue
|
||||||
|
|
||||||
|
Dans ELF, `DT_VERNEED` lie une exigence de version à une bibliothèque **nominalement identifiée**. La section `.gnu.version_r` de `watcher.node` dit :
|
||||||
|
|
||||||
|
> « J'ai besoin de `GLIBCXX_3.4.30` fourni par `libstdc++.so.6`. »
|
||||||
|
|
||||||
|
Le linker dynamique (`ld.so`) vérifie cette version **directement dans `libstdc++.so.6`**, pas dans les bibliothèques chargées via `LD_PRELOAD`. Un shim préchargé sous un autre nom de fichier ne peut pas satisfaire cette exigence — même s'il exporte le bon symbole sous le bon nom de version.
|
||||||
|
|
||||||
|
### Erreur observée
|
||||||
|
|
||||||
|
Malgré `LD_PRELOAD`, le daemon plante toujours avec :
|
||||||
|
```
|
||||||
|
/lib64/libstdc++.so.6: version GLIBCXX_3.4.30 not found
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 2 — Patch texte simple ne suffit pas (hash ELF non mis à jour)
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Remplacer la chaîne `GLIBCXX_3.4.30` par `GLIBCXX_3.4.29` directement dans le binaire avec `sed` ou un patch Python simple.
|
||||||
|
|
||||||
|
### Pourquoi ça échoue
|
||||||
|
|
||||||
|
La section `.gnu.version_r` contient des structures `Elf64_Vernaux` :
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
Elf64_Word vna_hash; // hash ELF du nom de version
|
||||||
|
Elf64_Half vna_flags;
|
||||||
|
Elf64_Half vna_other;
|
||||||
|
Elf64_Word vna_name; // offset dans la string table
|
||||||
|
Elf64_Word vna_next;
|
||||||
|
} Elf64_Vernaux;
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque entrée contient **deux références** au nom de version :
|
||||||
|
1. `vna_name` → offset vers la chaîne dans `.dynstr`
|
||||||
|
2. `vna_hash` → hash ELF de cette chaîne (précalculé)
|
||||||
|
|
||||||
|
Un patch texte change la chaîne mais laisse `vna_hash` intact. Le linker dynamique vérifie le hash **en premier** — il ne trouve pas `hash(GLIBCXX_3.4.29)` dans `libstdc++.so.6` et rejette le module.
|
||||||
|
|
||||||
|
### Erreur observée
|
||||||
|
|
||||||
|
```
|
||||||
|
/lib64/libstdc++.so.6: version GLIBCXX_3.4.29 not found
|
||||||
|
```
|
||||||
|
(nouveau message — la chaîne a changé mais le hash est toujours celui de `GLIBCXX_3.4.30`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 3 — Mauvais offset dans le parseur ELF
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Écrire `patch-glibcxx-compat.py` pour parser `.gnu.version_r` et patcher hash + chaîne.
|
||||||
|
|
||||||
|
### Erreur de décodage
|
||||||
|
|
||||||
|
Structure `Elf64_Verneed` :
|
||||||
|
```
|
||||||
|
offset +0 : vn_version (2 bytes)
|
||||||
|
offset +2 : vn_cnt (2 bytes)
|
||||||
|
offset +4 : vn_file (4 bytes) ← oublié !
|
||||||
|
offset +8 : vn_aux (4 bytes)
|
||||||
|
offset +12 : vn_next (4 bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier jet lisait `vn_aux` à `+4` (en sautant `vn_file`) — le parseur pointait vers de mauvaises entrées, ne trouvait rien à patcher.
|
||||||
|
|
||||||
|
### Fix appliqué
|
||||||
|
|
||||||
|
Corriger les offsets dans le script Python :
|
||||||
|
```python
|
||||||
|
vn_aux = struct.unpack_from(f'{endian}I', data, vneed_pos + 8)[0] # +8, pas +4
|
||||||
|
vn_next = struct.unpack_from(f'{endian}I', data, vneed_pos + 12)[0] # +12, pas +8
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 4 — Patcher vers GLIBCXX_3.4.29 mais le symbole n'y existe pas
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Rediriger la dépendance de `GLIBCXX_3.4.30` vers `GLIBCXX_3.4.29` (version juste en dessous, disponible sur AlmaLinux 9).
|
||||||
|
|
||||||
|
### Pourquoi ça échoue
|
||||||
|
|
||||||
|
Le symbole `_ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE` n'existe **pas** sous `GLIBCXX_3.4.29` dans la libstdc++ système. Il est exporté sous `GLIBCXX_3.4.11` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ nm -D /lib64/libstdc++.so.6 | grep condition_variable | grep wait
|
||||||
|
...
|
||||||
|
_ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@@GLIBCXX_3.4.11
|
||||||
|
```
|
||||||
|
|
||||||
|
Patcher vers `.4.29` donnait encore `version GLIBCXX_3.4.29 not found` (pas de ce symbole sous cette version).
|
||||||
|
|
||||||
|
### Fix appliqué
|
||||||
|
|
||||||
|
Cibler `GLIBCXX_3.4.11` dans le patcher — c'est là que le symbole vit réellement :
|
||||||
|
|
||||||
|
```python
|
||||||
|
old_ver = b'GLIBCXX_3.4.30'
|
||||||
|
new_ver = b'GLIBCXX_3.4.11' # même longueur → patch in-place sûr
|
||||||
|
old_hash = elf_hash('GLIBCXX_3.4.30')
|
||||||
|
new_hash = elf_hash('GLIBCXX_3.4.11')
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux chaînes font 14 octets → remplacement in-place sans décalage de la table de chaînes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 5 — `bytearray.index(0, ...)` lève ValueError
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Lire le nom de version depuis `.dynstr` :
|
||||||
|
```python
|
||||||
|
name_end = data.index(0, name_off) # cherche le '\0' terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pourquoi ça échoue
|
||||||
|
|
||||||
|
`bytearray.index()` attend un entier ou une séquence d'octets selon le contexte, mais la forme `data.index(0, ...)` sur `bytearray` lève `ValueError: 0 is not in bytearray` (comportement différent de `bytes`).
|
||||||
|
|
||||||
|
### Fix appliqué
|
||||||
|
|
||||||
|
```python
|
||||||
|
name_end = data.index(b'\x00', name_off) # forme bytes, toujours valide
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 6 — `gcc -lstdc++` introuvable
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Compiler le shim LD_PRELOAD (approche abandonnée) avec `-lstdc++`.
|
||||||
|
|
||||||
|
### Pourquoi ça échoue
|
||||||
|
|
||||||
|
AlmaLinux 9 ne fournit pas le lien symbolique `libstdc++.so` (sans version) nécessaire au linker. Seul `libstdc++.so.6` (avec version) est présent.
|
||||||
|
|
||||||
|
### Fix appliqué
|
||||||
|
|
||||||
|
Passer le chemin complet :
|
||||||
|
```bash
|
||||||
|
gcc -shared -fPIC -Wl,--version-script=glibcxx_compat.map \
|
||||||
|
-o libglibcxx_compat.so glibcxx_compat.c /usr/lib64/libstdc++.so.6
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 7 — Systemd tuait le daemon après démarrage (`lms server start` quitte après 0)
|
||||||
|
|
||||||
|
### Ce qui a été tenté
|
||||||
|
|
||||||
|
Service `Type=simple` avec `ExecStart=lms server start --port 1234 --bind 0.0.0.0`.
|
||||||
|
|
||||||
|
### Comportement observé
|
||||||
|
|
||||||
|
`lms server start` :
|
||||||
|
1. Lance le daemon `llmster` en arrière-plan
|
||||||
|
2. Attend que le serveur HTTP soit prêt
|
||||||
|
3. Affiche `Success! Server is now running on port 1234`
|
||||||
|
4. **Quitte avec code 0**
|
||||||
|
|
||||||
|
Systemd détecte la fin du processus principal → `KillMode=control-group` (défaut) → tue tout le cgroup → `llmster` mort → service `inactive`.
|
||||||
|
|
||||||
|
Le daemon survivait parfois grâce à un double-fork qui lui permettait d'échapper au cgroup, mais c'était non fiable et `systemctl status` montrait `inactive` même quand le serveur tournait.
|
||||||
|
|
||||||
|
### Ce qui n'a pas fonctionné en intermédiaire
|
||||||
|
|
||||||
|
Lancer `llmster` directement via `ExecStart` + `ExecStartPost=lms server start` :
|
||||||
|
|
||||||
|
```ini
|
||||||
|
ExecStart=/opt/lmstudio/.lmstudio/llmster/0.0.12-1/llmster
|
||||||
|
ExecStartPost=/bin/bash -c 'sleep 5 && lms server start --port 1234 --bind 0.0.0.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
Erreur :
|
||||||
|
```
|
||||||
|
Failed to authenticate: Invalid passkey for lms CLI client.
|
||||||
|
```
|
||||||
|
|
||||||
|
`lms server start` utilise un passkey stocké/généré lors du démarrage du daemon via `lms`. Démarrer `llmster` directement bypasse ce mécanisme → authentification impossible depuis `lms`.
|
||||||
|
|
||||||
|
### Fix appliqué — wrapper `run-llmster.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
LMS="${HOME}/.lmstudio/bin/lms"
|
||||||
|
|
||||||
|
# lms gère le passkey ET le démarrage du daemon
|
||||||
|
"$LMS" server start --port "$LMS_PORT" --bind "$LMS_BIND"
|
||||||
|
|
||||||
|
# Rester en vie tant que llmster tourne → systemd garde le service "active"
|
||||||
|
while pgrep -u "$(id -un)" -x llmster > /dev/null 2>&1; do
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 1 # déclenche Restart=on-failure si le daemon meurt
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultat :
|
||||||
|
- Le wrapper est le processus principal (`Type=simple`)
|
||||||
|
- `lms server start` gère correctement passkey + démarrage du daemon
|
||||||
|
- La boucle maintient le service `active (running)` dans systemd
|
||||||
|
- Si `llmster` meurt, le wrapper sort avec code 1 → `Restart=on-failure` relance tout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 8 — Port 1234 inaccessible depuis le réseau
|
||||||
|
|
||||||
|
### Symptôme
|
||||||
|
|
||||||
|
`curl http://192.168.10.20:1234/v1/models` depuis storage-01 → `No route to host`
|
||||||
|
|
||||||
|
Le serveur écoutait bien sur `0.0.0.0:1234` mais le pare-feu AlmaLinux bloquait le port.
|
||||||
|
|
||||||
|
### Fix appliqué
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firewall-cmd --permanent --add-port=1234/tcp
|
||||||
|
firewall-cmd --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajouté dans `tasks/main.yml` via `ansible.posix.firewalld` pour idempotence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution finale
|
||||||
|
|
||||||
|
| Composant | Approche retenue |
|
||||||
|
|---|---|
|
||||||
|
| Dépendance GLIBCXX | Patch ELF de `watcher.node` : réécriture de `vna_hash` + chaîne `GLIBCXX_3.4.30` → `GLIBCXX_3.4.11` |
|
||||||
|
| Service systemd | Wrapper shell : `lms server start` + boucle de monitoring `pgrep llmster` |
|
||||||
|
| Firewall | `firewall-cmd --permanent --add-port=1234/tcp` |
|
||||||
|
|
||||||
|
### Fichiers du rôle Ansible
|
||||||
|
|
||||||
|
```
|
||||||
|
roles/lm_studio/
|
||||||
|
├── files/
|
||||||
|
│ ├── patch-glibcxx-compat.py # parseur ELF, patch hash+string VERNEED
|
||||||
|
│ ├── install-lmstudio-compat.sh # télécharge bundle, patche watcher.node, bootstrap
|
||||||
|
│ └── run-llmster.sh # wrapper systemd
|
||||||
|
└── templates/
|
||||||
|
└── lm-studio.service.j2 # Type=simple, ExecStart=run-llmster.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis storage-01
|
||||||
|
curl http://192.168.10.20:1234/v1/models
|
||||||
|
# → {"data":[...],"object":"list"}
|
||||||
|
|
||||||
|
# Sur gpu-01
|
||||||
|
systemctl status lm-studio
|
||||||
|
# → active (running), MainPID=run-llmster.sh, llmster dans le cgroup
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 9 — llmster rebind sur 127.0.0.1 après chargement Vulkan
|
||||||
|
|
||||||
|
### Symptôme
|
||||||
|
|
||||||
|
`lms server start --bind 0.0.0.0` démarre correctement, mais après
|
||||||
|
`lms load <modele> --gpu max` avec le backend Vulkan, llmster rebind son
|
||||||
|
serveur HTTP sur `127.0.0.1:1234`. Le port 1234 devient inaccessible depuis
|
||||||
|
le réseau LAN.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sur gpu-01 après lms load :
|
||||||
|
ss -tlnp | grep 1234
|
||||||
|
# LISTEN 0 128 127.0.0.1:1234 ← loopback seulement
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cause
|
||||||
|
|
||||||
|
Comportement interne de llmster lors de l'initialisation du backend Vulkan :
|
||||||
|
le processus recrée son socket HTTP et le bind sur loopback plutôt que
|
||||||
|
sur l'adresse configurée au démarrage.
|
||||||
|
|
||||||
|
### Fix appliqué — socat proxy dans run-llmster.sh
|
||||||
|
|
||||||
|
Après le chargement du modèle, un proxy socat est lancé sur l'IP LAN pour
|
||||||
|
forwarder vers le loopback. Les deux sockets sont non-conflictuels (IPs
|
||||||
|
différentes) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LAN_IP=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||||
|
socat TCP-LISTEN:1234,fork,bind="${LAN_IP}",reuseaddr TCP:127.0.0.1:1234 &
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pourquoi ne pas relancer `lms server start --bind 0.0.0.0` ?**
|
||||||
|
Relancer `lms server start` après le chargement recharge le modèle avec le
|
||||||
|
contexte par défaut (4096 tokens), écrasant le `--context-length 65536`
|
||||||
|
passé à `lms load`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocage 10 — Gemma 4 crash Vulkan sur prompts longs (>800 tokens)
|
||||||
|
|
||||||
|
### Symptôme
|
||||||
|
|
||||||
|
Avec `google/gemma-4-e4b` ou `google/gemma-4-e2b`, la première inférence
|
||||||
|
avec un prompt de plus de ~800 tokens provoque un crash du processus llmster :
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP 400 {"error":"The model has crashed without additional information. (Exit code: null)"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Après le crash, llmster recharge le modèle avec le contexte par défaut (4096),
|
||||||
|
et les requêtes suivantes échouent avec `n_keep >= n_ctx`.
|
||||||
|
|
||||||
|
### Cause
|
||||||
|
|
||||||
|
Bug dans l'implémentation Vulkan de llama.cpp pour l'architecture Gemma 4.
|
||||||
|
Gemma 4 utilise une attention **interleaved global/local** avec un sliding
|
||||||
|
window de 512 tokens. Quand le prompt dépasse 512 tokens, les layers
|
||||||
|
d'attention globale crashent dans le shader Vulkan.
|
||||||
|
|
||||||
|
Seuil confirmé par test binaire :
|
||||||
|
- OK à 500 tokens (~100x "You are a helpful assistant.")
|
||||||
|
- CRASH à ~900 tokens (~130x)
|
||||||
|
|
||||||
|
La VRAM n'est pas en cause : avec GQA aggressif et sliding window, le KV
|
||||||
|
cache de Gemma 4 à 65536 tokens est ~2 GB seulement (bien dans les 12GB VRAM).
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
Utiliser un modèle avec attention GQA standard (sans sliding window interleaved) :
|
||||||
|
- **Qwen3.5-9B** ✓ — support Vulkan mature, GQA standard
|
||||||
|
- **Qwen2.5-7B-Instruct** ✓ — alternative
|
||||||
|
|
||||||
|
Ne pas utiliser Gemma 4 E2B ou E4B avec le backend Vulkan de LM Studio jusqu'à
|
||||||
|
correction du bug upstream dans llama.cpp.
|
||||||
18
ansible/roles/lm_studio/defaults/main.yml
Normal file
18
ansible/roles/lm_studio/defaults/main.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
lm_studio_user: lmstudio
|
||||||
|
lm_studio_home: /opt/lmstudio
|
||||||
|
lm_studio_port: 1234
|
||||||
|
lm_studio_host: "0.0.0.0"
|
||||||
|
lm_studio_context_length: 65536
|
||||||
|
lm_studio_gpu_offload: "max"
|
||||||
|
|
||||||
|
# HSA override for unofficially supported GPUs (e.g. RX 6700XT = gfx1031 → 10.3.0)
|
||||||
|
hsa_override_gfx_version: "10.3.0"
|
||||||
|
|
||||||
|
# Modèle chargé au démarrage du service
|
||||||
|
# Format HuggingFace : "organization/repo/fichier.gguf"
|
||||||
|
# Laisser vide pour ne pas auto-charger (chargement manuel via lms load)
|
||||||
|
lm_studio_default_model: ""
|
||||||
|
|
||||||
|
# Chemin NFS où stocker les modèles (laisse vide = stockage local par défaut)
|
||||||
|
lm_studio_models_nfs_path: ""
|
||||||
57
ansible/roles/lm_studio/files/install-lmstudio-compat.sh
Normal file
57
ansible/roles/lm_studio/files/install-lmstudio-compat.sh
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install LM Studio on AlmaLinux 9.
|
||||||
|
#
|
||||||
|
# AlmaLinux 9 ships GCC 11 → libstdc++ provides up to GLIBCXX_3.4.29.
|
||||||
|
# LM Studio bundles watcher.node (compiled with GCC 12) which requires
|
||||||
|
# _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@GLIBCXX_3.4.30.
|
||||||
|
# That symbol exists in the system libstdc++ under GLIBCXX_3.4.11.
|
||||||
|
#
|
||||||
|
# Fix: ELF-patch watcher.node to rewrite the VERNEED entry from
|
||||||
|
# GLIBCXX_3.4.30 → GLIBCXX_3.4.11 (same symbol, correct version tag).
|
||||||
|
# Both vna_hash and the version string are updated in-place.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Ansible copy task places files in /tmp
|
||||||
|
if [ -f "/tmp/patch-glibcxx-compat.py" ]; then
|
||||||
|
PATCHER="/tmp/patch-glibcxx-compat.py"
|
||||||
|
else
|
||||||
|
PATCHER="${SCRIPT_DIR}/patch-glibcxx-compat.py"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Fetch installer metadata ---
|
||||||
|
echo "[lmstudio-install] Fetching installer metadata..."
|
||||||
|
INSTALL_SH=$(curl -fsSL https://lmstudio.ai/install.sh)
|
||||||
|
VERSION=$(echo "$INSTALL_SH" | grep '^APP_VERSION=' | cut -d'"' -f2)
|
||||||
|
ARTIFACT_URL_BASE=$(echo "$INSTALL_SH" | grep '^ARTIFACT_DOWNLOAD_URL=' | cut -d'"' -f2)
|
||||||
|
|
||||||
|
ARTIFACT="${VERSION}-linux-x64.full.tar.gz"
|
||||||
|
URL="https://${ARTIFACT_URL_BASE}/${ARTIFACT}"
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||||||
|
|
||||||
|
echo "[lmstudio-install] Downloading ${ARTIFACT}..."
|
||||||
|
curl -fsSL "$URL" -o "$TMPDIR/bundle.tar.gz"
|
||||||
|
|
||||||
|
echo "[lmstudio-install] Extracting bundle..."
|
||||||
|
tar xf "$TMPDIR/bundle.tar.gz" -C "$TMPDIR"
|
||||||
|
|
||||||
|
# Patch watcher.node before bootstrap so the daemon never sees GLIBCXX_3.4.30
|
||||||
|
echo "[lmstudio-install] Patching watcher.node (GLIBCXX_3.4.30 → GLIBCXX_3.4.11)..."
|
||||||
|
find "$TMPDIR" -name 'watcher.node' | while read -r node; do
|
||||||
|
python3 "$PATCHER" "$node"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[lmstudio-install] Running bootstrap..."
|
||||||
|
LMS_BOOTSTRAP_INSTALL_SH=1 "$TMPDIR/llmster" bootstrap
|
||||||
|
|
||||||
|
# Also patch the installed copy in ~/.lmstudio after bootstrap
|
||||||
|
echo "[lmstudio-install] Patching installed watcher.node copies..."
|
||||||
|
find "${HOME}/.lmstudio" -name 'watcher.node' 2>/dev/null | while read -r node; do
|
||||||
|
python3 "$PATCHER" "$node"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[lmstudio-install] Installation complete."
|
||||||
147
ansible/roles/lm_studio/files/patch-glibcxx-compat.py
Normal file
147
ansible/roles/lm_studio/files/patch-glibcxx-compat.py
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Patch ELF .so/.node files that require GLIBCXX_3.4.30 to use GLIBCXX_3.4.29
|
||||||
|
instead. Required for AlmaLinux 9 / RHEL 9 which ships GCC 11 (max GLIBCXX_3.4.29).
|
||||||
|
|
||||||
|
Both the version string AND the vna_hash in .gnu.version_r are updated so the
|
||||||
|
dynamic linker finds the version correctly at runtime.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def elf_hash(name: str) -> int:
|
||||||
|
"""ELF version symbol hash (not GNU hash — used in .gnu.version_r)."""
|
||||||
|
h = 0
|
||||||
|
for c in name:
|
||||||
|
h = ((h << 4) + ord(c)) & 0xFFFFFFFF
|
||||||
|
g = h & 0xF0000000
|
||||||
|
if g:
|
||||||
|
h ^= g >> 24
|
||||||
|
h &= (~g) & 0xFFFFFFFF
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path: str) -> bool:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
data = bytearray(f.read())
|
||||||
|
|
||||||
|
old_ver = b'GLIBCXX_3.4.30'
|
||||||
|
new_ver = b'GLIBCXX_3.4.11'
|
||||||
|
|
||||||
|
if old_ver not in data:
|
||||||
|
print(f' skip (no GLIBCXX_3.4.30): {path}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ----- ELF header -----
|
||||||
|
if data[:4] != b'\x7fELF':
|
||||||
|
print(f' skip (not ELF): {path}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
ei_class = data[4] # 1=32-bit, 2=64-bit
|
||||||
|
ei_data = data[5] # 1=LE, 2=BE
|
||||||
|
endian = '<' if ei_data == 1 else '>'
|
||||||
|
|
||||||
|
if ei_class == 2:
|
||||||
|
# 64-bit ELF
|
||||||
|
e_shoff_fmt = f'{endian}Q'
|
||||||
|
ehdr_size = 64
|
||||||
|
shdr_size = 64
|
||||||
|
shdr_fmt = f'{endian}IIQQQQIIQQ' # 64-byte shdr
|
||||||
|
(e_shoff,) = struct.unpack_from(f'{endian}Q', data, 40)
|
||||||
|
(e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 58)
|
||||||
|
shdr_type_off = 4
|
||||||
|
shdr_off_off = 24 # sh_offset within shdr
|
||||||
|
shdr_link_off = 48 # sh_link within shdr
|
||||||
|
else:
|
||||||
|
# 32-bit ELF
|
||||||
|
(e_shoff,) = struct.unpack_from(f'{endian}I', data, 32)
|
||||||
|
(e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 46)
|
||||||
|
shdr_type_off = 4
|
||||||
|
shdr_off_off = 16
|
||||||
|
shdr_link_off = 28
|
||||||
|
|
||||||
|
SHT_GNU_verneed = 0x6FFFFFFE
|
||||||
|
|
||||||
|
patched = False
|
||||||
|
# Target GLIBCXX_3.4.11 — that's where condition_variable::wait actually lives
|
||||||
|
# in AlmaLinux 9's libstdc++.so.6 (same byte length: 14 chars each)
|
||||||
|
old_hash = elf_hash('GLIBCXX_3.4.30')
|
||||||
|
new_hash = elf_hash('GLIBCXX_3.4.11')
|
||||||
|
old_hash_bytes = struct.pack(f'{endian}I', old_hash)
|
||||||
|
new_hash_bytes = struct.pack(f'{endian}I', new_hash)
|
||||||
|
|
||||||
|
for i in range(e_shnum):
|
||||||
|
shdr_base = e_shoff + i * e_shentsize
|
||||||
|
sh_type = struct.unpack_from(f'{endian}I', data, shdr_base + shdr_type_off)[0]
|
||||||
|
|
||||||
|
if sh_type != SHT_GNU_verneed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# sh_offset and sh_link (index of associated string table section)
|
||||||
|
if ei_class == 2:
|
||||||
|
sh_offset = struct.unpack_from(f'{endian}Q', data, shdr_base + 24)[0]
|
||||||
|
sh_size = struct.unpack_from(f'{endian}Q', data, shdr_base + 32)[0]
|
||||||
|
sh_link = struct.unpack_from(f'{endian}I', data, shdr_base + 40)[0]
|
||||||
|
sh_info = struct.unpack_from(f'{endian}I', data, shdr_base + 44)[0]
|
||||||
|
else:
|
||||||
|
sh_offset = struct.unpack_from(f'{endian}I', data, shdr_base + 16)[0]
|
||||||
|
sh_size = struct.unpack_from(f'{endian}I', data, shdr_base + 20)[0]
|
||||||
|
sh_link = struct.unpack_from(f'{endian}I', data, shdr_base + 24)[0]
|
||||||
|
sh_info = struct.unpack_from(f'{endian}I', data, shdr_base + 28)[0]
|
||||||
|
|
||||||
|
# String table section referenced by sh_link
|
||||||
|
strtab_shdr = e_shoff + sh_link * e_shentsize
|
||||||
|
if ei_class == 2:
|
||||||
|
strtab_off = struct.unpack_from(f'{endian}Q', data, strtab_shdr + 24)[0]
|
||||||
|
else:
|
||||||
|
strtab_off = struct.unpack_from(f'{endian}I', data, strtab_shdr + 16)[0]
|
||||||
|
|
||||||
|
# Walk Elf64_Verneed / Elf64_Vernaux entries
|
||||||
|
# Elf64_Verneed: vn_version(2)+vn_cnt(2)+vn_file(4)+vn_aux(4)+vn_next(4) = 16 bytes
|
||||||
|
# Elf64_Vernaux: vna_hash(4)+vna_flags(2)+vna_other(2)+vna_name(4)+vna_next(4) = 16 bytes
|
||||||
|
vneed_pos = sh_offset
|
||||||
|
for _ in range(sh_info): # sh_info = number of Verneed entries
|
||||||
|
vn_cnt = struct.unpack_from(f'{endian}H', data, vneed_pos + 2)[0]
|
||||||
|
vn_aux = struct.unpack_from(f'{endian}I', data, vneed_pos + 8)[0] # +8 (after vn_file)
|
||||||
|
vn_next = struct.unpack_from(f'{endian}I', data, vneed_pos + 12)[0]
|
||||||
|
|
||||||
|
vaux_pos = vneed_pos + vn_aux
|
||||||
|
for _ in range(vn_cnt):
|
||||||
|
vna_hash = struct.unpack_from(f'{endian}I', data, vaux_pos)[0]
|
||||||
|
vna_name = struct.unpack_from(f'{endian}I', data, vaux_pos + 8)[0]
|
||||||
|
vna_next = struct.unpack_from(f'{endian}I', data, vaux_pos + 12)[0]
|
||||||
|
|
||||||
|
# Read the version name from the string table
|
||||||
|
name_off = strtab_off + vna_name
|
||||||
|
name_end = data.index(b'\x00', name_off)
|
||||||
|
ver_name = bytes(data[name_off:name_end])
|
||||||
|
|
||||||
|
if ver_name == old_ver:
|
||||||
|
# Patch hash
|
||||||
|
struct.pack_into(f'{endian}I', data, vaux_pos, new_hash)
|
||||||
|
# Patch string (same byte length — safe in-place)
|
||||||
|
data[name_off:name_off + len(old_ver)] = new_ver
|
||||||
|
patched = True
|
||||||
|
|
||||||
|
if vna_next == 0:
|
||||||
|
break
|
||||||
|
vaux_pos += vna_next
|
||||||
|
|
||||||
|
if vn_next == 0:
|
||||||
|
break
|
||||||
|
vneed_pos += vn_next
|
||||||
|
|
||||||
|
if patched:
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
print(f' patched (hash+string): {path}')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f' skip (GLIBCXX_3.4.30 not in verneed): {path}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
patch_file(path)
|
||||||
45
ansible/roles/lm_studio/files/run-llmster.sh
Normal file
45
ansible/roles/lm_studio/files/run-llmster.sh
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# LM Studio service wrapper.
|
||||||
|
#
|
||||||
|
# Lets lms server start handle daemon launch (passkey setup, etc.),
|
||||||
|
# optionally auto-loads a model (LMS_MODEL + LMS_CONTEXT_LENGTH),
|
||||||
|
# then monitors the llmster process and exits when it dies so systemd
|
||||||
|
# can restart the service.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LMS="${HOME}/.lmstudio/bin/lms"
|
||||||
|
LMS_PORT="${LMS_PORT:-1234}"
|
||||||
|
LMS_BIND="${LMS_BIND:-0.0.0.0}"
|
||||||
|
LMS_MODEL="${LMS_MODEL:-}"
|
||||||
|
LMS_CONTEXT_LENGTH="${LMS_CONTEXT_LENGTH:-65536}"
|
||||||
|
LMS_GPU_OFFLOAD="${LMS_GPU_OFFLOAD:-max}"
|
||||||
|
|
||||||
|
echo "Starting LM Studio server (port ${LMS_PORT}, bind ${LMS_BIND})..."
|
||||||
|
"$LMS" server start --port "$LMS_PORT" --bind "$LMS_BIND"
|
||||||
|
|
||||||
|
if [[ -n "$LMS_MODEL" ]]; then
|
||||||
|
echo "Unloading any previously loaded models..."
|
||||||
|
"$LMS" unload --all 2>/dev/null || true
|
||||||
|
echo "Auto-loading model ${LMS_MODEL} (context ${LMS_CONTEXT_LENGTH}, gpu ${LMS_GPU_OFFLOAD})..."
|
||||||
|
"$LMS" load "$LMS_MODEL" --context-length "$LMS_CONTEXT_LENGTH" --gpu "$LMS_GPU_OFFLOAD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# llmster bind sur 127.0.0.1 après chargement du backend Vulkan.
|
||||||
|
# socat proxy sur l'IP LAN (non-conflictuel avec le loopback) → llmster.
|
||||||
|
if [[ "$LMS_BIND" != "127.0.0.1" ]]; then
|
||||||
|
LAN_IP=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||||
|
if [[ -n "$LAN_IP" ]]; then
|
||||||
|
echo "Starting socat proxy ${LAN_IP}:${LMS_PORT} → 127.0.0.1:${LMS_PORT}..."
|
||||||
|
socat TCP-LISTEN:"${LMS_PORT}",fork,bind="${LAN_IP}",reuseaddr TCP:127.0.0.1:"${LMS_PORT}" &
|
||||||
|
SOCAT_PID=$!
|
||||||
|
echo "socat PID: ${SOCAT_PID}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Server started. Monitoring llmster daemon..."
|
||||||
|
while pgrep -u "$(id -un)" -x llmster > /dev/null 2>&1; do
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "llmster daemon stopped unexpectedly, exiting..."
|
||||||
|
exit 1
|
||||||
9
ansible/roles/lm_studio/handlers/main.yml
Normal file
9
ansible/roles/lm_studio/handlers/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Restart lm-studio
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: lm-studio
|
||||||
|
state: restarted
|
||||||
144
ansible/roles/lm_studio/tasks/main.yml
Normal file
144
ansible/roles/lm_studio/tasks/main.yml
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
---
|
||||||
|
- name: Install LM Studio dependencies
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- fuse
|
||||||
|
- fuse-libs
|
||||||
|
- curl
|
||||||
|
- libatomic
|
||||||
|
- python3
|
||||||
|
- mesa-vulkan-drivers
|
||||||
|
- vulkan-tools
|
||||||
|
- socat
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create lmstudio system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ lm_studio_user }}"
|
||||||
|
home: "{{ lm_studio_home }}"
|
||||||
|
shell: /bin/bash
|
||||||
|
groups:
|
||||||
|
- video
|
||||||
|
- render
|
||||||
|
append: true
|
||||||
|
system: false
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Check if lms CLI is already installed
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ lm_studio_home }}/.lmstudio/bin/lms"
|
||||||
|
register: lms_binary
|
||||||
|
|
||||||
|
- name: Copy patch-glibcxx-compat.py
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: patch-glibcxx-compat.py
|
||||||
|
dest: /tmp/patch-glibcxx-compat.py
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Copy LM Studio installer files
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ item.src }}"
|
||||||
|
dest: "{{ item.dest }}"
|
||||||
|
mode: "{{ item.mode }}"
|
||||||
|
loop:
|
||||||
|
- {src: install-lmstudio-compat.sh, dest: /tmp/install-lmstudio-compat.sh, mode: '0755'}
|
||||||
|
when: not lms_binary.stat.exists
|
||||||
|
|
||||||
|
- name: Run LM Studio installer
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: /tmp/install-lmstudio-compat.sh
|
||||||
|
executable: /bin/bash
|
||||||
|
become: true
|
||||||
|
become_user: "{{ lm_studio_user }}"
|
||||||
|
environment:
|
||||||
|
HOME: "{{ lm_studio_home }}"
|
||||||
|
when: not lms_binary.stat.exists
|
||||||
|
register: lms_install_result
|
||||||
|
|
||||||
|
- name: Show installer output
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: lms_install_result.stdout_lines
|
||||||
|
when: lms_install_result is changed
|
||||||
|
|
||||||
|
- name: Deploy llmster wrapper script
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: run-llmster.sh
|
||||||
|
dest: /opt/lmstudio/run-llmster.sh
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Patch all GLIBCXX_3.4.30-dependent binaries (watcher.node, libllm_engine.so, ...)
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
find "{{ lm_studio_home }}/.lmstudio" \( -name '*.node' -o -name 'lib*.so' \) \
|
||||||
|
-exec python3 /tmp/patch-glibcxx-compat.py {} \;
|
||||||
|
executable: /bin/bash
|
||||||
|
register: patch_result
|
||||||
|
changed_when: "'patched' in patch_result.stdout"
|
||||||
|
|
||||||
|
- name: Show patch output
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: patch_result.stdout_lines
|
||||||
|
when: patch_result is changed
|
||||||
|
|
||||||
|
- name: Force Vulkan backend (AMD GPU — CPU backend selected by default)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ lm_studio_home }}/.lmstudio/.internal/backend-preferences-v1.json"
|
||||||
|
owner: "{{ lm_studio_user }}"
|
||||||
|
group: "{{ lm_studio_user }}"
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model_format": "gguf",
|
||||||
|
"name": "llama.cpp-linux-x86_64-vulkan-avx2",
|
||||||
|
"version": "2.13.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
notify:
|
||||||
|
- Reload systemd
|
||||||
|
- Restart lm-studio
|
||||||
|
|
||||||
|
- name: Deploy LM Studio systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: lm-studio.service.j2
|
||||||
|
dest: /etc/systemd/system/lm-studio.service
|
||||||
|
mode: '0644'
|
||||||
|
notify:
|
||||||
|
- Reload systemd
|
||||||
|
- Restart lm-studio
|
||||||
|
|
||||||
|
- name: Enable lm-studio service
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: lm-studio
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Redirect models dir vers NFS (symlink)
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: "{{ lm_studio_models_nfs_path }}"
|
||||||
|
dest: "{{ lm_studio_home }}/.lmstudio/models"
|
||||||
|
state: link
|
||||||
|
force: true
|
||||||
|
owner: "{{ lm_studio_user }}"
|
||||||
|
group: "{{ lm_studio_user }}"
|
||||||
|
when: lm_studio_models_nfs_path is defined and lm_studio_models_nfs_path != ""
|
||||||
|
|
||||||
|
- name: Deploy lms global wrapper (/usr/local/bin/lms)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /usr/local/bin/lms
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
exec sudo -u {{ lm_studio_user }} \
|
||||||
|
HOME={{ lm_studio_home }} \
|
||||||
|
HSA_OVERRIDE_GFX_VERSION={{ hsa_override_gfx_version }} \
|
||||||
|
{{ lm_studio_home }}/.lmstudio/bin/lms "$@"
|
||||||
|
|
||||||
|
- name: Open LM Studio port in firewall
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ lm_studio_port }}/tcp"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
39
ansible/roles/lm_studio/templates/lm-studio.service.j2
Normal file
39
ansible/roles/lm_studio/templates/lm-studio.service.j2
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
[Unit]
|
||||||
|
Description=LM Studio Server
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User={{ lm_studio_user }}
|
||||||
|
Group={{ lm_studio_user }}
|
||||||
|
|
||||||
|
# Répertoire home pour que lms trouve ses fichiers
|
||||||
|
Environment=HOME={{ lm_studio_home }}
|
||||||
|
|
||||||
|
# ROCm — obligatoire pour RX 6700XT (gfx1031 non officiel)
|
||||||
|
Environment=HSA_OVERRIDE_GFX_VERSION={{ hsa_override_gfx_version }}
|
||||||
|
|
||||||
|
# PATH inclut le binaire lms
|
||||||
|
Environment=PATH={{ lm_studio_home }}/.lmstudio/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
Environment=LMS_PORT={{ lm_studio_port }}
|
||||||
|
Environment=LMS_BIND={{ lm_studio_host }}
|
||||||
|
Environment=LMS_MODEL={{ lm_studio_default_model }}
|
||||||
|
Environment=LMS_CONTEXT_LENGTH={{ lm_studio_context_length }}
|
||||||
|
Environment=LMS_GPU_OFFLOAD={{ lm_studio_gpu_offload }}
|
||||||
|
|
||||||
|
# Wrapper: runs lms server start, auto-loads model if LMS_MODEL is set, then monitors llmster
|
||||||
|
ExecStart=/opt/lmstudio/run-llmster.sh
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=15
|
||||||
|
TimeoutStartSec=120
|
||||||
|
|
||||||
|
# Journalisation
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=lm-studio
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
1
ansible/roles/minio/tasks/main.yml
Normal file
1
ansible/roles/minio/tasks/main.yml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
---
|
||||||
7
ansible/roles/nfs_client/defaults/main.yml
Normal file
7
ansible/roles/nfs_client/defaults/main.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
nfs_server_ip: 192.168.10.1
|
||||||
|
nfs_server_export: /srv/data/nfs/k8s
|
||||||
|
|
||||||
|
nfs_client_mounts:
|
||||||
|
- local_path: /mnt/nfs
|
||||||
|
remote_export: "{{ nfs_server_export }}"
|
||||||
5
ansible/roles/nfs_client/handlers/main.yml
Normal file
5
ansible/roles/nfs_client/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: Reload systemd and activate automounts
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
listen: Reload systemd and activate automounts
|
||||||
42
ansible/roles/nfs_client/tasks/main.yml
Normal file
42
ansible/roles/nfs_client/tasks/main.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
- name: Install nfs-utils
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: nfs-utils
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create NFS mount points
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item.local_path }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
loop: "{{ nfs_client_mounts }}"
|
||||||
|
|
||||||
|
- name: Write NFS entries in fstab (sans monter immédiatement)
|
||||||
|
ansible.posix.mount:
|
||||||
|
src: "{{ nfs_server_ip }}:{{ item.remote_export }}"
|
||||||
|
path: "{{ item.local_path }}"
|
||||||
|
fstype: nfs
|
||||||
|
# Options pour extinctions fréquentes :
|
||||||
|
# soft → échoue proprement (EIO) si serveur injoignable
|
||||||
|
# timeo=30 / retrans=3 → 3s timeout, 3 essais avant EIO
|
||||||
|
# _netdev → montage après réseau disponible seulement
|
||||||
|
# nofail → n'empêche pas le boot si NFS absent
|
||||||
|
# x-systemd.automount → monté à la demande (premier accès), pas au boot
|
||||||
|
# x-systemd.mount-timeout=30 → 30s max pour réussir le montage à la demande
|
||||||
|
opts: "nfsvers=4,soft,timeo=30,retrans=3,_netdev,nofail,x-systemd.automount,x-systemd.mount-timeout=30"
|
||||||
|
state: present
|
||||||
|
dump: 0
|
||||||
|
passno: 0
|
||||||
|
loop: "{{ nfs_client_mounts }}"
|
||||||
|
notify: Reload systemd and activate automounts
|
||||||
|
|
||||||
|
- name: Reload systemd et activer les automounts NFS
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Enable and start NFS automount units
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "{{ item.local_path | replace('/', '-') | regex_replace('^-', '') }}.automount"
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
loop: "{{ nfs_client_mounts }}"
|
||||||
10
ansible/roles/nfs_server/defaults/main.yml
Normal file
10
ansible/roles/nfs_server/defaults/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
raid_device: /dev/md127
|
||||||
|
raid_uuid: "3add8360-fa01-47eb-8aa1-0e84bacbbc15"
|
||||||
|
raid_fstype: ext4
|
||||||
|
raid_mount: /srv/data
|
||||||
|
|
||||||
|
nfs_exports:
|
||||||
|
- path: /srv/data/nfs/k8s
|
||||||
|
network: "{{ cluster_network }}"
|
||||||
|
options: "rw,sync,no_subtree_check,no_root_squash"
|
||||||
14
ansible/roles/nfs_server/handlers/main.yml
Normal file
14
ansible/roles/nfs_server/handlers/main.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
- name: Rebuild initramfs
|
||||||
|
ansible.builtin.command: dracut --force
|
||||||
|
listen: Rebuild initramfs
|
||||||
|
|
||||||
|
- name: Reload exports
|
||||||
|
ansible.builtin.command: exportfs -ra
|
||||||
|
listen: Reload exports
|
||||||
|
|
||||||
|
- name: Restart nfs-server
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: nfs-server
|
||||||
|
state: restarted
|
||||||
|
listen: Restart nfs-server
|
||||||
83
ansible/roles/nfs_server/tasks/main.yml
Normal file
83
ansible/roles/nfs_server/tasks/main.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
- name: Install nfs-utils and mdadm
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- nfs-utils
|
||||||
|
- mdadm
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# --- RAID persistence ---
|
||||||
|
|
||||||
|
- name: Persist mdadm array configuration
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: mdadm --detail --scan
|
||||||
|
register: mdadm_scan
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Write /etc/mdadm.conf
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ mdadm_scan.stdout }}\n"
|
||||||
|
dest: /etc/mdadm.conf
|
||||||
|
mode: '0644'
|
||||||
|
notify: Rebuild initramfs
|
||||||
|
|
||||||
|
# --- RAID mount ---
|
||||||
|
|
||||||
|
- name: Create RAID mount point
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ raid_mount }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Mount RAID5 array (fstab + immediate)
|
||||||
|
ansible.posix.mount:
|
||||||
|
src: "UUID={{ raid_uuid }}"
|
||||||
|
path: "{{ raid_mount }}"
|
||||||
|
fstype: "{{ raid_fstype }}"
|
||||||
|
opts: defaults,noatime,nodiratime
|
||||||
|
state: mounted
|
||||||
|
dump: 0
|
||||||
|
passno: 2
|
||||||
|
|
||||||
|
# --- Répertoires NFS ---
|
||||||
|
|
||||||
|
- name: Create NFS export directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item.path }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
owner: nobody
|
||||||
|
group: nobody
|
||||||
|
loop: "{{ nfs_exports }}"
|
||||||
|
|
||||||
|
# --- NFS server ---
|
||||||
|
|
||||||
|
- name: Deploy /etc/exports
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: exports.j2
|
||||||
|
dest: /etc/exports
|
||||||
|
mode: '0644'
|
||||||
|
notify:
|
||||||
|
- Reload exports
|
||||||
|
- Restart nfs-server
|
||||||
|
|
||||||
|
- name: Enable and start nfs-server
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: nfs-server
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# --- Firewall ---
|
||||||
|
|
||||||
|
- name: Allow NFS through firewall (firewalld uniquement, pas nftables)
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
service: "{{ item }}"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
|
loop:
|
||||||
|
- nfs
|
||||||
|
- mountd
|
||||||
|
- rpc-bind
|
||||||
|
when: ansible_facts.services['firewalld.service'] is defined
|
||||||
|
and ansible_facts.services['firewalld.service']['state'] == 'running'
|
||||||
4
ansible/roles/nfs_server/templates/exports.j2
Normal file
4
ansible/roles/nfs_server/templates/exports.j2
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# /etc/exports — géré par Ansible, ne pas éditer manuellement
|
||||||
|
{% for export in nfs_exports %}
|
||||||
|
{{ export.path }} {{ export.network }}({{ export.options }})
|
||||||
|
{% endfor %}
|
||||||
24
ansible/roles/postgresql/defaults/main.yml
Normal file
24
ansible/roles/postgresql/defaults/main.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
postgresql_version: 16
|
||||||
|
postgresql_port: 5432
|
||||||
|
postgresql_data_dir: /srv/data/postgres
|
||||||
|
|
||||||
|
# Écoute sur localhost + LAN cluster (pour les services k8s futurs)
|
||||||
|
postgresql_listen_addresses: "localhost, 192.168.10.1"
|
||||||
|
|
||||||
|
postgresql_max_connections: 100
|
||||||
|
postgresql_shared_buffers: "256MB"
|
||||||
|
|
||||||
|
# Bases et utilisateurs à créer
|
||||||
|
# Mots de passe dans le vault : vault_pg_hermes_password, vault_pg_litellm_password
|
||||||
|
postgresql_databases:
|
||||||
|
- name: hermes
|
||||||
|
- name: litellm
|
||||||
|
|
||||||
|
postgresql_users:
|
||||||
|
- name: hermes
|
||||||
|
password: "{{ vault_pg_hermes_password }}"
|
||||||
|
db: hermes
|
||||||
|
- name: litellm
|
||||||
|
password: "{{ vault_pg_litellm_password }}"
|
||||||
|
db: litellm
|
||||||
10
ansible/roles/postgresql/handlers/main.yml
Normal file
10
ansible/roles/postgresql/handlers/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
- name: Restart postgresql
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "postgresql-{{ postgresql_version }}"
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Reload postgresql
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "postgresql-{{ postgresql_version }}"
|
||||||
|
state: reloaded
|
||||||
142
ansible/roles/postgresql/tasks/main.yml
Normal file
142
ansible/roles/postgresql/tasks/main.yml
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
# --- Dépôt officiel PostgreSQL ---
|
||||||
|
|
||||||
|
- name: Install PostgreSQL repo
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm"
|
||||||
|
state: present
|
||||||
|
disable_gpg_check: true
|
||||||
|
|
||||||
|
- name: Disable built-in postgresql module
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: dnf -qy module disable postgresql
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
# --- Installation ---
|
||||||
|
|
||||||
|
- name: Install PostgreSQL {{ postgresql_version }}
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- "postgresql{{ postgresql_version }}-server"
|
||||||
|
- "postgresql{{ postgresql_version }}"
|
||||||
|
- python3-psycopg2
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# --- Répertoire données sur RAID5 ---
|
||||||
|
|
||||||
|
- name: Create PostgreSQL data directory on RAID5
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ postgresql_data_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: postgres
|
||||||
|
group: postgres
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
# --- Service override pour PGDATA custom ---
|
||||||
|
|
||||||
|
- name: Create systemd override directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/etc/systemd/system/postgresql-{{ postgresql_version }}.service.d"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Deploy PGDATA override
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "/etc/systemd/system/postgresql-{{ postgresql_version }}.service.d/pgdata.conf"
|
||||||
|
content: |
|
||||||
|
[Service]
|
||||||
|
Environment=PGDATA={{ postgresql_data_dir }}
|
||||||
|
mode: '0644'
|
||||||
|
notify: Restart postgresql
|
||||||
|
|
||||||
|
# --- Initialisation (une seule fois) ---
|
||||||
|
|
||||||
|
- name: Check if database cluster is initialized
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ postgresql_data_dir }}/PG_VERSION"
|
||||||
|
register: pg_initialized
|
||||||
|
|
||||||
|
- name: Initialize database cluster
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "/usr/pgsql-{{ postgresql_version }}/bin/initdb -D {{ postgresql_data_dir }} --auth-local=peer --auth-host=scram-sha-256"
|
||||||
|
become: true
|
||||||
|
become_user: postgres
|
||||||
|
environment:
|
||||||
|
PGDATA: "{{ postgresql_data_dir }}"
|
||||||
|
when: not pg_initialized.stat.exists
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
|
||||||
|
- name: Deploy pg_hba.conf
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: pg_hba.conf.j2
|
||||||
|
dest: "{{ postgresql_data_dir }}/pg_hba.conf"
|
||||||
|
owner: postgres
|
||||||
|
group: postgres
|
||||||
|
mode: '0600'
|
||||||
|
notify: Reload postgresql
|
||||||
|
|
||||||
|
- name: Configure listen_addresses
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ postgresql_data_dir }}/postgresql.conf"
|
||||||
|
regexp: "^#?listen_addresses"
|
||||||
|
line: "listen_addresses = '{{ postgresql_listen_addresses }}'"
|
||||||
|
notify: Reload postgresql
|
||||||
|
|
||||||
|
- name: Configure port
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ postgresql_data_dir }}/postgresql.conf"
|
||||||
|
regexp: "^#?port"
|
||||||
|
line: "port = {{ postgresql_port }}"
|
||||||
|
notify: Reload postgresql
|
||||||
|
|
||||||
|
- name: Configure shared_buffers
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ postgresql_data_dir }}/postgresql.conf"
|
||||||
|
regexp: "^#?shared_buffers"
|
||||||
|
line: "shared_buffers = {{ postgresql_shared_buffers }}"
|
||||||
|
notify: Reload postgresql
|
||||||
|
|
||||||
|
# --- Démarrage ---
|
||||||
|
|
||||||
|
- name: Enable and start PostgreSQL
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "postgresql-{{ postgresql_version }}"
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# --- Bases de données et utilisateurs ---
|
||||||
|
|
||||||
|
- name: Create databases
|
||||||
|
community.postgresql.postgresql_db:
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
encoding: UTF8
|
||||||
|
state: present
|
||||||
|
become: true
|
||||||
|
become_user: postgres
|
||||||
|
loop: "{{ postgresql_databases }}"
|
||||||
|
|
||||||
|
- name: Create users
|
||||||
|
community.postgresql.postgresql_user:
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
password: "{{ item.password }}"
|
||||||
|
state: present
|
||||||
|
become: true
|
||||||
|
become_user: postgres
|
||||||
|
loop: "{{ postgresql_users }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Grant user privileges on their database
|
||||||
|
community.postgresql.postgresql_privs:
|
||||||
|
db: "{{ item.db }}"
|
||||||
|
privs: ALL
|
||||||
|
type: database
|
||||||
|
role: "{{ item.name }}"
|
||||||
|
state: present
|
||||||
|
become: true
|
||||||
|
become_user: postgres
|
||||||
|
loop: "{{ postgresql_users }}"
|
||||||
|
|
||||||
|
# Firewall géré centralement par le rôle gateway (nftables)
|
||||||
|
# Port 5432 ouvert pour cluster_network dans nftables.conf.j2
|
||||||
9
ansible/roles/postgresql/templates/pg_hba.conf.j2
Normal file
9
ansible/roles/postgresql/templates/pg_hba.conf.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# TYPE DATABASE USER ADDRESS METHOD
|
||||||
|
# Accès local superuser
|
||||||
|
local all postgres peer
|
||||||
|
# Accès local apps — scram-sha-256
|
||||||
|
local all all scram-sha-256
|
||||||
|
host all all 127.0.0.1/32 scram-sha-256
|
||||||
|
host all all ::1/128 scram-sha-256
|
||||||
|
# Accès depuis le LAN cluster (services k8s futurs)
|
||||||
|
host all all {{ cluster_network }} scram-sha-256
|
||||||
13
ansible/roles/qdrant/defaults/main.yml
Normal file
13
ansible/roles/qdrant/defaults/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
qdrant_version: "1.13.4"
|
||||||
|
qdrant_user: qdrant
|
||||||
|
qdrant_data_dir: /srv/data/qdrant
|
||||||
|
qdrant_install_dir: /opt/qdrant
|
||||||
|
|
||||||
|
qdrant_http_port: 6333
|
||||||
|
qdrant_grpc_port: 6334
|
||||||
|
|
||||||
|
# 0.0.0.0 — accessible depuis le LAN cluster (firewall filtre)
|
||||||
|
qdrant_host: "0.0.0.0"
|
||||||
|
|
||||||
|
qdrant_log_level: INFO
|
||||||
5
ansible/roles/qdrant/handlers/main.yml
Normal file
5
ansible/roles/qdrant/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: Restart qdrant
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: qdrant
|
||||||
|
state: restarted
|
||||||
92
ansible/roles/qdrant/tasks/main.yml
Normal file
92
ansible/roles/qdrant/tasks/main.yml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
---
|
||||||
|
# --- Utilisateur système ---
|
||||||
|
|
||||||
|
- name: Create qdrant system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ qdrant_user }}"
|
||||||
|
system: true
|
||||||
|
shell: /sbin/nologin
|
||||||
|
home: "{{ qdrant_install_dir }}"
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# --- Répertoires données (RAID5) ---
|
||||||
|
|
||||||
|
- name: Create Qdrant data directories on RAID5
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ qdrant_user }}"
|
||||||
|
group: "{{ qdrant_user }}"
|
||||||
|
mode: '0750'
|
||||||
|
loop:
|
||||||
|
- "{{ qdrant_data_dir }}"
|
||||||
|
- "{{ qdrant_data_dir }}/storage"
|
||||||
|
- "{{ qdrant_data_dir }}/snapshots"
|
||||||
|
|
||||||
|
# --- Binaire ---
|
||||||
|
|
||||||
|
- name: Check if qdrant binary is already installed
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ qdrant_install_dir }}/qdrant"
|
||||||
|
register: qdrant_binary
|
||||||
|
|
||||||
|
- name: Check installed qdrant version
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "{{ qdrant_install_dir }}/qdrant --version"
|
||||||
|
register: qdrant_current_version
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
when: qdrant_binary.stat.exists
|
||||||
|
|
||||||
|
- name: Download qdrant binary
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://github.com/qdrant/qdrant/releases/download/v{{ qdrant_version }}/qdrant-x86_64-unknown-linux-musl.tar.gz"
|
||||||
|
dest: /tmp/qdrant.tar.gz
|
||||||
|
mode: '0644'
|
||||||
|
when: >
|
||||||
|
not qdrant_binary.stat.exists or
|
||||||
|
qdrant_version not in (qdrant_current_version.stdout | default(''))
|
||||||
|
|
||||||
|
- name: Extract qdrant binary
|
||||||
|
ansible.builtin.unarchive:
|
||||||
|
src: /tmp/qdrant.tar.gz
|
||||||
|
dest: "{{ qdrant_install_dir }}"
|
||||||
|
remote_src: true
|
||||||
|
owner: "{{ qdrant_user }}"
|
||||||
|
group: "{{ qdrant_user }}"
|
||||||
|
when: >
|
||||||
|
not qdrant_binary.stat.exists or
|
||||||
|
qdrant_version not in (qdrant_current_version.stdout | default(''))
|
||||||
|
notify: Restart qdrant
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
|
||||||
|
- name: Deploy Qdrant config
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: config.yaml.j2
|
||||||
|
dest: "{{ qdrant_install_dir }}/config.yaml"
|
||||||
|
owner: "{{ qdrant_user }}"
|
||||||
|
group: "{{ qdrant_user }}"
|
||||||
|
mode: '0640'
|
||||||
|
notify: Restart qdrant
|
||||||
|
|
||||||
|
# --- Service systemd ---
|
||||||
|
|
||||||
|
- name: Deploy Qdrant systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: qdrant.service.j2
|
||||||
|
dest: /etc/systemd/system/qdrant.service
|
||||||
|
mode: '0644'
|
||||||
|
notify:
|
||||||
|
- Restart qdrant
|
||||||
|
|
||||||
|
- name: Enable and start Qdrant
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: qdrant
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# Firewall géré centralement par le rôle gateway (nftables)
|
||||||
|
# Ports 6333 (HTTP) et 6334 (gRPC) ouverts pour cluster_network dans nftables.conf.j2
|
||||||
11
ansible/roles/qdrant/templates/config.yaml.j2
Normal file
11
ansible/roles/qdrant/templates/config.yaml.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
storage:
|
||||||
|
storage_path: {{ qdrant_data_dir }}/storage
|
||||||
|
snapshots_path: {{ qdrant_data_dir }}/snapshots
|
||||||
|
|
||||||
|
service:
|
||||||
|
host: {{ qdrant_host }}
|
||||||
|
http_port: {{ qdrant_http_port }}
|
||||||
|
grpc_port: {{ qdrant_grpc_port }}
|
||||||
|
enable_cors: true
|
||||||
|
|
||||||
|
log_level: {{ qdrant_log_level }}
|
||||||
18
ansible/roles/qdrant/templates/qdrant.service.j2
Normal file
18
ansible/roles/qdrant/templates/qdrant.service.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Qdrant — vector database
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User={{ qdrant_user }}
|
||||||
|
Group={{ qdrant_user }}
|
||||||
|
ExecStart={{ qdrant_install_dir }}/qdrant --config-path {{ qdrant_install_dir }}/config.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=qdrant
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
25
ansible/roles/rocm/defaults/main.yml
Normal file
25
ansible/roles/rocm/defaults/main.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
rocm_rhel_version: "9.4"
|
||||||
|
rocm_amdgpu_repo: "https://repo.radeon.com/amdgpu/latest/rhel/{{ rocm_rhel_version }}/main/x86_64/"
|
||||||
|
rocm_repo: "https://repo.radeon.com/rocm/rhel9/latest/main/"
|
||||||
|
rocm_gpg_key: "https://repo.radeon.com/rocm/rocm.gpg.key"
|
||||||
|
|
||||||
|
rocm_packages:
|
||||||
|
- rocminfo
|
||||||
|
- rocm-hip-runtime
|
||||||
|
- rocm-opencl-runtime
|
||||||
|
- rocblas
|
||||||
|
- hipblaslt
|
||||||
|
|
||||||
|
# Packages supplémentaires requis pour compiler llama.cpp from source (rôle llama_server)
|
||||||
|
rocm_build_packages:
|
||||||
|
- hipblas-devel
|
||||||
|
- rocblas-devel
|
||||||
|
- hip-devel
|
||||||
|
- hipcc
|
||||||
|
- cmake
|
||||||
|
- gcc-c++
|
||||||
|
- git
|
||||||
|
- patchelf
|
||||||
|
|
||||||
|
hsa_override_gfx_version: "10.3.0"
|
||||||
6
ansible/roles/rocm/handlers/main.yml
Normal file
6
ansible/roles/rocm/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
- name: Reload amdgpu module
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
modprobe -r amdgpu || true
|
||||||
|
modprobe amdgpu
|
||||||
|
changed_when: true
|
||||||
60
ansible/roles/rocm/tasks/main.yml
Normal file
60
ansible/roles/rocm/tasks/main.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
- name: Import AMD GPG key
|
||||||
|
ansible.builtin.rpm_key:
|
||||||
|
key: "{{ rocm_gpg_key }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Add amdgpu repository
|
||||||
|
ansible.builtin.yum_repository:
|
||||||
|
name: amdgpu
|
||||||
|
description: AMD GPU drivers
|
||||||
|
baseurl: "{{ rocm_amdgpu_repo }}"
|
||||||
|
enabled: true
|
||||||
|
gpgcheck: true
|
||||||
|
gpgkey: "{{ rocm_gpg_key }}"
|
||||||
|
|
||||||
|
- name: Add ROCm repository
|
||||||
|
ansible.builtin.yum_repository:
|
||||||
|
name: rocm
|
||||||
|
description: ROCm
|
||||||
|
baseurl: "{{ rocm_repo }}"
|
||||||
|
enabled: true
|
||||||
|
gpgcheck: true
|
||||||
|
gpgkey: "{{ rocm_gpg_key }}"
|
||||||
|
|
||||||
|
- name: Install kernel headers and devel (requis pour amdgpu-dkms)
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- kernel-headers
|
||||||
|
- kernel-devel
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install ROCm packages
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "{{ rocm_packages }}"
|
||||||
|
state: present
|
||||||
|
notify: Reload amdgpu module
|
||||||
|
|
||||||
|
- name: Add ansible user to video and render groups
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ ansible_service_user }}"
|
||||||
|
groups:
|
||||||
|
- video
|
||||||
|
- render
|
||||||
|
append: true
|
||||||
|
|
||||||
|
- name: Set HSA_OVERRIDE_GFX_VERSION system-wide
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/environment
|
||||||
|
regexp: '^HSA_OVERRIDE_GFX_VERSION='
|
||||||
|
line: "HSA_OVERRIDE_GFX_VERSION={{ hsa_override_gfx_version }}"
|
||||||
|
create: true
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Set HSA_OVERRIDE_GFX_VERSION dans profile.d (sessions interactives)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/profile.d/rocm.sh
|
||||||
|
content: |
|
||||||
|
export HSA_OVERRIDE_GFX_VERSION={{ hsa_override_gfx_version }}
|
||||||
|
export PATH=$PATH:/opt/rocm/bin
|
||||||
|
mode: '0644'
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ansible-core>=2.17,<2.18
|
||||||
Loading…
Add table
Add a link
Reference in a new issue