diff --git a/CLAUDE.md b/CLAUDE.md index 0d5d357..1c50a09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,15 +6,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **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 / llama-server ROCm), 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-13)** : -- ✅ Hermes Agent opérationnel sur storage-01 (llama-server GPU via LiteLLM) +**État actuel (2026-05-17)** : +- ✅ Hermes Agent opérationnel — multi-profils (funk-ai/system/monitor/brain) +- ✅ RAG opérationnel — Qdrant 284 chunks, `rag-ingest` / `rag-query`, skill funk-ai +- ✅ Monitoring complet — Prometheus + Grafana + AlertManager → webhook Hermes - ✅ storage-01 installé — `192.168.10.1` (LAN cluster), `192.168.1.200` (WAN) - ✅ gpu-01 installé — llama-server GPU (:1234) + CPU (:1236/:1237) opérationnels - ✅ LiteLLM sur storage-01 — proxy multi-modèles + ask-agent - ✅ Cluster k8s Talos opérationnel (MetalLB + Traefik + wildcard DNS lab.local) +- ✅ postfix_relay — SMTP sortant via Brevo (aliyesilkaya93@gmail.com) - ⏳ WOL compute nodes non configuré (BIOS) — allumage manuel requis +- ⏳ ArgoCD bootstrap + workloads IA (Open WebUI, n8n) + +Journal de progression : `PROGRESS.md` → `progress/YYYY-MM-DD.md` --- @@ -22,42 +26,16 @@ Référence complète : `Funk/Funk-infra/HOMELAB.md` | 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 | +| storage-01 | 192.168.10.1 / 192.168.1.200 | 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 | llama-server dédié (RX 6700XT, ROCm 7.x) — **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 +- `storage-01` et `gpu-01` sont **hors du cluster k8s** — consommés via `ExternalName` services ou `Endpoints` manuels - 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, llama_server, litellm, hermes_agent, hermes_user -├── 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) -``` +- Modèles LLM stockés sur NFS RAID5 de storage-01 (`/srv/data/models`), montés sur gpu-01 (`/mnt/models`) --- @@ -65,22 +43,67 @@ lab-infra/ | Outil | Cible | Déclenchement | |---|---|---| -| **Ansible** | storage-01, gpu-01 (AlmaLinux) | Push manuel : `ansible-playbook` | +| **Ansible** | storage-01, gpu-01 (AlmaLinux) | Push manuel : `make apply-storage` / `make apply-gpu` | | **talhelper** | compute-01/02/03 (Talos) | Push manuel : `talosctl apply-config` | | **ArgoCD** | Workloads k8s | Pull automatique depuis Git | -### Ansible +--- + +## Ansible + +### Commandes via Makefile (depuis la racine du repo) ```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 +make install # pip install + ansible-galaxy (à faire une fois) +make ping # vérifier la connectivité vers tous les hôtes +make check # dry-run site.yml (storage-01 + gpu-01) +make apply # appliquer site.yml complet +make apply-storage # appliquer uniquement storage-01.yml +make apply-gpu # appliquer uniquement gpu-01.yml +make vault-view # lire le vault chiffré +make vault-edit # éditer le vault (ouvre $EDITOR) +make vault-encrypt # (ré)chiffrer vault.yml ``` -Rôles : `common`, `gateway`, `dnsmasq`, `nfs_server`, `nfs_client`, `postgresql`, `qdrant`, `minio`, `rocm`, `llama_server`, `litellm`, `hermes_agent`, `hermes_user`. Tous idempotents. Secrets via Ansible Vault. +### Commandes directes (depuis `ansible/`) -### talhelper (nœuds Talos) +```bash +ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm +ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server +``` + +Les tags correspondent aux noms de rôles. Tous les rôles sont idempotents. + +### Inventaire et connectivité + +- `storage-01` : accès direct SSH via `192.168.1.200` (WAN) +- `gpu-01` : accès via ProxyJump sur `storage-01` (configuré dans `inventory.yml`) +- Vault password dans `.vault_pass` (ignoré par git, chargé automatiquement via `ansible.cfg`) + +### Rôles — storage-01 + +`hermes_user`, `common`, `gateway` (nftables + NAT), `dnsmasq`, `nfs_server`, `qdrant`, `postgresql`, `minio`, `litellm`, `hermes_agent`, `rag`, `node_exporter`, `alertmanager_webhook`, `postfix_relay` + +### Rôles — gpu-01 + +`hermes_user`, `common`, `rocm`, `nfs_client`, `llama_server`, `node_exporter` + +> `lm_studio` est présent dans `ansible/roles/` mais n'est plus utilisé dans aucun playbook. + +### Caveat premier déploiement — hermes_agent + +Le rôle `hermes_agent` installe le binaire via `curl | bash` seulement si le binaire est absent. La configuration (`.env`, `config.yaml`) est ensuite conditionnelle à `hermes_binary.stat.exists`. Sur un hôte vierge, il faut **deux runs consécutifs** : le premier installe, le second configure. + +### Secrets Ansible Vault + +Variables chiffrées dans `ansible/group_vars/all/vault.yml` : +- `vault_anthropic_api_key` — clé Anthropic pour le profil brain +- `vault_pg_hermes_password`, `vault_pg_litellm_password` +- `vault_postfix_relay_password` — SMTP Brevo + +--- + +## talhelper (nœuds Talos) ```bash cd talos/ @@ -90,102 +113,151 @@ talhelper gencommand bootstrap --node compute-01 | bash # bootstrap (1ère fo talhelper gencommand kubeconfig | bash # récupérer kubeconfig ``` -`talos/clusterconfig/` est généré — ne pas committer (mettre au `.gitignore`). +`talos/clusterconfig/` est généré — gitignored, ne pas committer. +`talos/talsecret.sops.yaml` chiffré avec SOPS+age (clé dans `.config/sops/`, ignorée par git). -### ArgoCD (workloads k8s) +--- -Pattern "App of Apps" : modifier un manifest → `git commit` + `git push` → ArgoCD applique automatiquement (poll toutes les ~3 min). +## ArgoCD (workloads k8s) + +Pattern "App of Apps" : modifier un manifest → `git commit` + `git push` → ArgoCD poll automatiquement (~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 ``` +ArgoCD utilise le repo `git@github.com:Alkatrazz24/Funk-lab.git` branche `main`. Les deux namespaces gérés actuellement : `infra` (Traefik, MetalLB, NFS-provisioner) et `monitoring` (kube-prometheus-stack 85.0.2). + --- -## Réseaux internes Kubernetes +## Stack IA — Hermes Agent -- CNI (Flannel) — Pods : `10.42.0.0/16` +### Profils + +| Profil | Modèle | Port | Usage | +|---|---|---|---| +| `funk-ai` | Qwen3-8B Q4_K_M | GPU :1234 | Agent principal (~35 tok/s) | +| `system` | Qwen3-1.7B Q4_K_M | CPU :1236 | Calculs, formatage, résumés | +| `monitor` | Qwen3-1.7B Q4_K_M | CPU :1237 | Analyse logs, métriques, état services | +| `brain` | Claude Sonnet 4.6 API | Anthropic | Raisonnement complexe — facturé | + +Hermes écoute sur le port `8080` (gateway). Dashboard web sur le port `9119` (accessible uniquement depuis `192.168.1.10`). + +### Switcher le modèle de Hermes à chaud + +```bash +hermes-switch status # modèle actuel de hermes-default +hermes-switch qwen # → Qwen3-8B sur gpu-01 (local, gratuit) +hermes-switch claude # → Claude Sonnet 4.6 via API Anthropic (facturé) +``` + +`hermes-switch` modifie `/etc/litellm/config.yaml` puis redémarre `litellm`. L'alias `hermes-default` dans LiteLLM est ce que Hermes consomme — changer cet alias ne nécessite pas de reconfigurer Hermes. + +### ask-agent (délégation inter-agents) + +Binaire dans `/usr/local/bin/ask-agent` sur storage-01. Appel LiteLLM direct (:4000), réponse en ~2-5s. + +**Règle** : ask-agent reçoit du **texte**, jamais des commandes shell. Toujours 2 étapes : +```bash +# Étape 1 — collecter les données +journalctl -u litellm -n 20 --no-pager --output=cat +# Étape 2 — passer le texte à ask-agent +ask-agent monitor "logs litellm : — erreurs ?" +``` + +### RAG — Documentation Funk + +Qdrant collection `funk-docs` indexée depuis `admin/` (284 chunks, score min 0.60). + +```bash +rag-query "comment relancer dnsmasq ?" +rag-query "nftables port cluster configuration" +rag-query "hermes profils litellm modèles" --top 3 +``` + +Scripts installés sur storage-01 : `/usr/local/bin/rag-query`, `/usr/local/bin/rag-ingest` + +Après modification de `admin/`, re-déployer via `make apply-storage --tags rag` (exécute `rag-ingest`). Le script `admin/index_knowledge.py` est l'ingesteur Python brut. + +### Hermes Skills — `hermes-skills/` + +Versionnés dans ce repo, déployés par le rôle `hermes_agent` vers `~hermes/`. + +``` +hermes-skills/ +├── funk/ +│ ├── agent-delegation/SKILL.md # usage de ask-agent (2 étapes, agents dispo) +│ └── rag-docs/SKILL.md # usage de rag-query +└── souls/ # prompts système par profil + ├── funk-ai.md # profil principal (agent sur storage-01) + ├── system.md # profil calculs/formatage + ├── monitor.md # profil analyse logs + └── brain.md # profil Claude API +``` + +--- + +## Base de connaissance — `admin/` + +Documentation admin versionnée, indexée dans le RAG : + +``` +admin/ +├── ia/ # hermes, litellm, llama_server, rocm, rag, alertmanager-webhook +├── infra/ # dnsmasq, nfs, réseau, ssh, email +├── install/ # procédures installation storage-01, gpu-01, k8s +├── k8s/ # argocd, talos, monitoring +└── ops/ # ansible, cluster, système +``` + +--- + +## Réseaux + +- 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` +- MetalLB pool : `192.168.10.200-230` (IP virtuelles Traefik/services) +- DNS wildcard `*.lab.local` via dnsmasq → Traefik + +nftables géré par le rôle `gateway` sur storage-01. Modifier `ansible/roles/gateway/templates/nftables.conf.j2` puis `make apply-storage --tags gateway`. --- ## Contrainte GPU critique -La RX 6700XT (gfx1031) n'est **pas officiellement supportée** par ROCm. Toujours s'assurer que la variable est présente dans le service systemd `llama-server` : +La RX 6700XT (gfx1031) n'est **pas officiellement supportée** par ROCm. Variable obligatoire dans tous les services GPU : ``` HSA_OVERRIDE_GFX_VERSION=10.3.0 ``` -ROCm tourne directement sur l'hôte AlmaLinux (pas dans des conteneurs). L'inférence est assurée par llama-server (llama.cpp compilé ROCm 7.x). - -CLI `lms` disponible sur gpu-01 uniquement pour télécharger des modèles (`lms get `, `lms ls`). +Gérée automatiquement par le rôle `llama_server`. ROCm tourne directement sur l'hôte AlmaLinux. --- -## Deux setups IA - -### Setup 1 — Poste perso (9950X3D) — dev local - -| Composant | Endpoint | -|---|---| -| Ollama | `http://127.0.0.1:11434` | -| LM Studio (optionnel) | `http://127.0.0.1:1234/v1` | -| Hermes Agent | `base_url: http://127.0.0.1:1234/v1` ou Ollama | - -### Setup 2 — Cluster Funk (production) - -| Composant | Endpoint | -|---|---| -| llama-server GPU (gpu-01) | `http://192.168.10.20:1234/v1` | -| llama-server CPU system (gpu-01) | `http://192.168.10.20:1236/v1` | -| llama-server CPU monitor (gpu-01) | `http://192.168.10.20:1237/v1` | -| LiteLLM proxy (storage-01) | `http://127.0.0.1:4000/v1` (master_key: `lm-studio`) | -| Hermes Agent | service systemd sur storage-01, via LiteLLM | +## Gestion du cluster ```bash -# Vérifier l'état complet depuis le poste perso -funk-status +funk-cluster status # état de tous les services (storage-01 + gpu-01 + k8s) +funk-cluster start # WOL gpu-01 + démarrage services storage-01 +funk-cluster start --k8s # idem + nœuds k8s (WOL → drain-aware boot) +funk-cluster stop # arrêt propre gpu-01 + services +funk-cluster stop --k8s # idem + drain k8s → NFS check → talosctl shutdown +``` + +**etcd backup** (single control-plane → critique) : +```bash +talosctl etcd snapshot /path/to/backup.db --nodes compute-01 ``` --- ## Conventions -- **Domaine** : `lab.local` — wildcard DNS via dnsmasq sur storage-01 -- **Ingress** : `.lab.local` → IP MetalLB de Traefik (`192.168.10.200-230`) -- **llama-server endpoint** : `http://192.168.10.20:1234/v1` (GPU) — accès via LiteLLM sur storage-01 -- **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 -``` +- **Git** : branches `main` (prod) / `feature/*` — conventional commits (`feat:`, `fix:`, `chore:`, `docs:`) +- **Secrets** : Ansible Vault (Ansible), SOPS+age (Talos), Sealed Secrets (k8s) +- **Namespaces k8s** : `argocd`, `infra`, `monitoring`, `external` +- **Hermes data** : `~hermes/` doit résider sur le RAID5 `/srv/data`, pas sur le SSD OS +- **RAM compute-02/03** : toujours définir `resources.requests` + `resources.limits` (8 GB RAM, OOM-killer actif) diff --git a/admin/infra/email.md b/admin/infra/email.md new file mode 100644 index 0000000..a30db68 --- /dev/null +++ b/admin/infra/email.md @@ -0,0 +1,172 @@ +# Email — Relay SMTP via Postfix → Gmail + +## Architecture + +``` +Hermes / AlertManager / cron / kubectl + │ + ▼ + Postfix (storage-01 :25) + smtp_generic_maps → réécrit l'expéditeur en aliyesilkaya93@gmail.com + │ SASL + TLS (App Password) + ▼ + smtp.gmail.com:587 + │ + ▼ + aliyesilkaya93@gmail.com +``` + +Postfix est configuré en **relay-only** sur storage-01 : aucune livraison locale, tous les mails transitent vers Gmail SMTP. Les réseaux autorisés à relayer : `127.0.0.0/8`, `192.168.10.0/24`, pods k8s (`10.42.0.0/16`), services k8s (`10.43.0.0/16`). + +**Pourquoi Gmail et pas Brevo ?** Brevo exige de posséder un domaine vérifié (SPF/DKIM) pour relayer. Gmail SMTP avec App Password ne nécessite aucun domaine. + +--- + +## Prérequis — Google App Password + +La vérification en 2 étapes Google doit être activée sur le compte. + +1. `myaccount.google.com` → Sécurité → Validation en 2 étapes → Activer +2. `myaccount.google.com/apppasswords` → Nouveau → nom `funk-postfix` → Créer +3. Google génère un code 16 caractères (ex: `abcd efgh ijkl mnop`) — c'est le mot de passe SMTP + +--- + +## Installation + +### 1. Ajouter le App Password dans le vault + +```bash +make vault-edit +``` + +Modifier ou ajouter : +```yaml +vault_postfix_relay_password: "abcd efgh ijkl mnop" # App Password Google (avec espaces, OK) +``` + +```bash +make vault-encrypt +``` + +### 2. Vérifier les vars gateway + +Dans `ansible/group_vars/gateway/vars.yml` : +```yaml +postfix_relay_host: smtp.gmail.com +postfix_relay_port: 587 +postfix_relay_user: "aliyesilkaya93@gmail.com" +postfix_sender_email: "aliyesilkaya93@gmail.com" +``` + +### 3. Déployer + +```bash +make apply-storage +# ou cibler uniquement le rôle : +ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags postfix_relay +``` + +--- + +## Vérification + +### Test depuis storage-01 + +```bash +echo "Test Funk Lab - $(date)" | sudo mail -s "[Funk] Test postfix" aliyesilkaya93@gmail.com +``` + +### Suivre les logs en temps réel + +```bash +sudo journalctl -u postfix -f --no-pager --output=cat +``` + +Résultat attendu : `status=sent (250 2.0.0 OK ... - gsmtp)` + +### Vérifier / vider la file d'attente + +```bash +sudo mailq # lister les mails en attente +sudo postqueue -f # forcer le flush +sudo postsuper -d ALL # vider la queue (mails échoués) +``` + +### Vérifier la réécriture expéditeur + +```bash +sudo postconf smtp_generic_maps +sudo cat /etc/postfix/generic +``` + +--- + +## Utilisation depuis les services + +### Depuis storage-01 (shell / cron / Hermes) + +```bash +echo "Corps du mail" | sudo mail -s "[Funk] Sujet" aliyesilkaya93@gmail.com +``` + +Via Python (dans un skill Hermes) : +```python +import smtplib +from email.message import EmailMessage + +msg = EmailMessage() +msg['Subject'] = '[Funk] Alerte' +msg['From'] = 'aliyesilkaya93@gmail.com' +msg['To'] = 'aliyesilkaya93@gmail.com' +msg.set_content('Contenu du mail') + +with smtplib.SMTP('127.0.0.1', 25) as s: + s.send_message(msg) +``` + +### Depuis AlertManager (k8s) + +Dans `k8s/infra/monitoring/values.yaml` : + +```yaml +alertmanager: + config: + receivers: + - name: email + email_configs: + - to: aliyesilkaya93@gmail.com + from: aliyesilkaya93@gmail.com + smarthost: "192.168.10.1:25" + require_tls: false # TLS géré par Postfix vers Gmail, pas sur le LAN interne + route: + receiver: email +``` + +### Depuis n'importe quel pod k8s + +SMTP relay accessible sur `192.168.10.1:25` depuis tout le cluster. + +--- + +## Dépannage + +| Symptôme | Cause probable | Action | +|---|---|---| +| `Connection refused` sur :25 | postfix non démarré | `sudo systemctl status postfix` | +| `Relay access denied` | IP source hors mynetworks | `sudo postconf mynetworks` | +| `Authentication failed` | App Password révoqué ou 2FA désactivé | Régénérer dans `myaccount.google.com/apppasswords` | +| Mail jamais reçu | Réécriture expéditeur manquante | Vérifier `/etc/postfix/generic` et `postconf smtp_generic_maps` | +| Mail en spam | Expéditeur non reconnu | Normal si From ≠ aliyesilkaya93@gmail.com | +| Queue bloquée | Ancien mail échoué en retry | `sudo postsuper -d ALL` puis retester | + +--- + +## Configuration Ansible + +- **Rôle** : `ansible/roles/postfix_relay/` +- **Vars** : `ansible/group_vars/gateway/vars.yml` → `postfix_relay_*`, `postfix_sender_email` +- **Secret** : `vault_postfix_relay_password` dans `ansible/group_vars/all/vault.yml` +- **Templates** : `main.cf.j2`, `sasl_passwd.j2` +- **Sender rewrite** : `/etc/postfix/generic` (déployé par le rôle, postmap automatique) +- **nftables** : port 25 ouvert automatiquement depuis `192.168.10.0/24` diff --git a/ansible/group_vars/all/vault.yml b/ansible/group_vars/all/vault.yml index faac79e..a4d29f3 100644 --- a/ansible/group_vars/all/vault.yml +++ b/ansible/group_vars/all/vault.yml @@ -1,30 +1,34 @@ $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 +65373732613635326664316362333337616633646539666662353839353665636135373262623230 +6433633033393837323330323730356331313031366664380a376533393237613334663635646636 +30663138346337643935636236646337653762383137306634353437313865356331376630656630 +6233323836333137330a346530376665623064346462633437656139376433666436333835333835 +64393738393238633132373831326436343630373261303466376537303731333164353731646434 +34366437346335653538353763353631623639613865336431653964613163653033653062366232 +37353132633463396661613864633065306564353337383035303037626435623634646530323464 +63346230383463323534653434663632303030333030636636306234353566343039363433643938 +65393063633962376133353264376261386436386661633933613135653537663039346630373033 +32646534653163303239646131306131666632623337363836623033656562353334313366633934 +61623436346431623263633536653236393235393539616133376135656164323336636664333566 +36616230613564626338333934386336616631313739366330303431333836373935356465613365 +61336662636535333566313763383738363363393535323434336234303130643632646364373433 +37343635623165343965613263616535373036616337613234656231313230623330383566623633 +63393263383039613639316135633133636364613532633237313934313136336661376635383435 +65633065653539363066633832396437613364303634656334386266636461393663623462323030 +37653138323037323965336165626531663463383334663937666536663935303230313235383038 +66353162306233313034326431383438333031393735363264386232333034313664366630366262 +36643164663837363132653165636635643162303331386262653130303438383038646361363564 +62643962613061326332643862623538646262333264656335323235353733663563336639633262 +62616161306235656666353738313234316462373065373330623134323836633833343032623832 +35363663653463616265303535623066623134356233663638333565623034323362323163656633 +32316363313865653633636665333561303866376339366163623331396430303436346633323563 +34633266306435633535306439323638336532656335663864646434343233366535396335643763 +36363662323861643638656630383738356331343834303736393337336435646663333134306532 +35383039623432353161633036323430333036613239356439376666323866653331376362613338 +32356661336131373334333132333039623665383065346439626531653364346434643734313237 +34343966313934613734646465663231396435333135356165366435386366336338343466363331 +36666461363735636637613364656366346263353338333539353662643039383338396437316633 +38343434386533613439316632313963346563373131333965396637313937656265346133373635 +33643338316433323139636262353636616231636564383332343862656661636463663938636131 +38386665663635356666336261633761653362373534363161623166643830373737363730613361 +6262 diff --git a/ansible/group_vars/gateway/vars.yml b/ansible/group_vars/gateway/vars.yml index 4313e57..d0f4985 100644 --- a/ansible/group_vars/gateway/vars.yml +++ b/ansible/group_vars/gateway/vars.yml @@ -3,3 +3,10 @@ wan_interface: enp4s0 # NIC1 — port 2 switch — VLAN 1 (192.168.1.20 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 + +# Postfix relay — SMTP sortant via provider externe +postfix_relay_host: smtp.gmail.com +postfix_relay_port: 587 +postfix_relay_user: "aliyesilkaya93@gmail.com" +postfix_sender_email: "aliyesilkaya93@gmail.com" # expéditeur = compte Gmail (DMARC OK) +postfix_relay_password: "{{ vault_postfix_relay_password }}" # dans vault.yml diff --git a/ansible/playbooks/storage-01.yml b/ansible/playbooks/storage-01.yml index aed4a5d..5493981 100644 --- a/ansible/playbooks/storage-01.yml +++ b/ansible/playbooks/storage-01.yml @@ -15,3 +15,4 @@ - { role: rag, tags: [rag] } - { role: node_exporter, tags: [node_exporter] } - { role: alertmanager_webhook, tags: [alertmanager_webhook] } + - { role: postfix_relay, tags: [postfix_relay] } diff --git a/ansible/roles/gateway/handlers/main.yml b/ansible/roles/gateway/handlers/main.yml index 69e8f72..27b3758 100644 --- a/ansible/roles/gateway/handlers/main.yml +++ b/ansible/roles/gateway/handlers/main.yml @@ -2,4 +2,4 @@ - name: Reload nftables ansible.builtin.systemd: name: nftables - state: reloaded + state: restarted diff --git a/ansible/roles/gateway/templates/nftables.conf.j2 b/ansible/roles/gateway/templates/nftables.conf.j2 index a5725af..977dfe9 100644 --- a/ansible/roles/gateway/templates/nftables.conf.j2 +++ b/ansible/roles/gateway/templates/nftables.conf.j2 @@ -69,6 +69,10 @@ table inet filter { } table ip nat { + chain prerouting { + type nat hook prerouting priority -100; + } + chain postrouting { type nat hook postrouting priority 100; diff --git a/ansible/roles/hermes_agent/tasks/main.yml b/ansible/roles/hermes_agent/tasks/main.yml index 19f6cf3..c732fcf 100644 --- a/ansible/roles/hermes_agent/tasks/main.yml +++ b/ansible/roles/hermes_agent/tasks/main.yml @@ -76,14 +76,17 @@ # --- Packages Python pour les skills --- -- name: Install Python packages for Hermes skills - ansible.builtin.pip: - name: "qdrant-client>=1.12.0" - executable: "{{ hermes_home }}/.hermes/hermes-agent/venv/bin/pip" - state: present +- name: Install Python packages for Hermes skills (via uv) + ansible.builtin.command: + cmd: > + {{ hermes_home }}/.local/bin/uv pip install qdrant-client>=1.12.0 + --python {{ hermes_home }}/.hermes/hermes-agent/venv/bin/python + environment: + UV_CACHE_DIR: /tmp/uv-cache become: true - become_user: "{{ hermes_user }}" + become_user: root when: hermes_binary.stat.exists + changed_when: false # --- Configuration --- diff --git a/ansible/roles/postfix_relay/defaults/main.yml b/ansible/roles/postfix_relay/defaults/main.yml new file mode 100644 index 0000000..f860b6a --- /dev/null +++ b/ansible/roles/postfix_relay/defaults/main.yml @@ -0,0 +1,22 @@ +--- +postfix_relay_host: smtp-relay.brevo.com +postfix_relay_port: 587 +postfix_relay_user: "" # à définir dans group_vars/gateway/vars.yml +postfix_relay_password: "" # depuis vault (vault_postfix_relay_password) + +postfix_myhostname: "storage-01.lab.local" +postfix_myorigin: "$myhostname" + +# Adresse expéditeur réécrite pour tous les mails sortants +postfix_sender_email: "" # à définir dans group_vars/gateway/vars.yml + +# Réseaux autorisés à relayer — ajuster si besoin +postfix_mynetworks: + - "127.0.0.0/8" + - "[::1]/128" + - "192.168.10.0/24" # LAN cluster + - "10.42.0.0/16" # pods k8s + - "10.43.0.0/16" # services k8s + +# Ouvrir le port 25 depuis le LAN cluster dans nftables +postfix_open_nftables: true diff --git a/ansible/roles/postfix_relay/handlers/main.yml b/ansible/roles/postfix_relay/handlers/main.yml new file mode 100644 index 0000000..4d0deb7 --- /dev/null +++ b/ansible/roles/postfix_relay/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart postfix + ansible.builtin.systemd: + name: postfix + state: restarted + +- name: Reload nftables + ansible.builtin.systemd: + name: nftables + state: reloaded diff --git a/ansible/roles/postfix_relay/tasks/main.yml b/ansible/roles/postfix_relay/tasks/main.yml new file mode 100644 index 0000000..28ae171 --- /dev/null +++ b/ansible/roles/postfix_relay/tasks/main.yml @@ -0,0 +1,72 @@ +--- +- name: Install postfix and SASL auth library + ansible.builtin.dnf: + name: + - postfix + - cyrus-sasl-plain + - s-nail + state: present + +- name: Deploy main.cf + ansible.builtin.template: + src: main.cf.j2 + dest: /etc/postfix/main.cf + owner: root + group: root + mode: '0644' + notify: Restart postfix + +- name: Deploy sasl_passwd + ansible.builtin.template: + src: sasl_passwd.j2 + dest: /etc/postfix/sasl_passwd + owner: root + group: root + mode: '0600' + register: sasl_passwd_result + +- name: Run postmap on sasl_passwd + ansible.builtin.command: postmap /etc/postfix/sasl_passwd + when: sasl_passwd_result.changed + changed_when: true + notify: Restart postfix + +- name: Deploy generic sender rewrite map + ansible.builtin.copy: + dest: /etc/postfix/generic + content: | + # Réécriture expéditeur — géré par Ansible + @{{ postfix_myhostname }} {{ postfix_sender_email }} + root {{ postfix_sender_email }} + owner: root + group: root + mode: '0644' + register: generic_map_result + when: postfix_sender_email | length > 0 + +- name: Run postmap on generic map + ansible.builtin.command: postmap /etc/postfix/generic + when: generic_map_result is changed + changed_when: true + notify: Restart postfix + +- name: Reset postfix failed state if any + ansible.builtin.command: systemctl reset-failed postfix.service + changed_when: false + failed_when: false + +- name: Enable and start postfix + ansible.builtin.systemd: + name: postfix + enabled: true + state: started + daemon_reload: true + +- name: Open port 25 from cluster LAN in nftables + ansible.builtin.lineinfile: + path: /etc/nftables.conf + insertafter: '# alertmanager-webhook' + line: " tcp dport 25 ip saddr 192.168.10.0/24 accept comment \"postfix-relay\"" + state: present + when: postfix_open_nftables + notify: Reload nftables diff --git a/ansible/roles/postfix_relay/templates/main.cf.j2 b/ansible/roles/postfix_relay/templates/main.cf.j2 new file mode 100644 index 0000000..424697b --- /dev/null +++ b/ansible/roles/postfix_relay/templates/main.cf.j2 @@ -0,0 +1,38 @@ +# Postfix — relay-only — géré par Ansible (rôle postfix_relay) +# Ne pas éditer manuellement + +compatibility_level = 2 +myhostname = {{ postfix_myhostname }} +myorigin = {{ postfix_myorigin }} + +# Relay SMTP externe +relayhost = [{{ postfix_relay_host }}]:{{ postfix_relay_port }} + +# Écouter sur toutes les interfaces pour permettre au cluster de relayer +inet_interfaces = all +inet_protocols = ipv4 + +# Réseaux autorisés à relayer (pas un open relay) +mynetworks = {{ postfix_mynetworks | join(' ') }} + +# Désactiver la livraison locale +mydestination = +local_recipient_maps = +local_transport = error:local delivery disabled + +# TLS sortant +smtp_tls_security_level = encrypt +smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt +smtp_tls_loglevel = 1 + +# Authentification SASL vers le relay +smtp_sasl_auth_enable = yes +smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd +smtp_sasl_security_options = noanonymous +smtp_sasl_tls_security_options = noanonymous + +# Réécriture expéditeur — remplace root@lab.local par une adresse réelle +{% if postfix_sender_email %} +smtp_generic_maps = hash:/etc/postfix/generic +{% endif %} + diff --git a/ansible/roles/postfix_relay/templates/sasl_passwd.j2 b/ansible/roles/postfix_relay/templates/sasl_passwd.j2 new file mode 100644 index 0000000..346627c --- /dev/null +++ b/ansible/roles/postfix_relay/templates/sasl_passwd.j2 @@ -0,0 +1,2 @@ +# Géré par Ansible — ne pas éditer manuellement +[{{ postfix_relay_host }}]:{{ postfix_relay_port }} {{ postfix_relay_user }}:{{ postfix_relay_password }}