# Migration Kea 3.2 / python3.12 — état des lieux et reprise

**Date :** 10 août 2026
**Statut :** lab FreeBSD réparé et fonctionnel. Prod AlmaLinux **non traitée** (pas encore
montée de version). Chantier non planifié, déclenché par une mise à jour système du lab.

---

## 1. Ce qui s'est passé

Une mise à jour système du lab FreeBSD (`against`, 192.168.1.2) a livré deux ruptures
simultanées, sans rapport entre elles :

1. **python3.11 → python3.12** — le rc.d pointait sur `/usr/local/bin/python3.11`, disparu.
   Les paquets pip étant installés par version, `mysql-connector-python` devait aussi être
   réinstallé pour 3.12 (`ModuleNotFoundError: No module named 'mysql'` sinon).
2. **Kea 3.0.3 → 3.2.0** — `kea-ctrl-agent` a été **supprimé** (déprécié en 2.7.2, retiré en
   **3.1.8** ; ARM 3.2 §18.8 « Migration from the obsolete Control Agent »). Plus rien
   n'écoutait sur le port 8000.

### Symptômes observés

- `Config synchronisée — Kea API non joignable (connexion refusée)` au test de synchro.
- Page **Leases** vide, bouton **Libérer** inopérant.
- Avertissements « lease actif » (création/édition/suppression de réservation) **silencieusement
  désactivés** — `LeaseService::findActiveLeases()` renvoie `[]` quand l'API est KO, donc aucune
  alerte, aucune erreur visible. C'est le symptôme le plus insidieux du lot.
- `kea-dhcp4` lui-même continuait de servir les clients (config chargée en mémoire au boot).

### Méthode de diagnostic (à réutiliser)

`ConnectionRefusedError` est une information précise : le SYN a reçu un RST, donc **personne
n'écoute**. Ça élimine d'emblée l'authentification (donnerait un 401), le pare-feu en DROP
(donnerait un timeout) et une erreur côté Kea (donnerait un HTTP 200 avec `result != 0`).
Le message vient de `kea-sync-from-db.py`, branche `isinstance(e.reason, ConnectionRefusedError)`,
et la ligne de log adjacente contient **l'URL réellement tentée**.

---

## 2. Ce qui a été livré

Quatre commits sur `master`, tous synchronisés.

| Commit | Objet |
|--------|-------|
| `6824b4a` | rc.d → python3.12, doc de la procédure de montée de version Python |
| `a1dd9e2` | `control-sockets` portées par kea-dhcp4 (remplacement du Control Agent) |
| `76064bc` | secrets d'API par `user-file` + `password-file` |
| `c147aa3` | pool short-lease en `client-classes` (liste) |

### Le point central : `control-sockets`

`build_config()` émettait `control-socket` (objet, singulier). Il émet désormais
`control-sockets` (liste) :

```json
"control-sockets": [
    { "socket-type": "unix", "socket-name": "/var/run/kea/kea-dhcp4-ctrl.sock" },
    { "socket-type": "http", "socket-address": "192.168.1.2", "socket-port": 8000,
      "authentication": { "type": "basic", "realm": "kea-dhcp4-server",
                          "directory": "/usr/local/etc/kea",
                          "clients": [ { "user-file": "kea-api-user",
                                         "password-file": "kea-api-password" } ] } }
]
```

- L'entrée http est **dérivée de `[kea] api_url`** (`http_control_socket()`) : une seule valeur
  à maintenir, et le serveur écoute par construction là où le script vient frapper.
  `localhost` → `127.0.0.1` (Kea attend une IP), port par défaut 8000.
- **Kea ≥ 3.2 refuse `user` ET `password` en clair** dans la config
  (`DHCP4_PARSER_FAIL … use of clear text '<champ>' is NOT SECURE`). Piège : l'erreur est levée
  **un champ à la fois** — corriger `password` fait réapparaître la même erreur sur `user`.
  `write_api_secret_files()` recopie les deux valeurs dans deux fichiers 0600 (même dossier
  obligatoire, Kea n'accepte qu'un seul `directory`).
- **Format de réponse inchangé** (résultat toujours encapsulé dans une liste JSON) : aucun
  changement dans `kea_config_reload()` ni dans le parsing PHP de `LeaseService`.
- **`kea_api_url` côté appli n'a pas bougé.** Aucun réglage à modifier dans DHCPMan.

### Tests

`python3.12 kea-sync-from-db/test_kea_sync.py` — 18 tests purs, sans MySQL
(`mysql.connector` stubbé, module chargé par `importlib`). Couvrent la dérivation d'`api_url`,
les cas d'authentification, et **vérifient qu'aucune valeur ni clé en clair ne subsiste dans
le JSON généré**.

---

## 3. Vérification du schéma SQL (fait le 10/08/2026)

Base `kea` sur 192.168.1.2, **`schema_version` = 30.0**, cohérente avec Kea 3.2.0 en service.

Contrôle de tout ce dans quoi DHCPMan écrit directement :

| Objet | Attendu par l'appli | Constaté | Verdict |
|-------|--------------------|----------|---------|
| `createAuditRevisionDHCP4` | 4 args (ts, server_tag, message, cascade) | `audit_ts`, `server_tag`, `audit_log_message`, `cascade_transaction` | inchangé |
| `hosts` NOT NULL sans défaut | `dhcp_identifier`, `dhcp_identifier_type` | idem (+ `host_id` auto) | OK |
| `dhcp4_pool` NOT NULL sans défaut | start/end_address, subnet_id, modification_ts | idem | OK |
| `dhcp4_subnet` NOT NULL sans défaut | subnet_id, subnet_prefix, modification_ts | idem | OK |
| `dhcp4_options` NOT NULL sans défaut | code, scope_id | idem | OK |
| `dhcp4_client_class` | name, valid_lifetime, modification_ts | présentes | OK |
| `dhcp4_subnet.relay` / `client_classes` | longtext nullable | idem | OK |

**Conclusion : aucune migration applicative requise.** Les `INSERT` de `Subnet::syncToKea()`
nomment leurs colonnes explicitement, donc d'éventuelles nouvelles colonnes nullables sont sans
effet. Correction de doc au passage : `hosts.dhcp_identifier` est `VARBINARY(255)`, pas 128.

Commande pour refaire ce contrôle après une montée de version :

```bash
mysql -h 192.168.1.2 -u claude -pclaudedev123 kea -e "SELECT * FROM schema_version;"
mysql -h 192.168.1.2 -u claude -pclaudedev123 kea -e "
  SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE
  FROM information_schema.COLUMNS
  WHERE TABLE_SCHEMA='kea' AND IS_NULLABLE='NO' AND COLUMN_DEFAULT IS NULL
    AND TABLE_NAME IN ('hosts','dhcp4_pool','dhcp4_subnet','dhcp4_options')
  ORDER BY TABLE_NAME, COLUMN_NAME;"
```

---

## 4. Reste à faire sur le lab FreeBSD

- [ ] **Déployer `c147aa3`** (`client-classes`) : copier `kea-sync-from-db.py` vers
      `/usr/local/sbin/`, `service kea_sync_from_db restart`, puis une synchro depuis l'appli.
      Vérifier ensuite que `kea-dhcp4 -t … 2>&1 | grep -i deprecated` ne renvoie rien.
      *Déprécation seulement — aucune urgence.*
- [ ] `rm /usr/local/etc/rc.d/kea-sync-from-db` — doublon à tirets, ancienne copie
      (pointe sur python3.11, absent). Diff vérifié : aucune modification locale à préserver.
      Les deux fichiers lisent le même `kea_sync_from_db_enable`, donc les deux démarrent au boot.
- [ ] `mv /usr/local/etc/kea/kea-ctrl-agent.conf /root/` — résidu, plus rien ne le lit.

Écarté explicitement par l'utilisateur : rotation du mot de passe de l'API (lab privé).

---

## 5. Anticipation : la prod AlmaLinux

**Elle n'a pas encore été mise à jour.** Le jour où elle passe en Kea ≥ 3.1.8, elle tombera
exactement de la même façon — mais le correctif **ne sera pas le même**, et c'est le piège.

### La différence structurante

| | FreeBSD (lab) | AlmaLinux (prod) |
|---|---|---|
| `kea-dhcp4.conf` | **généré** par `kea-sync-from-db.py` | **maintenu à la main** (les subnets viennent du Config Backend MySQL) |
| Correctif control-sockets | déjà fait, dans le générateur | **édition manuelle du fichier**, aucun code ne le produira |

Autrement dit : le travail livré ici ne protège **pas** la prod. Il faudra y ajouter le bloc
`control-sockets` à la main dans `/etc/kea/kea-dhcp4.conf`.

### Checklist prod (à dérouler le jour J)

1. **Avant la mise à jour**, relever la version Kea et le schéma :
   `kea-dhcp4 -V` et `SELECT * FROM schema_version;`
2. Sauvegarder `/etc/kea/kea-dhcp4.conf` et `/etc/kea/kea-ctrl-agent.conf`.
3. Mettre à jour, puis **`kea-admin db-upgrade`** si le schéma a bougé, et refaire le contrôle
   des colonnes du §3.
4. Constater la disparition de `kea-ctrl-agent` : `ls /usr/sbin/kea-ctrl-agent`,
   `systemctl status kea-ctrl-agent` (unité qui disparaît du paquet).
5. **Ajouter à la main** dans `/etc/kea/kea-dhcp4.conf`, dans l'objet `Dhcp4` :
   ```json
   "control-sockets": [
       { "socket-type": "unix", "socket-name": "/run/kea/kea4-ctrl-socket" },
       { "socket-type": "http", "socket-address": "<hôte de kea_api_url>",
         "socket-port": <port de kea_api_url>,
         "authentication": { "type": "basic", "realm": "kea-dhcp4-server",
                             "directory": "/etc/kea",
                             "clients": [ { "user-file": "kea-api-user",
                                            "password-file": "kea-api-password" } ] } }
   ]
   ```
   avec `/etc/kea/kea-api-user` et `/etc/kea/kea-api-password` en **0600**, contenant les
   valeurs de `app_settings.kea_api_user` / `kea_api_password`, **sans saut de ligne final**.
   Reprendre `http-host`/`http-port` de l'ancien `kea-ctrl-agent.conf` pour l'adresse et le port.
6. **`kea-dhcp4 -t /etc/kea/kea-dhcp4.conf` avant tout redémarrage.** Kea refuse ses champs un
   par un : prévoir plusieurs itérations. Tant qu'on ne redémarre pas, le service tourne sur sa
   config en mémoire.
7. `systemctl restart kea-dhcp4`, puis `ss -lntp | grep <port>` — doit montrer **kea-dhcp4**.
8. Vérifier que l'IP d'écoute est bien portée localement (`ip a`) : `-t` valide la syntaxe,
   **pas** la capacité à binder.
9. **SELinux** : `httpd_can_network_connect` reste requis pour qu'Apache joigne l'API
   (déjà documenté dans CLAUDE.md). Si l'écoute change d'adresse ou de port, revalider
   firewalld également.
10. Tests fonctionnels : Paramètres → Tester la synchronisation, **et** page Leases (c'est le
    seul chemin qui utilise les identifiants d'`app_settings` plutôt que ceux du serveur).

### Ce qui ne concerne PAS la prod

- La déprécation `client-class` : le Config Backend écrit dans la colonne
  `dhcp4_pool.client_classes`, déjà au pluriel.
- Le passage python3.12 : `kea-sync-from-db.py` ne tourne qu'en mode FreeBSD.

### Piste d'amélioration à considérer

L'onglet **Debug API Kea** de `settings.php` pourrait détecter un refus de connexion et afficher
explicitement « Control Agent supprimé depuis Kea 3.1.8 — voir la section Canal de contrôle Kea ».
Ça transformerait 45 minutes d'enquête en un message. À arbitrer à la reprise.

---

## 6. Leçons à conserver

- **`kea-dhcp4 -t` est le garde-fou.** Trois refus successifs encaissés sans jamais interrompre
  le service. Ne jamais redémarrer Kea sans un `-t` vert.
- **Kea signale ses violations un champ à la fois** — prévoir des allers-retours, pas un
  correctif unique.
- **`-t` ne teste que la syntaxe**, pas le bind : vérifier séparément que l'adresse d'écoute
  est locale.
- **La dérivation d'`api_url`** (au lieu d'un réglage séparé pour l'écoute) supprime par
  construction toute possibilité de désaccord entre appelant et écouteur.
- Une **API muette dégrade en silence** : `findActiveLeases()` renvoyant `[]` supprime les
  avertissements sans rien signaler. À garder en tête pour les futurs garde-fous.
