← Retour au site Chargement…

rescriptum

Écrire des réponses

Guide

Généré le 30 août 2026

Écrire des réponses

Écrire des réponses

Une réponse est le document que reçoit un installateur : un answer.toml Proxmox, un user-data d’autoinstall Ubuntu, un kickstart, un preseed, une configuration Ignition, un profil AutoYaST, un unattend.xml Windows. Tout le travail de rescriptum consiste à choisir le bon pour la machine qui demande et à le lui remettre, assemblé à partir du nombre de couches que vous avez écrites.

L’agencement

Un répertoire par identité. Une machine est un répertoire nommé d’après elle, qui contient un document par système d’exploitation :

answers/
├── 98fa9b50d810/           une machine
│   ├── proxmox.toml            en tant que Proxmox
│   └── debian.preseed          …et le même matériel en tant que Debian
├── aabbccddeeff/           une autre machine
│   └── ubuntu.yaml             en tant qu'Ubuntu
├── default/                quand rien d'autre ne correspond
│   └── proxmox.toml
└── groups/
    ├── rack-a/             partagé par une baie, revendique ses membres
    │   ├── proxmox.toml
    │   └── debian.preseed
    └── rhel-compute/       revendique des machines pour ce qu'elles sont
        └── rhel.ks
  • Une machine est un répertoire nommé d’après elle — une adresse MAC, dans n’importe quel style de séparateur — qui porte la configuration de cette machine, ou seulement la part qui diffère de son groupe.
  • Un groupe est un répertoire sous groups/ et il est partagé. Il revendique des machines en les listant dans members, ou par un bloc match testé contre la requête.
  • default/ répond quand rien d’autre ne le fait. Un document par format : un défaut TOML ne doit pas répondre à un client qui a demandé du kickstart.

L’extension décide ; le nom, non

Dans un répertoire, l’extension est le format et ce qui précède ne veut rien dire. proxmox.toml et answer.toml sont le même document pour le serveur ; le nom est là pour qui ouvre le dossier. rescriptum en écrit des lisibles — proxmox.toml, ubuntu.yaml, debian.preseed, boot.ipxe — et ne renomme jamais les vôtres.

La seule règle qui en découle : un répertoire contient au plus un document par format. Deux .toml dans un même répertoire est signalé comme un problème plutôt que tranché, parce que rien ne pourrait choisir entre eux d’une façon que vous auriez prévue. Deux formats différents ne sont pas un doublon — c’est justement l’intérêt du répertoire.

L’organisation du stockage et l’URL restent délibérément séparées : un dossier peut être réorganisé, une URL gravée dans une ISO non. Voir formats.

:::note[Migration depuis un répertoire plat] Les réponses étaient des fichiers à la racine du répertoire : 98fa9b50d810.toml à côté de 98fa9b50d810.preseed. Ils ne sont plus servis, et chacun est signalé par son nom avec son nouveau chemin. rescriptum migrate montre ce qu’il déplacerait ; rescriptum migrate --apply les déplace. :::

Les cinq choses à savoir

Comment une réponse est choisiePar nom, par liste de membres, ou par ce que la machine est. Nommer gagne toujours ; entre sélecteurs, plus de critères gagne ; les égalités se départagent sur le nom trié
Un document par système d’exploitationL’extension est le format, l’endpoint choisit entre eux, et une machine peut exister en plusieurs systèmes à la fois
Groupes et fusionLes couches s’appliquent de la plus basse à la plus haute et la machine gagne toujours. Les maps fusionnent ; les tableaux remplacent
Templating{{ serial }} rempli depuis la requête, pour qu’un fichier de groupe couvre une baie
Validationrender montre ce qu’une machine recevrait ; check rend tout et signale ce qui casse

Les clés de contrôle

Trois clés pilotent la résolution et sont retirées avant que la réponse ne soit envoyée, donc l’installateur ne les voit jamais :

CléRôle
membersles machines pour lesquelles ce groupe répond
matchdes critères testés contre les faits de la requête
extendsle groupe au-dessus duquel ce document se superpose

Elles voyagent dans ce que chaque format permet — clés de premier niveau en TOML, YAML et JSON, un élément <answer-meta> en XML, des directives # answer: en kickstart et preseed. L’écriture par format est dans formats.

Exemples travaillés

Le répertoire examples/ du dépôt contient un exemple commenté de chaque format supporté, tous sélectionnés différemment — par matériel, par liste de membres, par nom de répertoire — et ils sont exercés par la suite de tests :

$ RESCRIPTUM_ANSWERS_DIR=examples rescriptum check
$ RESCRIPTUM_ANSWERS_DIR=examples rescriptum render --query "path=/rhel/ks&serial=7ABC123"

C’est le seul endroit où les formats sont montrés en train de se composer ensemble ; commencez par là si vous ne savez pas à quoi ressemble un vrai fichier.

Comment une réponse est choisie

Comment une réponse est choisie

Un document n’est pas cherché ; il revendique la requête. Trois façons de le faire, ordonnées par la finesse avec laquelle elles visent une machine.

1. Par le nom

Nommez un répertoire d’après l’adresse MAC de la machine, et mettez-y ses documents :

answers/
├── 98-fa-9b-50-d8-10/
│   └── proxmox.toml
├── aabbccddeeff/
│   └── proxmox.toml
└── default/
    └── proxmox.toml

Quand une requête arrive, le serveur passe en minuscules tout ce qu’elle porte et retire chaque caractère non alphanumérique, fait de même avec le nom de chaque répertoire, et sert le premier dont le nom apparaît à l’intérieur de la requête. Ainsi 98-fa-9b-50-d8-10, 98:fa:9b:50:d8:10 et 98fa9b50d810 nomment tous la même machine — vous n’avez jamais à vous soucier du style de séparateur que Proxmox utilise cette version-ci, ni de la façon dont il structure son JSON.

L’identité est le nom du répertoire ; les noms de fichiers qu’il contient ne choisissent rien, ils ne portent que le format dans leur extension.

Cette normalisation est toute l’astuce, et c’est pourquoi cela survit à un changement de format de corps entre versions de Proxmox : c’est un test de sous-chaîne sur des octets, pas un schéma.

Rien n’empêche de nommer un répertoire d’après un numéro de série, un code d’inventaire ou un nom d’hôte. N’importe quelle chaîne apparaissant dans ce que la machine envoie fera l’affaire.

2. Par liste de membres

Un groupe revendique un ensemble de machines en les listant :

# answers/groups/rack-a/proxmox.toml
members = ["98:fa:9b:50:d8:10", "98:fa:9b:50:d8:11", "98:fa:9b:50:d8:12"]

Les chaînes de members sont normalisées exactement comme les noms de répertoires, donc le style de séparateur n’a pas d’importance ici non plus. Une machine listée n’a besoin d’aucun répertoire propre à moins d’avoir quelque chose à surcharger — voir groupes.

3. Par ce que la machine est

Un bloc match revendique une machine par ses propriétés plutôt que par son identité :

# answers/groups/dell-r620/proxmox.toml
[match]
manufacturer = "Dell Inc."
product      = "PowerEdge R620"
serial       = "7ABC*"          # * et ? fonctionnent

Tous les critères doivent tenir pour que le groupe revendique la requête. * correspond à n’importe quelle suite de caractères et ? à exactement un ; les deux côtés sont normalisés avant comparaison, donc la casse et le style de séparateur n’ont jamais d’importance.

Un document machine peut porter un bloc match lui aussi — utile pour « quelle que soit la machine actuellement dans cet emplacement de châssis ».

Les faits qu’un sélecteur peut tester

Les faits viennent de trois endroits, délibérément étagés du plus structuré au moins structuré.

Les paramètres de query

?mac=…&uuid=…&serial=… — la façon dont tout installateur autre que Proxmox s’identifie, parce qu’iPXE substitue les valeurs dans l’URL avant de la chercher. Fiable, clés arbitraires, aucune devinette.

Trois autres sont synthétisés depuis l’URL elle-même :

FaitVaut
pathle chemin entier, débarrassé de ses slashes — rhel/ks
fileson dernier segment — ks. C’est ce qui distingue le user-data de cloud-init de son meta-data
segmentchaque segment, comme valeurs séparées — rhel et ks

Un corps JSON POSTé

Quand le corps est réellement du JSON, il est aplati en à la fois ses chemins pointés complets et ses noms de feuilles nus :

{ "dmi": { "system": { "serial": "7ABC123" } } }

donne à la fois dmi.system.serial et serial tout court. La forme feuille est l’essentiel. La documentation de Proxmox avertit elle-même que le contenu de dmi « peut varier énormément selon le système », donc un sélecteur disant « un champ nommé serial, où qu’il se trouve » survit à une réorganisation qu’un chemin figé ne supporterait pas.

Les indices de tableau font partie du chemin mais pas du nom de feuille, donc network_interfaces.0.mac est aussi atteignable par mac tout court.

Un corps qui n’est pas du JSON n’est pas une erreur — il n’apporte simplement rien d’autre que la botte de foin.

Le corps brut

Normalisé en alphanumériques minuscules : la botte de foin de sous-chaînes qui fait fonctionner la correspondance par nom. Les valeurs de query et les segments de chemin y sont aussi ajoutés, donc un répertoire nommé d’après une MAC résout que la MAC soit arrivée dans un corps POST ou dans une query string. Sans cela, un GET — qui n’a aucun corps — ne pourrait jamais correspondre par nom.

Quand plusieurs documents revendiquent la même requête

La règle est fixe, et un test l’épingle :

  1. Nommer une machine gagne toujours. Quel que soit le nombre de critères d’un sélecteur, une correspondance d’identité le bat — nommer une machine est la chose la plus précise qu’on puisse faire.
  2. Entre sélecteurs, plus de critères gagne. Trois critères satisfaits battent deux ; une règle plus délibérée est une règle plus spécifique.
  3. Les égalités se départagent sur le nom trié. Le premier par ordre alphabétique.

La réponse ne dépend jamais de l’ordre du système de fichiers ni de l’ordre dans lequel les lignes sont sorties d’une base. matchbox, l’antécédent le plus proche, documente que sa propre résolution entre groupes concurrents « ne sera pas déterministe ». Celle-ci l’est.

Seul le premier groupe correspondant s’applique. La composition s’exprime avec extends, pas en fusionnant tous les groupes qui correspondent — l’ordre entre plusieurs groupes correspondants serait arbitraire, et un ordre arbitraire est la façon dont une machine reçoit discrètement le mauvais schéma de disques.

Et si rien ne correspond

default.<ext> est servi, s’il en existe un pour le format demandé par l’endpoint. Sinon la réponse est 404 — journalisée en no answer file applies.

Essayer avant de démarrer quoi que ce soit

render résout exactement comme le ferait le serveur, à partir des faits que vous fournissez :

$ rescriptum render 98:fa:9b:50:d8:10                              # par identité
$ rescriptum render --query "serial=7ABC123&mac=98:fa:9b:50:d8:10" # par étiquette
$ rescriptum render --query "path=/rhel/ks&serial=7ABC123"         # endpoint compris
$ rescriptum render --body captured-request.json                   # un vrai corps capturé

Un identifiant nu ne prétend rien sur quel genre d’identifiant il est — il remplit la botte de foin et rien d’autre. C’est assez pour la correspondance par nom, mais un sélecteur sur serial a besoin de --query "serial=…" pour avoir quelque chose à tester. check fonctionne pareil, ce qui est pourquoi un template ayant besoin d’un fait propre à la requête est signalé comme problème.

Pour capturer ce que vos machines envoient réellement, voir Capturer les requêtes.

Un document par système d'exploitation

Un document par système d’exploitation

Un installateur qui va chercher une URL attend une chose bien précise en retour. Un client kickstart veut du kickstart et s’étranglerait avec du TOML. C’est le protocole, pas une convention que quelqu’un aurait choisie. Donc :

  • l’endpoint déclare le format/rhel/ks demande du kickstart ;
  • le document le porte comme extensiongroups/rhel-compute/rhel.ks ;
  • seuls les documents de ce format peuvent répondre.

La conséquence qui fait comprendre

La réponse d’une machine est spécifique au système d’exploitation auquel elle est destinée. Donc ceci n’est pas une machine et deux fichiers :

answers/
└── 98fa9b50d810/
    ├── proxmox.toml        « cette machine, en tant que Proxmox »
    └── debian.preseed      « cette machine, en tant que Debian »

C’est un même matériel avec deux réponses, et les deux peuvent exister en même temps. Celle qu’une requête reçoit dépend de l’URL sur laquelle elle est arrivée — /proxmox/answer obtient le TOML, /debian/preseed obtient le preseed. Aucune n’est plus « la » réponse que l’autre.

En interne, c’est pourquoi un document est indexé par (identifiant, format) plutôt que par identifiant seul — et pourquoi un répertoire contient un document par format, pas davantage.

Le stockage n’est pas l’URL

Les documents sont regroupés par identité, jamais par format, et c’est délibéré. Répertoires et lignes de base sont un espace de recherche — ils doivent rester libres d’être réorganisés. Une URL est un contrat public gravé dans une ISO — elle ne doit pas bouger parce que quelqu’un a renommé un dossier. Une conception antérieure faisait du nom de répertoire le segment d’URL et a été écartée pour exactement cette raison.

La même règle explique pourquoi le nom de fichier dans le répertoire d’une machine ne porte aucun sens : proxmox.toml se lit bien et fait écho à l’endpoint /proxmox/, mais seul le .toml est porteur. Renommez-le answer.toml et rien ne change.

Quel alias sert quelle extension est dans la référence des formats ; comment en choisir un pour votre média est dans préparer les médias d’installation.

Les formats

ExtensionPourSuperposition
tomlProxmox VEfusion structurelle
yaml, ymlautoinstall Ubuntu, cloud-initfusion structurelle
json, ignIgnition, Flatcar, Fedora CoreOSfusion structurelle
xml, autoyast, unattendAutoYaST, unattend.xml Windowsfusion structurelle, par élément
kskickstart — RHEL, CentOS, Fedora, Alma, Rockyconcaténation
preseed, seedpreseed Debianconcaténation
cfg, ipxescripts de boot et autres configurations en lignesconcaténation

La liste blanche est délibérée : txt n’y est pas, pour qu’un fichier de notes égaré à côté de vos réponses ne devienne jamais un candidat.

.autoyast et .unattend sont du XML sous un nom qui dit lequel, pour qu’un store contenant à la fois un profil SUSE et un unattend Windows puisse les distinguer. Le .xml simple répond encore aux deux, ce qui va très bien jusqu’au jour où vous avez les deux.

Fusion structurelle

Pour toml, yaml, json et xml, la superposition est une vraie fusion :

  • Les maps fusionnent clé par clé, récursivement — y compris les tables inline et pointées de TOML.
  • Toute autre valeur est remplacée intégralement par la couche supérieure.
  • Les tableaux remplacent, ils ne concatènent pas. Concaténer rendrait une liste impossible à raccourcir depuis une couche supérieure, et « ce nœud a deux disques, pas quatre » doit rester exprimable.

Les détails, avec exemples, sont dans groupes.

Concaténation

Pour ks, preseed, cfg, seed et ipxe, la superposition est une concaténation dans l’ordre des couches, et le module le dit plutôt que de prétendre le contraire. Une directive d’une couche supérieure suit celle d’une couche inférieure au lieu de la supprimer.

Savoir si cela équivaut à une surcharge est l’affaire du format cible : la dernière réponse gagne en preseed, ce n’est pas toujours le cas en kickstart. Rendez le résultat et lisez-le avant de confier une baie à cela.

Une chose à savoir avant d’écrire une dissertation en tête d’un kickstart : les commentaires ordinaires sont servis. Seules les lignes de directive # answer: sont retirées. C’est très bien — kickstart et preseed autorisent tous deux les commentaires — mais l’installateur verra tout le reste.

XML

XML apparie les frères par nom d’élément plus un attribut discriminantname, id, key, alias ou pass. C’est ce qui rend

<settings pass="specialize">
  <component name="Microsoft-Windows-Shell-Setup" …>

fusionnable : surcharger une pass laisse les autres tranquilles, et surcharger un component ne remplace pas tous les autres composants du fichier. Des frères répétés sans attribut discriminant sont traités comme une liste, et le config:type="list" d’AutoYaST est respecté.

Ce qui survit à une fusion : la déclaration <?xml?>, le <!DOCTYPE>, les espaces de noms et les attributs. Ce qui n’y survit pas : l’indentation d’origine et le placement des commentaires — la sortie est re-rendue, pas rustinée.

Il ne comprend aucun schéma. Rendez et vérifiez avant de lui confier une baie.

Où vivent les clés de contrôle

Les clés de contrôle voyagent dans ce que chaque format permet, et sont retirées avant que la réponse ne soit envoyée.

TOML

extends = "base"
members = ["98:fa:9b:50:d8:10"]

[match]
product = "PowerEdge R6*"

YAML / JSON — les mêmes trois, en clés de premier niveau :

extends: base
members: ["98:fa:9b:50:d8:10"]
match:
  file: "user-data"
  product: "PowerEdge R6*"

XML — un élément <answer-meta>, avec extends en attribut dessus :

<answer-meta extends="base">
  <member>52:54:00:11:22:33</member>
  <match manufacturer="Dell Inc." product="PowerEdge R6*" />
</answer-meta>

Kickstart, preseed et tout ce qui est en lignes — des directives # answer: (// fonctionne aussi, pour les formats qui commentent ainsi) :

# answer: extends base
# answer: member 00:11:22:33:44:55, 00:11:22:33:44:56
# answer: match serial=7ABC* product=PowerEdge*

match prend des paires clé=motif séparées par des espaces, member une liste séparée par des virgules.

Une réponse, un format

Toutes les couches d’une même réponse doivent être du même format. Un document machine YAML au-dessus d’un groupe TOML est refusé, pas servi à moitié, et extends se résout à l’intérieur d’un format pour la même raison — superposer un preseed sur une base TOML n’a aucun sens.

Le groupement n’est par ailleurs pas affecté par tout cela : une baie partage un groupe par format, et une machine qui existe en deux systèmes d’exploitation rejoint deux d’entre eux.

default suit la même règle — un .toml dans default/ répond à une requête qui a demandé du TOML, et jamais à une qui a demandé du kickstart.

Groupes et fusion

Groupes et fusion

Une baie de machines partage d’habitude tout sauf ses adresses MAC. L’écrire une fois par machine est la façon dont la configuration d’un parc dérive. Donc les réponses se composent.

answers/
├── groups/
│   ├── base/
│   │   └── proxmox.toml       partagé par tout
│   └── rack-a/
│       └── proxmox.toml       extends = "base" ; members = [ … ]
├── 98-fa-9b-50-d8-10/
│   └── proxmox.toml           les surcharges d'une machine (optionnel)
└── default/
    └── proxmox.toml           seulement quand rien d'autre ne correspond

La partie partagée

# answers/groups/rack-a/proxmox.toml
members = [
  "98:fa:9b:50:d8:10",
  "98:fa:9b:50:d8:11",
  "98:fa:9b:50:d8:12",
]

[global]
keyboard = "fr"
country  = "fr"
timezone = "Europe/Paris"

[disk-setup]
filesystem = "zfs"
zfs.raid   = "raid1"
disk-list  = ["sda", "sdb"]

La différence

Une machine qui diffère reçoit un document contenant seulement la différence :

# answers/98-fa-9b-50-d8-10/proxmox.toml
[global]
fqdn = "node01.example.com"

[disk-setup]
zfs.raid  = "raid10"                       # celle-ci a quatre disques
disk-list = ["sda", "sdb", "sdc", "sdd"]

…et reçoit les deux fusionnés, ses propres valeurs l’emportant :

$ rescriptum render 98:fa:9b:50:d8:10
# format=toml machine=98-fa-9b-50-d8-10 group=rack-a

[global]
keyboard = "fr"
country = "fr"
timezone = "Europe/Paris"
fqdn = "node01.example.com"

[disk-setup]
filesystem = "zfs"
zfs.raid = "raid10"
disk-list = ["sda", "sdb", "sdc", "sdd"]

Règles de fusion

Couchesla chaîne de groupes d’abord, le document machine en dernier — la machine gagne toujours
Mapsfusionnent récursivement, y compris les tables inline et pointées de TOML
Autres valeursremplacées intégralement par la couche supérieure
Tableauxremplacent, ils ne concatènent pas
Formats texteconcaténés dans l’ordre des couches — voir formats

Pourquoi les tableaux remplacent. Concaténer est le choix intuitif jusqu’au moment où il faut raccourcir une liste. disk-list = ["sda", "sdb"] dans un groupe et ["sda"] dans un document machine n’a qu’un seul sens raisonnable — celle-ci n’a qu’un disque — et la concaténation ne peut pas l’exprimer. La règle vaut dans tous les formats, pour que vous n’ayez jamais à vous rappeler dans lequel vous êtes.

extends

Un groupe peut en étendre un autre, ce qui donne une chaîne — ce que toutes les baies partagent dans un fichier, les différences par baie dans un autre :

# answers/groups/base/proxmox.toml
[global]
mailto   = "ops@example.com"
timezone = "Europe/Paris"
root-ssh-keys = ["ssh-ed25519 AAAA…REPLACE ops@example.com"]
# answers/groups/rack-a/proxmox.toml
extends = "base"
members = ["98:fa:9b:50:d8:10", "98:fa:9b:50:d8:11"]

[disk-setup]
filesystem = "zfs"

Les couches s’appliquent alors baserack-a → document machine.

extends dans un document machine l’emporte sur l’appartenance. C’est l’échappatoire pour une machine qui a besoin d’un groupe où elle n’est pas listée :

# answers/98-fa-9b-50-d8-99/proxmox.toml
extends = "rack-a"          # même si rack-a ne liste pas cette MAC

[global]
fqdn = "spare01.example.com"

extends se résout à l’intérieur d’un format — superposer un preseed sur une base TOML n’a aucun sens, et la fusion le refuserait de toute façon.

Seul le premier groupe correspondant s’applique

Si deux groupes revendiquent la même machine, un seul s’applique — le plus spécifique, les égalités départagées sur le nom trié. Composez avec extends plutôt que de compter sur plusieurs groupes correspondant à la fois : l’ordre entre eux serait arbitraire, et un ordre arbitraire est la façon dont une machine reçoit discrètement le mauvais schéma de disques.

Quand un groupe est cassé

Les cycles et les parents manquants sont détectés à la lecture du store, signalés une fois dans le log, et le groupe cassé est écarté plutôt qu’appliqué à moitié :

2026-08-24T08:43:36Z - warning: group "rack-a": extends unknown group "base"

Un groupe cassé n’empêche pas les autres baies de s’installer. Une machine qui avait besoin de ce groupe reçoit un 500 bruyant plutôt qu’une réponse à moitié construite — servir une configuration dont la base manque installerait la machine à moitié configurée, et personne ne s’en apercevrait avant qu’elle ne tourne.

rescriptum check signale les mêmes problèmes, ce qui est un meilleur endroit pour l’apprendre que le log à 3 h du matin.

Le groupement est le chemin rapide

Mesuré à 2 000 machines, 3 000 requêtes à 100 en concurrence :

AgencementDébit
2 000 documents machine, aucun groupe12 132 req/s
un groupe de 2 000 membres, aucun document machine13 036 req/s
2 000 documents machine plus un groupe (une fusion par requête)8 816 req/s

Un groupe sans surcharge machine et sans placeholder est rendu une fois, à la lecture du store, puis servi comme une chaîne préparée. Le cas courant en datacenter ne parse rien par requête. Ajouter une surcharge par machine coûte une fusion par requête — ça vaut le coup là où c’est nécessaire, et ça vaut le coup de l’éviter ailleurs.

L’autre moitié du même argument, c’est ce que coûte une lecture. Le store entier est relu au plus une fois par seconde, et avec un répertoire par identité cette lecture ajoute un readdir par machine au fichier qu’elle ouvrait déjà — mesuré à 2 000 machines sur un M1 Pro : 28 ms avant le changement d’agencement, 63 ms après. C’est amorti sur une seconde de requêtes dans les deux cas, et les débits ci-dessus n’ont pas bougé de façon mesurable ; mais un groupe qui dispense d’un répertoire par machine évite ce coût aussi.

Ensuite

  • Templating{{ serial }} supprime la dernière raison d’avoir un répertoire par machine.
  • Validation — une réponse fusionnée est un document que personne n’a écrit ; regardez-le avant qu’une baie ne le fasse.

Templating

Templating

Le groupement supprime la duplication entre machines d’accord. Le templating supprime la dernière raison d’écrire un document par machine : les valeurs qui doivent différer.

# answers/groups/rack-a/proxmox.toml
members = ["98:fa:9b:50:d8:10", "98:fa:9b:50:d8:11", "…"]

[global]
fqdn = "node-{{ serial }}.example.com"

[network]
filter.ID_NET_NAME_MAC = "*{{ mac }}"

Cinq cents machines, un document. Sans cela, un nom d’hôte par machine signifie un document par machine — et cinq cents documents qui diffèrent d’une ligne chacun.

Les placeholders fonctionnent dans tous les formats : TOML, YAML, JSON, XML, kickstart, preseed.

Dans les formats structurés, la substitution porte sur des valeurs de chaîne parsées — un commentaire n’est donc qu’un commentaire. Dans les formats en lignes (ks, preseed, cfg, ipxe, seed), le document est une chaîne opaque : un placeholder écrit dans un commentaire reste un placeholder et doit quand même se résoudre. Mentionner {{ serial }} dans une ligne # pour l’expliquer au prochain lecteur fera échouer le rendu exactement comme un vrai.

Ce qu’on peut mettre dedans

PlaceholderRempli depuis
{{ mac }}, {{ serial }}, {{ uuid }}, …n’importe quel fait porté par la requête — paramètres de query, et champs d’un corps JSON POSTé par nom de feuille ou chemin complet
{{ dmi.system.serial }}le même corps, par son chemin exact
{{ path }}, {{ file }}, {{ segment }}l’URL sur laquelle la requête est arrivée
{{ group }}le nom du groupe qui s’est appliqué
{{ machine }}l’identifiant du document machine qui a matché

Les espaces à l’intérieur des accolades sont optionnels : {{serial}} et {{ serial }} sont identiques.

machine exige un document machine

{{ machine }} est l’identifiant du document machine qui a matché — il n’est donc disponible que lorsque la machine a un document à elle. Une machine revendiquée par la liste members d’un groupe, sans répertoire à elle, n’a pas de valeur machine et le rendu échoue avec template needs {{ machine }}, but this request carries no "machine".

Dans un groupe, utilisez plutôt un fait de la requête :

[global]
fqdn = "node-{{ mac }}.example.com"      # fonctionne pour chaque membre

{{ machine }} sert à un document machine qui veut se nommer sans répéter sa propre MAC.

Une valeur manquante est une erreur

Un placeholder que la requête ne peut pas remplir est un 500 avec la raison, jamais une chaîne vide :

$ rescriptum render 98:fa:9b:50:d8:10
error: template needs {{ serial }}, but this request carries no "serial"

C’est délibéré. Servir node-.example.com installe une machine avec un nom d’hôte cassé et personne ne le remarque avant plus tard — possiblement bien plus tard, sur une machine déjà en production. Faire échouer l’installation est le résultat le moins coûteux.

Les caractères de contrôle sont refusés pour la même classe de raison : un saut de ligne dans une valeur kickstart injecterait une directive dans le fichier que l’installateur exécute.

$ rescriptum render --query "mac=aa:bb&serial=$(printf 'a\nb')"
error: value for "serial" contains a control character and will not be substituted

La substitution est sûre vis-à-vis de l’échappement

La substitution se fait sur des valeurs parsées, jamais sur le texte brut du document. La valeur est placée dans le modèle de données du document et le sérialiseur du format l’écrit — donc c’est le sérialiseur qui fait l’échappement.

Un numéro de série contenant un guillemet ne peut pas casser le TOML dans lequel il atterrit :

$ rescriptum render --query 'mac=aa:bb&serial=a"b'"'"'c<d>e'
[global]
fqdn = """node-a"b'c<d>e.example.com"""

L’écrivain TOML a choisi de lui-même une chaîne multi-ligne. La même valeur dans un document XML revient échappée en entités, et en JSON, échappée en JSON. Un test fait passer a"b'c<d>e&f dans les quatre formats structurés et reparse la sortie.

C’est pourquoi le templating peut sans risque être alimenté par une requête que contrôle une machine que vous n’avez jamais vue.

check et les faits propres à la requête

rescriptum check rend chaque machine à partir de sa seule identité — il n’a aucune requête sur laquelle s’appuyer, puisqu’il n’y a pas de requête. Un template ayant besoin de serial, qui n’arrive jamais que dans un corps ou une query string, est donc signalé comme un problème :

$ rescriptum check
  FAIL group "rack-a" member "98fa9b50d811": template needs {{ serial }}, but this request carries no "serial"

C’est honnête — check ne peut réellement pas prouver que cette réponse se rend — mais c’est bruyant pour un jeu qui template délibérément sur des faits de requête. Vérifiez ceux-là avec render et des faits représentatifs :

$ rescriptum render --query "mac=98:fa:9b:50:d8:11&serial=7ABC123"

Le coût

Nul, quand vous ne l’utilisez pas. Un groupe dont la chaîne préparée ne contient pas {{ est servi tel quel, sans être parsé par requête — la vérification de présence de placeholders se fait une fois, à la lecture du store. Le templating ne fait basculer un groupe sur le chemin « fusion par requête » que pour les documents qui en contiennent réellement un.

Ensuite

Valider ce qui sera servi

Valider ce qui sera servi

Avant que les réponses ne se composent, un administrateur écrivait un document complet et le validait :

$ proxmox-auto-install-assistant validate-answer answer.toml

Une fois qu’une réponse est assemblée à partir d’une chaîne de groupes, plus un document machine, plus un remplissage de template, le document que reçoit l’installateur est un document que personne n’a jamais vu — et une mauvaise fusion se manifeste par une installation automatisée ratée à 3 h du matin. Deux sous-commandes existent pour combler ce manque, et tout changement de la fusion doit les garder fonctionnelles.

render — ce que cette machine recevrait

$ rescriptum render 98:fa:9b:50:d8:10                              # par identité
$ rescriptum render --query "serial=7ABC123&mac=98:fa:9b:50:d8:10" # par étiquette
$ rescriptum render --query "path=/rhel/ks&serial=7ABC123"         # endpoint compris
$ rescriptum render --body captured-request.json                   # un vrai corps capturé

Il résout exactement comme le serveur — même correspondance, même superposition, même remplissage de template — et affiche le résultat. Le document part sur stdout ; la ligne expliquant comment il a été obtenu part sur stderr :

$ rescriptum render 98:fa:9b:50:d8:10
# format=toml machine=98-fa-9b-50-d8-10 group=rack-a
[global]

donc une redirection ne vous donne que le document :

$ rescriptum render 98:fa:9b:50:d8:10 > /tmp/answer.toml

Ajoutez path=… à --query quand vous voulez vérifier ce qu’un endpoint particulier répondrait — sans cela, la résolution n’est pas contrainte par le format et peut choisir un document que la vraie URL aurait exclu.

Le code de sortie est 0 quand quelque chose s’est résolu, non nul quand rien ne s’appliquait (le serveur aurait renvoyé un 404) ou quand le rendu a échoué.

check — tout rendre, signaler ce qui casse

$ rescriptum check
checking files:examples
  10 group(s), 8 machine document(s)
  group "rhel-compute" selects on serial=7ABC*
    (verify with: rescriptum render --query "...")
  group "ubuntu-web" selects on file=user-data product=PowerEdge R6*
    (verify with: rescriptum render --query "...")
  1 answer(s) validated by their installer's own tool
  note: no schema validator exists for preseed answers
  note: toml answers not schema-checked — proxmox-auto-install-assistant is not on PATH
  ok — everything renders

Well-formed and merging cleanly is not the same as valid for an
installer. Where a validator exists and is installed it was used above;
install proxmox-auto-install-assistant, xmllint or ksvalidator for the rest.

Ce qu’il fait :

  • Signale les problèmes de chargement — un groupe qui en étend un inexistant, un cycle entre groupes, un document qui ne parse pas.
  • Rend chaque document machine, et chaque membre de chaque groupe. C’est ce qui exerce réellement la fusion.
  • Nomme les groupes qui sélectionnent sur un bloc match et dit qu’il n’a pas pu les essayer, plutôt que de laisser croire qu’ils ont été vérifiés — un sélecteur a besoin d’une vraie requête.
  • Signale un groupe sans members ni match comme atteignable seulement via extends, au cas où ce ne serait pas l’intention.
  • Appelle le validateur de l’installateur là où il existe et est dans le PATH, et dit quels formats il n’a pas pu vérifier.

Le code de sortie est 0 quand tout se rend, 1 dès que quelque chose a échoué — il tombe donc directement dans une CI.

Les validateurs qu’il connaît

FormatOutil
tomlproxmox-auto-install-assistant validate-answer
xml, autoyast, unattendxmllint --noout
ksksvalidator
yaml, json, ign, preseed, cfg, ipxeaucun n’existe — rendez et lisez

Un outil manquant est signalé une fois comme note, jamais traité comme un échec. Un vérificateur qui refuse de tourner sans outillage optionnel est un vérificateur que personne ne lance.

check n’est pas lui-même un vérificateur de schéma : il prouve que vos documents sont bien formés et fusionnent proprement. Pour tout ce dont il ne peut pas appeler un validateur, faites-le vous-même :

$ rescriptum render 98:fa:9b:50:d8:10 > /tmp/answer.toml
$ proxmox-auto-install-assistant validate-answer /tmp/answer.toml

Ce que check ne peut pas prouver

check rend chaque machine à partir de sa seule identité. Il n’a pas de requête, donc il ne peut pas fournir de faits qui n’arrivent qu’avec une requête — un serial depuis un corps POSTé, une mac depuis une query string. Un template en ayant besoin est signalé comme problème :

FAIL group "rack-a" member "98fa9b50d811": template needs {{ serial }}, but this request carries no "serial"

C’est exact — check ne peut réellement pas prouver que cette réponse se rend — mais cela signifie qu’un jeu templatisant délibérément sur des faits de requête ne reviendra pas propre. Vérifiez ceux-là avec render --query et des faits représentatifs. Voir templating.

En CI

Si vos réponses vivent dans git, cela mérite un job à part :

# .github/workflows/answers.yml
name: answers
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Get rescriptum
        run: |
          curl -fsSL https://github.com/z29k/rescriptum/releases/latest/download/rescriptum-x86_64-unknown-linux-musl.tar.gz \
            | tar xz --strip-components=1
      - run: RESCRIPTUM_ANSWERS_DIR=answers ./rescriptum check

Ajoutez proxmox-auto-install-assistant au runner et le même job vérifie aussi le schéma du TOML.

Avant de déployer

deploy.sh lance check avant d’expédier quoi que ce soit, et refuse de déployer si les réponses ne reviennent pas propres. Servir un jeu de réponses cassé est pire que ne pas déployer.