diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d914a6 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# pve-confsync + +Synchronisation d'une partie de la configuration d'un cluster Proxmox VE 9.x +« référence » vers N clusters « clients », via l'API PVE et des tokens API. +Sens unique référence → clients. Pas de VM, pas de disques. + +**État : phase 0 (CLI).** Export, diff et apply en YAML pour les options datacenter +et le SDN (zones VLAN/VXLAN, VNets, subnets). Testé contre un faux serveur PVE ; +**pas encore testé sur les clusters de labo.** + +## Périmètre + +| Type (`kind`) | API | Politique par défaut | +|---|---|---| +| `datacenter-options` | `/cluster/options` | `tag-style`, `registered-tags`, `user-tag-access`, `next-id` en `sync`, le reste ignoré ; `webauthn`, `u2f`, `http_proxy` jamais synchronisés | +| `sdn-zone` (vlan, vxlan) | `/cluster/sdn/zones` | `nodes`, `bridge`, `peers`, `fabric`, `dns`, `reversedns`, `dnszone` en `override` (mapping client) ; le reste en `sync` | +| `sdn-vnet` | `/cluster/sdn/vnets` | `sync` | +| `sdn-subnet` | `/cluster/sdn/vnets/{vnet}/subnets` | `sync` | + +Hors périmètre v1 : EVPN (et les zones simple, QinQ, faucet, avec leurs VNets, ignorées +avec un avertissement), firewall, storage, pools/ACL, jobs, secrets. + +Détail des routes, schémas et droits, avec les sources : [docs/api-pve.md](docs/api-pve.md). + +## Fonctionnement + +``` +lecture source -> normalisation -> diff -> plan -> apply -> vérification +``` + +- **Lecture** : la référence est lue en configuration SDN *appliquée* (`running=1`) ; les + clients en configuration courante. Chaque cluster est refusé si un nœud en ligne n'est + pas en PVE 9.x (`/nodes/{node}/version`). +- **Normalisation** : objets JSON génériques → champs canoniques (chaînes, listes triées, + property strings à clés triées). Les champs calculés par PVE sont retirés. +- **Politiques** : `sync` (copie de la référence), `ignore`, `override` (valeur fournie + uniquement par le `mapping` du client). Le mapping d'un client est prioritaire sur la + référence. Résolution : politique du client → politique globale → défaut de l'adaptateur. +- **Plan** : création / mise à jour (champs modifiés, champs à supprimer) ; suppression + d'objets uniquement avec `prune` (option explicite), dépendants d'abord. Erreurs bloquantes : + champ obligatoire absent à la création (ex. `bridge` d'une zone VLAN sans mapping), + champ immuable modifié (type de zone, vnet d'un subnet). +- **Apply SDN** : `POST /cluster/sdn/lock` → écritures avec `lock-token` → `PUT /cluster/sdn` + (commit + rechargement réseau, suivi de la tâche). Toute erreur avant le commit déclenche + `POST /cluster/sdn/rollback`. Le verrou est refusé si le client a déjà des changements SDN + en attente : on ne mélange pas nos modifications avec celles de quelqu'un d'autre. +- **Vérification** : relecture du client, nouveau plan qui doit être vide, et aucune zone en + `error` sur les nœuds (`/nodes/{node}/sdn/zones`). + +## Utilisation + +```sh +go build -o pve-confsync ./cmd/pve-confsync +cp examples/confsync.example.yaml confsync.yaml # puis adapter + +./pve-confsync check # accès, version 9.x, droits des tokens +./pve-confsync export -o ref.yaml # état normalisé de la référence (versionnable dans Git) +./pve-confsync diff # écarts par client ; code retour 2 s'il y en a +./pve-confsync diff -format json +./pve-confsync apply # dry-run (défaut) : affiche le plan, n'écrit rien +./pve-confsync apply -client site-a -confirm # applique ; SDN : saisir le nom du client +./pve-confsync apply -source ref.yaml -confirm -yes # depuis un export Git, sans question +``` + +Avec docker compose (images et utilisateur non privilégié alignés sur Logstream) : + +```sh +mkdir -p secrets && printf '%s' 'xxxxxxxx-...' > secrets/pve_ref +chmod 600 secrets/* && sudo chown 10001 secrets/* # lisibles par l'utilisateur du conteneur uniquement +docker compose run --rm confsync check +docker compose run --rm confsync apply -confirm +``` + +## Sécurité + +- Secrets des tokens : jamais dans le YAML (`token_secret` en clair est refusé) ; lus depuis + une variable d'environnement (`token_secret_env`) ou un fichier (`token_secret_file`, + secret docker). Ils ne sont ni affichés ni journalisés. Le chiffrement au repos arrive avec + le stockage SQLite (phase 1). +- TLS : `fingerprint` (pinning SHA-256 du certificat, adapté aux certificats auto-signés PVE), + `ca_file`, ou `insecure: true` réservé au labo. +- Tokens (privilege separation activée, l'ACL doit être posée sur l'utilisateur et le token) : + +```sh +# référence : lecture seule +pveum role add ConfSyncRO --privs "Sys.Audit SDN.Audit" +pveum user add confsync@pve +pveum user token add confsync@pve ro --privsep 1 +pveum acl modify / --users confsync@pve --roles ConfSyncRO +pveum acl modify / --tokens 'confsync@pve!ro' --roles ConfSyncRO + +# clients : Sys.Modify uniquement si les options datacenter sont synchronisées +pveum role add ConfSyncRW --privs "Sys.Audit Sys.Modify SDN.Audit SDN.Allocate" +``` + +`check` signale un token de référence capable d'écrire et un token client à qui il manque +un droit. + +## Arborescence + +``` +cmd/pve-confsync/ CLI (check, export, diff, apply) +internal/pve/ client API : token, TLS, form-encoding, version, tâches +internal/pve/pvetest/ faux serveur PVE 9.x pour les tests +internal/model/ objets génériques, snapshot YAML, valeurs canoniques +internal/adapter/ un adaptateur par type : routes, champs calculés, locaux, immuables +internal/config/ configuration YAML : clusters, politiques, mappings, sélection +internal/engine/ lecture, plan, apply (verrou SDN, rollback), vérification +docs/api-pve.md routes et schémas vérifiés dans les sources PVE +``` + +Tests : `go test ./...` (bout en bout contre le faux serveur : synchro complète, mapping +manquant, rollback sur erreur, refus de PVE 8, prune et ordre de suppression, forme +canonique des options datacenter, CLI dry-run/apply/export). + +## Phasage + +0. **CLI** export / diff / apply YAML (options datacenter, tags, SDN). ← ici +1. UI lecture seule : clusters, diff visuel par client. Ergonomie reprise de Logstream + (`web/index.html`, `style.css`, `app.js`, `login.html`, auth locale + OIDC, audit) ; + SQLite (clusters, profils, mappings, historique, audit), secrets chiffrés. +2. UI d'action : Dry-run / Synchroniser (par objet, par client, global), journal des exécutions. +3. Planification, détection de dérive en lecture seule, notifications. + +## Reste à faire avant la phase 1 + +- Valider sur les deux clusters de labo : forme réelle des réponses (`nodes`, `dhcp-range`, + booléens), comportement de `PUT /cluster/options` avec `delete`. +- Exploiter `GET /cluster/sdn/dry-run` (diff frr / interfaces, PVE ≥ 9.2 environ) pour + enrichir le dry-run SDN. +- Rechargement en échec après commit : aujourd'hui signalé, pas d'annulation automatique + (la configuration appliquée est déjà la nouvelle). diff --git a/docs/api-pve.md b/docs/api-pve.md new file mode 100644 index 0000000..8d35c32 --- /dev/null +++ b/docs/api-pve.md @@ -0,0 +1,85 @@ +# API Proxmox VE 9.x utilisée par pve-confsync + +Vérifiée le 2026-10-13 dans les sources Proxmox (miroirs GitHub officiels), faute d'accès +à l'API viewer (https://pve.proxmox.com/pve-docs/api-viewer/) depuis l'environnement de +développement. Versions lues : pve-manager 9.2.21, libpve-network-perl 1.6.7, pve-cluster 9.1.6. +**À recouper avec l'API viewer et les deux clusters de labo avant la phase 1.** + +Toutes les routes sont relatives à `/api2/json`. Authentification : en-tête +`Authorization: PVEAPIToken=USER@REALM!TOKENID=SECRET`. Écritures en +`application/x-www-form-urlencoded` (tableaux = paramètre répété), suppression de champs +via le paramètre `delete=a,b`. + +## Version et cluster + +| Méthode | Chemin | Usage | Droit | +|---|---|---|---| +| GET | `/cluster/status` | nœuds en ligne | Sys.Audit sur / | +| GET | `/nodes/{node}/version` | `version` (9.2.21), `release` (9.2) : refus si majeur ≠ 9 | — | +| GET | `/version` | repli pour un nœud isolé ([API2.pm](https://github.com/proxmox/pve-manager/blob/9c3f5e5258f73f31e273493232a79d615fd9e072/PVE/API2.pm#L86)) | — | +| GET | `/access/permissions` | droits effectifs du token (`check`) ([AccessControl.pm](https://github.com/proxmox/pve-access-control/blob/master/src/PVE/API2/AccessControl.pm#L510)) | — | +| GET | `/nodes/{node}/tasks/{upid}/status` | suivi de la tâche de rechargement SDN | — | + +## Options datacenter (`datacenter.cfg`) + +| Méthode | Chemin | Droit | +|---|---|---| +| GET | `/cluster/options` ([Cluster.pm](https://github.com/proxmox/pve-manager/blob/9c3f5e5258f73f31e273493232a79d615fd9e072/PVE/API2/Cluster.pm#L775)) | Sys.Audit sur / (sinon seuls console et tag-style) | +| PUT | `/cluster/options` ([Cluster.pm](https://github.com/proxmox/pve-manager/blob/9c3f5e5258f73f31e273493232a79d615fd9e072/PVE/API2/Cluster.pm#L830)) | Sys.Modify sur / | + +Points d'attention ([DataCenterConfig.pm](https://github.com/proxmox/pve-cluster/blob/7091d92e594952dba65c1e57568b3d7cc244e960/src/PVE/DataCenterConfig.pm#L560)) : + +- le GET renvoie des valeurs **déjà analysées** : `tag-style`, `next-id`, `user-tag-access`, + `migration`, `ha`, `crs`, `notify`, `webauthn`, `u2f` sous forme d'objets, + `registered-tags` et `user-tag-access.user-allow-list` sous forme de tableaux ; + le PUT attend des *property strings* (`shape=full,color-map=a:ff0000`, listes séparées par `;`). + L'adaptateur stocke la forme canonique « property string, clés triées ». +- le GET ajoute toujours `description` (vide possible), `mac_prefix` (`BC:24:11` par défaut, + [L11](https://github.com/proxmox/pve-cluster/blob/7091d92e594952dba65c1e57568b3d7cc244e960/src/PVE/DataCenterConfig.pm#L11)) et `allowed-tags` (lecture seule) : retirés à la normalisation. +- formats : `tag-style` [L286](https://github.com/proxmox/pve-cluster/blob/7091d92e594952dba65c1e57568b3d7cc244e960/src/PVE/DataCenterConfig.pm#L286), + `user-tag-access` [L321](https://github.com/proxmox/pve-cluster/blob/7091d92e594952dba65c1e57568b3d7cc244e960/src/PVE/DataCenterConfig.pm#L321), + `next-id` [L203](https://github.com/proxmox/pve-cluster/blob/7091d92e594952dba65c1e57568b3d7cc244e960/src/PVE/DataCenterConfig.pm#L203). + +## SDN ([SDN.pm](https://github.com/proxmox/pve-network/blob/ce388c5eec2376576d62a6be708439a64cf7fb62/src/PVE/API2/Network/SDN.pm)) + +| Méthode | Chemin | Usage | Droit | +|---|---|---|---| +| POST | `/cluster/sdn/lock` | verrou global, renvoie le `lock-token` ; refusé s'il existe des changements en attente (sauf `allow-pending=1`) | SDN.Allocate sur /sdn | +| DELETE | `/cluster/sdn/lock` | libère le verrou (`lock-token` ou `force=1`) | SDN.Allocate | +| POST | `/cluster/sdn/rollback` | annule les changements en attente (`lock-token`, `release-lock`) | SDN.Allocate | +| PUT | `/cluster/sdn` | commit + rechargement réseau de tous les nœuds, renvoie un UPID (`lock-token`, `release-lock`) | SDN.Allocate | +| GET | `/cluster/sdn/dry-run?node=` | diff frr / interfaces.d/sdn (libpve-network-perl ≥ 1.3.0, mars 2026) : non utilisé en phase 0 | Sys.Audit sur /nodes/{node} | +| GET | `/nodes/{node}/sdn/zones` | statut des zones par nœud (`available`, `pending`, `error`) | SDN.Audit | + +Le verrou et le paramètre `lock-token` sur toutes les écritures existent depuis +libpve-network-perl 1.1.3 (juillet 2025, avant PVE 9.0) : disponibles sur tout PVE 9.x. + +### Zones, VNets, subnets + +| Objet | Liste / création | Élément | Notes | +|---|---|---|---| +| zone | `/cluster/sdn/zones` | `/cluster/sdn/zones/{zone}` | GET d'un élément = SDN.**Allocate** : on lit donc la liste (SDN.Audit suffit) | +| vnet | `/cluster/sdn/vnets` | `/cluster/sdn/vnets/{vnet}` | | +| subnet | `/cluster/sdn/vnets/{vnet}/subnets` | `.../subnets/{zone}-{ip}-{masque}` | création : `subnet=`, `type=subnet` ; id calculé ([SubnetPlugin.pm](https://github.com/proxmox/pve-network/blob/ce388c5eec2376576d62a6be708439a64cf7fb62/src/PVE/Network/SDN/SubnetPlugin.pm#L42)) | + +Les GET acceptent `running=1` (configuration appliquée) ou `pending=1` ; sans paramètre, +la configuration courante, en attente comprise. La référence est lue en `running=1`, +les clients sans paramètre. + +Champs (schémas : [VlanPlugin.pm](https://github.com/proxmox/pve-network/blob/ce388c5eec2376576d62a6be708439a64cf7fb62/src/PVE/Network/SDN/Zones/VlanPlugin.pm), +[VxlanPlugin.pm](https://github.com/proxmox/pve-network/blob/ce388c5eec2376576d62a6be708439a64cf7fb62/src/PVE/Network/SDN/Zones/VxlanPlugin.pm), +[VnetPlugin.pm](https://github.com/proxmox/pve-network/blob/ce388c5eec2376576d62a6be708439a64cf7fb62/src/PVE/Network/SDN/VnetPlugin.pm), +[SubnetPlugin.pm](https://github.com/proxmox/pve-network/blob/ce388c5eec2376576d62a6be708439a64cf7fb62/src/PVE/Network/SDN/SubnetPlugin.pm)) : + +- zone vlan : `bridge` (obligatoire), `bridge-disable-mac-learning`, `nodes`, `mtu`, `dns`, `reversedns`, `dnszone`, `ipam` +- zone vxlan : `peers`, `vxlan-port` (4789), `fabric`, `nodes`, `mtu`, `dns`, `reversedns`, `dnszone`, `ipam` +- vnet : `zone` (obligatoire), `tag`, `alias`, `vlanaware`, `isolate-ports` +- subnet : `vnet`, `gateway`, `snat`, `dnszoneprefix`, `dhcp-range` (tableau), `dhcp-dns-server` +- calculés par PVE, ignorés : `digest`, `state`, `pending`, et pour les subnets `cidr`, `network`, `mask`, `zone` + +## Droits minimaux + +- **Référence** (lecture seule) : rôle avec `Sys.Audit` + `SDN.Audit` sur `/`. +- **Clients** : `Sys.Audit` + `SDN.Audit` + `SDN.Allocate` sur `/` (ou `/sdn`) ; + `Sys.Modify` sur `/` uniquement si les options datacenter sont synchronisées + (`PUT /cluster/options` n'a pas de droit plus fin).