rescriptum
Guide
an answer written for this machine
Ce qu'est rescriptum
Ce qu’est rescriptum
rescriptum — en droit romain, la réponse écrite d’une autorité à une question soulevée par un cas particulier. Vous exposiez votre situation ; vous receviez un document rédigé pour elle.
rescriptum sert la configuration d’installation qu’une machine réclame en s’installant, et la compose machine par machine à partir des couches que toute une baie partage. Un seul serveur répond à tous les installateurs que vous faites tourner. Servir un fichier est facile — décider lequel est tout le travail.
Le problème
Tout installateur automatisé va chercher sa configuration en HTTP, et chaque machine en a besoin d’une différente. L’URL est gravée dans le média, donc elle est identique sur chaque machine — un serveur de fichiers statique ne peut donc pas les servir. Les installateurs demandent de deux façons.
Ils POSTent ce qu’ils ont trouvé
Depuis Proxmox VE 8.2, un installateur préparé avec --fetch-from http POST une
description JSON du matériel qu’il a trouvé — cartes réseau et adresses MAC, disques,
DMI — et attend le fichier de réponse dans le corps de la réponse HTTP :
{
"network_interfaces": [{ "mac": "98:fa:9b:50:d8:10", "link": "up" }],
"dmi": { "system": { "serial": "7ABC123", "product": "PowerEdge R620" } }
}
La réponse dépend de la requête. Un serveur de fichiers statique ne sait pas faire ça : il a une réponse pour une URL, et l’URL est gravée dans l’ISO — identique sur chaque machine.
Ils GETent avec leur identité dans la query string
Tout le reste a la forme inverse. Un kickstart, un preseed, un autoinstall Ubuntu, une configuration Ignition, un profil AutoYaST sont récupérés, avec l’identité de la machine dans la query string, parce qu’iPXE la substitue dans l’URL qu’on lui a dit d’aller chercher :
GET /rhel/ks?serial=7ABC123&mac=98:fa:9b:50:d8:10
Dans les deux cas, la réponse doit être choisie — et le plus souvent assemblée — machine par machine.
Les quatre idées
1. L’endpoint déclare le format. Un client kickstart veut du kickstart et
s’étranglerait avec du TOML. Donc /rhel/ks sert des documents .ks et rien d’autre,
/proxmox/answer sert du .toml, /ubuntu/ sert du YAML. La conséquence qui fait
comprendre le modèle : la réponse d’une machine est spécifique au système d’exploitation
auquel elle est destinée, donc 98fa9b50d810/proxmox.toml n’est pas « cette machine » mais
« cette machine en tant que Proxmox » — et 98fa9b50d810/debian.preseed, dans le même
répertoire, est le même matériel en tant que Debian. Les deux existent en même temps.
→ Un document par système d’exploitation
2. Une machine est revendiquée, pas cherchée. Nommez un répertoire d’après la MAC et il
gagne. Ou listez la machine dans les members d’un groupe. Ou écrivez un bloc [match] et
laissez la machine être revendiquée pour ce qu’elle est — un Dell R620 dont le numéro de
série commence par 7ABC. La résolution est déterministe : nommer bat matcher, plus de
critères bat moins, les égalités se départagent sur le nom trié.
→ Comment une réponse est choisie
3. Les réponses se composent. Une baie de machines partage tout sauf ses adresses MAC.
Mettez la partie commune dans un groupe ; une machine qui diffère reçoit un document
contenant seulement la différence. Les formats structurés fusionnent vraiment — les
maps clé par clé, les tableaux remplacés pour qu’une liste puisse encore être raccourcie.
Ajoutez des placeholders {{ serial }} et un seul document de groupe couvre cinq cents
machines.
→ Groupes et fusion · Templating
4. Ce qui est servi est relisible avant d’être servi. La fusion crée un document que
personne n’a jamais écrit, et une mauvaise fusion se manifeste par une installation
automatisée ratée à 3 h du matin. rescriptum render affiche exactement ce qu’une machine
donnée recevrait ; rescriptum check rend tout et signale ce qui casse, en appelant le
validateur de l’installateur lui-même quand il est dans le PATH.
→ Valider ce qui sera servi
Ce que ce n’est pas
- Pas un serveur DHCP, sous aucune forme. Ni répondeur, ni proxy, ni derrière un drapeau. Les sites qui déploient ceci en ont déjà un, et le faire pointer vers un serveur de démarrage est un problème résolu depuis trente ans.
- Pas un système de gestion de configuration. Il livre un document au moment de l’installation et n’a ensuite plus rien à voir avec la machine — rien de ce qu’il installe n’en dépend ensuite.
- Pas un validateur de schéma. Il prouve que vos documents sont bien formés et
fusionnent proprement. Savoir si le résultat est du Proxmox valide est le travail de
proxmox-auto-install-assistant, etcheckl’appellera s’il est installé.
Deux réalités de déploiement
Les deux sont réelles, et la conception doit satisfaire les deux :
- Un Synology DS416j — ARMv7, 512 Mo, DSM 7, pas de Docker. La motivation d’origine, et la raison pour laquelle c’est un binaire statique unique sans runtime ni interpréteur.
- Un hôte de datacenter encaissant une rafale de provisioning, avec un répertoire de réponses par machine. La raison pour laquelle il est asynchrone, borne sa propre concurrence, et met en cache le listing du répertoire au lieu de le parcourir à chaque requête.
À 2 000 machines, une baie servie depuis un seul groupe rend 13 000 requêtes/seconde sans rien parser par requête — le groupement est le chemin rapide, pas seulement le plus propre.
Où aller ensuite
- Installation — mettre le binaire en route.
- Servir sa première réponse — de bout en bout en cinq minutes.
- Préparer les médias d’installation — l’URL à graver dans chaque ISO.
- Écrire des réponses — sélection, formats, groupes, templating.
- L’exploiter — déploiement, sécurité, stockage, dépannage.
- Médias de démarrage et démarrage réseau — servir l’installeur lui-même, pas seulement sa réponse.
Vous travaillez sur rescriptum plutôt qu’avec ? L’espace Développement est l’autre moitié de ce site.
Installation
Installation
rescriptum est un binaire autonome. Pas de runtime à installer, pas d’interpréteur, pas d’image de conteneur, et rien d’écrit en dehors du répertoire que vous lui indiquez. Copiez-le quelque part et lancez-le.
Télécharger une release
Les binaires de chaque cible publiée sont attachés à chaque release, avec une somme SHA-256 à côté.
| Cible | Pour |
|---|---|
armv7-unknown-linux-gnueabihf | Synology DS416j et autres NAS ARMv7 (glibc ≥ 2.17) |
aarch64-unknown-linux-musl | NAS ARM récents, Raspberry Pi |
x86_64-unknown-linux-musl | la plupart des autres hôtes Linux |
aarch64-apple-darwin | développement local, Apple silicon |
x86_64-apple-darwin | développement local, Mac Intel |
$ VERSION=0.2.0 TARGET=x86_64-unknown-linux-musl
$ curl -fsSLO https://github.com/z29k/rescriptum/releases/download/v$VERSION/rescriptum-$VERSION-$TARGET.tar.gz
$ curl -fsSLO https://github.com/z29k/rescriptum/releases/download/v$VERSION/rescriptum-$VERSION-$TARGET.tar.gz.sha256
$ shasum -a 256 -c rescriptum-$VERSION-$TARGET.tar.gz.sha256
$ tar xzf rescriptum-$VERSION-$TARGET.tar.gz
$ sudo install -m755 rescriptum-$VERSION-$TARGET/rescriptum /usr/local/bin/
Vérifiez la somme. Ce binaire tourne en root sur du matériel que vous êtes sur le point d’installer, ce qui représente à peu près toute la confiance qu’un programme puisse obtenir.
Sur un Synology
Prenez plutôt le .spk de votre modèle — rescriptum-<version>-armv7.spk pour le DS416j et
les autres machines armada38x, -x86_64.spk pour tous les modèles Intel — et installez-le
par Package Center → Installation manuelle. Il crée le dossier partagé, enregistre le
port auprès du pare-feu, lie le CLI dans le PATH et démarre au boot. Les détails, et ce
qu’il ne fait délibérément pas pour vous, sont sur la
page Synology.
Les builds Linux sont liés à musl statiquement — sauf armv7, qui vise la glibc 2.17
parce que musl 1.2 ne peut pas tourner sur les noyaux 3.10 de Synology (voir la
page de build) :
$ file /usr/local/bin/rescriptum
ELF 64-bit LSB executable, x86-64, ... statically linked, stripped
Ou le construire
Un build natif ne demande qu’une toolchain Rust :
$ git clone https://github.com/z29k/rescriptum && cd rescriptum
$ ./build.sh
La compilation croisée pour le NAS demande
cargo-zigbuild et Zig, qui remplacent une
toolchain croisée complète. La page de build donne les
détails, y compris ce qu’il faut vérifier selon la cible : que les builds musl sont bien
statiques, et que le build armv7 ne réclame pas une glibc plus récente que celle du NAS —
les deux échouent au moment de l’exec, sur la machine, et non au build sur votre portable.
Le lancer
$ mkdir -p /srv/answers
$ RESCRIPTUM_ANSWERS_DIR=/srv/answers rescriptum
2026-08-22T18:00:00Z - rescriptum 0.1.0 listening on 0.0.0.0:8000 — store=files:/srv/answers workers=8 max_conn=2048 timeout=10s
2026-08-22T18:00:00Z - warning: /srv/answers does not exist yet — every request will 404 until it does
La ligne de démarrage mérite d’être lue plutôt que défilée :
| Champ | Signification |
|---|---|
listening on | l’adresse réellement bindée, pas celle demandée — avec :0 elles diffèrent |
store= | files:<dir> ou sqlite:<path>, pour qu’un store mal configuré saute aux yeux |
workers= | threads du runtime, nombre de CPU par défaut. Pas une limite de concurrence |
max_conn= | connexions en vol avant que le serveur ne délestage en 503 |
timeout= | délai de lecture des en-têtes et échéance de la connexion entière |
Tout ce qui cloche dans le jeu de réponses — un groupe qui étend un groupe inexistant, un
document qui ne parse pas — est aussi signalé ici, une fois, au démarrage. C’est également
signalé par rescriptum check, qui est le meilleur endroit pour
l’apprendre.
Confirmez qu’il est vivant :
$ curl http://localhost:8000/health
OK
GET /health est le seul endpoint qui n’exige jamais de jeton et n’est jamais limité en
débit, donc une supervision continue de fonctionner même pendant que le serveur refuse tout
le reste.
Où il regarde par défaut
RESCRIPTUM_ANSWERS_DIR vaut par défaut /srv/answers, et RESCRIPTUM_DB_PATH
/srv/answers.db. /srv est l’endroit où la norme de hiérarchie des fichiers range les
données servies par le système, ce qu’elles sont. Rien ne crée le répertoire pour vous ; la
ligne de démarrage le signale s’il manque.
Tout se configure par l’environnement ; il n’y a pas de format de configuration à
apprendre ni de ligne de commande à se tromper. Si vous n’avez nulle part où mettre un
jeton — DSM 7, par exemple — RESCRIPTUM_ENV_FILE nomme un fichier contenant les mêmes
variables. La liste complète est dans la
référence de configuration.
Ensuite
- Servir sa première réponse — une vraie machine recevant un vrai document.
- Déploiement — systemd, ou le planificateur de tâches DSM.
Servir sa première réponse
Servir sa première réponse
Cinq minutes, un terminal, aucun installateur nécessaire. Tout ici est testable hors ligne :
rescriptum render résout une réponse exactement comme le ferait le serveur, donc vous
pouvez avoir la bonne réponse avant qu’aucune machine ne démarre.
1. Un répertoire et un document
$ mkdir -p answers/groups/rack-a
Un répertoire par identité. Un répertoire à la racine est une machine, nommé d’après
elle ; groups/ contient ceux qui sont partagés. Dans l’un comme dans l’autre, l’extension
nomme le format et le reste du nom de fichier n’est qu’une étiquette. Commencez par un
groupe, puisque c’est la forme que prend presque tout déploiement réel — une baie de
machines d’accord sur tout sauf sur les disques qu’elles ont :
# answers/groups/rack-a/proxmox.toml
members = ["98:fa:9b:50:d8:10", "98:fa:9b:50:d8:11"]
[global]
keyboard = "fr"
country = "fr"
timezone = "Europe/Paris"
root-password-hashed = "$6$rounds=656000$REPLACE$ME"
[network]
source = "from-dhcp"
[disk-setup]
filesystem = "zfs"
zfs.raid = "raid1"
disk-list = ["sda", "sdb"]
members liste les machines pour lesquelles ce groupe répond. Le style de séparateur n’a
pas d’importance — 98:fa:9b:50:d8:10, 98-FA-9B-50-D8-10 et 98fa9b50d810 sont une seule
MAC, des deux côtés de la comparaison. members est une clé de rescriptum, pas de Proxmox,
et elle est retirée de ce que l’installateur reçoit.
2. Voir ce qu’une machine recevrait
$ RESCRIPTUM_ANSWERS_DIR=answers rescriptum render 98:fa:9b:50:d8:11
# format=toml group=rack-a
[global]
keyboard = "fr"
country = "fr"
timezone = "Europe/Paris"
root-password-hashed = "$6$rounds=656000$REPLACE$ME"
[network]
source = "from-dhcp"
[disk-setup]
filesystem = "zfs"
zfs.raid = "raid1"
disk-list = ["sda", "sdb"]
La première ligne part sur stderr et dit comment la réponse a été obtenue — la famille de
format, quel document machine a matché, quel groupe s’est appliqué. Le document lui-même
part sur stdout, donc render … > answer.toml ne vous donne que le document.
3. Une machine qui diffère
Le second nœud de la baie a quatre disques. Il reçoit un répertoire nommé d’après sa MAC, contenant un document avec seulement la différence :
# answers/98-fa-9b-50-d8-10/proxmox.toml
[global]
fqdn = "node01.example.com"
[disk-setup]
zfs.raid = "raid10"
disk-list = ["sda", "sdb", "sdc", "sdd"]
$ RESCRIPTUM_ANSWERS_DIR=answers 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"
root-password-hashed = "$6$rounds=656000$REPLACE$ME"
fqdn = "node01.example.com"
[network]
source = "from-dhcp"
[disk-setup]
filesystem = "zfs"
zfs.raid = "raid10"
disk-list = ["sda", "sdb", "sdc", "sdd"]
Le groupe d’abord, le document propre à la machine par-dessus, et la machine a gagné partout où les
deux étaient en désaccord. Les tables ont fusionné clé par clé ; disk-list a été
remplacée, pas concaténée — une liste qui ne pourrait que grandir ne pourrait jamais
être raccourcie depuis une couche supérieure.
4. Vérifier l’ensemble
$ RESCRIPTUM_ANSWERS_DIR=answers rescriptum check
checking files:answers
1 group(s), 1 machine document(s)
note: toml answers not schema-checked — proxmox-auto-install-assistant is not on PATH
ok — everything renders
check rend chaque machine et chaque membre de groupe, et signale tout ce qui casse : un
document qui ne parse pas, un groupe qui en étend un inexistant, un placeholder que rien ne
peut remplir. Là où le validateur de l’installateur est dans le PATH, il le lance aussi, et
dit quels formats il n’a pas pu vérifier.
C’est la commande à mettre en CI si vos réponses vivent dans git.
5. Les servir pour de vrai
$ RESCRIPTUM_ANSWERS_DIR=answers rescriptum
2026-08-24T08:43:36Z - rescriptum 0.1.0 listening on 0.0.0.0:8000 — store=files:answers workers=10 max_conn=2048 timeout=10s
Dans un autre terminal, imitez ce qu’envoie l’installateur Proxmox :
$ curl -s -X POST http://localhost:8000/answer \
-d '{"network_interfaces":[{"mac":"98:fa:9b:50:d8:10","link":"up"}]}'
et regardez le serveur dire ce qu’il a fait :
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=rack-a bytes=431
Cette ligne est tout le diagnostic disponible quand un déploiement dérape : qui a demandé, quelle taille faisait son corps, ce qu’il a reçu, et à partir de quoi c’était construit.
Les nouveaux documents sont pris en compte au fur et à mesure — pas de redémarrage, pas de signal de rechargement. L’apparition ou la disparition du répertoire entier d’une machine est vue immédiatement ; un document ajouté ou modifié à l’intérieur de l’un d’eux est pris en compte en moins d’une seconde.
À lire ensuite
- Comment une réponse est choisie — vous avez vu le nommage et l’appartenance ; les sélecteurs revendiquent une machine pour ce qu’elle est.
- Un document par système d’exploitation — la même machine en Proxmox, en Debian, en Ubuntu, côte à côte.
- Templating —
fqdn = "node-{{ serial }}.example.com", pour qu’un groupe couvre une baie sans un répertoire par machine. - Préparer les médias d’installation — l’URL à graver dans l’ISO, par OS.
Préparer les médias d'installation
Préparer les médias d’installation
Chaque installateur se voit dire, au moment de la fabrication du média, où récupérer sa configuration. Cette URL fait ici deux choses :
- Elle atteint le serveur. N’importe quel chemin fonctionne —
POSTetGETsont traités sur tous, précisément pour que l’URL gravée dans une ISO ne soit jamais fausse. - Son chemin déclare le format. Un segment nommant un alias connu restreint la réponse aux documents de ce format, pour qu’un client kickstart ne reçoive pas du TOML.
Donnez à chaque installateur son URL, et un seul serveur répond pour tous.
Proxmox VE
$ proxmox-auto-install-assistant prepare-iso proxmox-ve.iso \
--fetch-from http \
--url http://SERVER:8000/proxmox/answer \
--output proxmox-auto.iso
L’installateur POST un inventaire JSON du matériel trouvé et attend la réponse dans le corps
de la réponse HTTP. /proxmox/ restreint la réponse aux documents .toml ; /answer seul
ne nomme aucun alias et ne contraint rien, ce qui est pourquoi un déploiement existant
continue de fonctionner sans changement.
Pour exiger une authentification, préparez l’ISO avec un jeton et donnez le même au serveur :
$ proxmox-auto-install-assistant prepare-iso proxmox-ve.iso \
--fetch-from http --url http://SERVER:8000/proxmox/answer \
--answer-auth-token 'une-longue-chaine-aleatoire' --output proxmox-auto.iso
$ export RESCRIPTUM_ANSWER_TOKEN='une-longue-chaine-aleatoire'
Voir Sécurité pour ce que cela protège et ce que cela ne protège pas.
Sans refabriquer l’ISO
Proxmox peut aussi découvrir l’URL au démarrage, ce qui évite de refabriquer le média quand l’adresse change :
- un enregistrement DNS TXT sur
proxmox-auto-installer.<votre-domaine>, ou - l’option DHCP 250.
Les deux sont hors du périmètre de ce serveur — il lui suffit d’être à l’adresse qu’ils nomment.
Tout le reste, via iPXE
Les autres installateurs récupèrent leur configuration et s’identifient dans la query string, parce qu’iPXE substitue ses propres variables dans l’URL avant de la chercher :
| Variable | Vaut |
|---|---|
${net0/mac} | l’adresse MAC de la première carte réseau |
${uuid} | l’UUID système SMBIOS |
${serial} | le numéro de série système |
${manufacturer}, ${product} | fabricant et modèle DMI |
Ces valeurs deviennent des faits sur lesquels un document peut être sélectionné, et alimentent aussi la botte de foin — donc un document nommé d’après une MAC résout que la MAC soit arrivée dans un corps POST ou dans une query string.
| Installateur | Paramètre de boot |
|---|---|
| RHEL / CentOS / Fedora / Alma / Rocky | inst.ks=http://SERVER:8000/rhel/ks?mac=${net0/mac} |
| Debian preseed | url=http://SERVER:8000/debian/preseed?mac=${net0/mac} |
| Ubuntu autoinstall | autoinstall ds=nocloud-net;s=http://SERVER:8000/ubuntu/?mac=${net0/mac} |
| Flatcar / Fedora CoreOS | ignition.config.url=http://SERVER:8000/flatcar/config?mac=${net0/mac} |
| openSUSE / SLES | autoyast=http://SERVER:8000/suse/profile?mac=${net0/mac} |
| Windows | récupéré par votre propre outillage depuis http://SERVER:8000/windows/unattend |
Un fragment de script iPXE complet :
#!ipxe
set base http://SERVER:8000
kernel ${base}/images/rhel9/vmlinuz inst.ks=${base}/rhel/ks?mac=${net0/mac}&serial=${serial}
initrd ${base}/images/rhel9/initrd.img
boot
rescriptum sert la réponse, pas le noyau — le netboot reste au serveur TFTP/HTTP que vous faites déjà tourner.
Ubuntu et cloud-init NoCloud
La source de données NoCloud de cloud-init récupère deux fichiers nommés depuis l’URL de
seed — user-data et meta-data — et ignore complètement la source de données si l’un des
deux manque. Comme ce serveur répond sur n’importe quel chemin, les deux requêtes recevraient
sinon le même document et l’installation ne démarrerait jamais.
Le dernier segment du chemin est disponible comme fait file, ce qui permet de les
distinguer avec un sélecteur :
# answers/groups/ubuntu-web/ubuntu.yaml
match:
file: "user-data"
product: "PowerEdge R6*"
# answers/groups/ubuntu-meta/ubuntu.yaml
match:
file: "meta-data"
instance-id: iid-local01
Notez le slash final dans s=http://SERVER:8000/ubuntu/ — cloud-init y accole le nom de
fichier.
NoCloud peut aussi développer __dmi.chassis-serial-number__ dans l’URL de seed, ce qui met
l’identité de la machine dans le chemin plutôt que dans la query. Les segments de chemin
alimentent aussi la botte de foin, donc un document nommé d’après ce numéro de série résout
quand même.
Choisir l’alias
| Segment d’URL | Sert les documents d’extension |
|---|---|
proxmox, pve, toml | .toml |
debian, preseed | .preseed, .seed |
rhel, centos, fedora, alma, rocky, kickstart, ks | .ks |
ubuntu, autoinstall, cloudinit, nocloud, yaml, yml | .yaml, .yml |
flatcar, coreos, ignition, ign | .ign, .json |
suse, opensuse, autoyast | .autoyast, .xml |
windows, unattend | .unattend, .xml |
json | .json, .ign |
xml, cfg, ipxe | l’extension correspondante |
N’importe quel segment du chemin peut nommer l’alias, donc /rhel/ks, /ks et
/provision/rhel/node.cfg restreignent tous au kickstart. Une URL n’en nommant aucun —
/answer — ne contraint rien.
La table complète, et pourquoi seed n’est délibérément pas un alias, sont dans la
référence des formats.
Ensuite
- Comment une réponse est choisie — ce que le serveur fait de ce que l’URL vient de lui dire.
- Un document par système d’exploitation — la même machine, en Proxmox et en Debian, en même temps.
É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 dansmembers, ou par un blocmatchtesté 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 choisie | Par 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’exploitation | L’extension est le format, l’endpoint choisit entre eux, et une machine peut exister en plusieurs systèmes à la fois |
| Groupes et fusion | Les 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 |
| Validation | render 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 |
|---|---|
members | les machines pour lesquelles ce groupe répond |
match | des critères testés contre les faits de la requête |
extends | le 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 :
| Fait | Vaut |
|---|---|
path | le chemin entier, débarrassé de ses slashes — rhel/ks |
file | son dernier segment — ks. C’est ce qui distingue le user-data de cloud-init de son meta-data |
segment | chaque 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 :
- 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.
- 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.
- 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/ksdemande du kickstart ; - le document le porte comme extension —
groups/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
| Extension | Pour | Superposition |
|---|---|---|
toml | Proxmox VE | fusion structurelle |
yaml, yml | autoinstall Ubuntu, cloud-init | fusion structurelle |
json, ign | Ignition, Flatcar, Fedora CoreOS | fusion structurelle |
xml, autoyast, unattend | AutoYaST, unattend.xml Windows | fusion structurelle, par élément |
ks | kickstart — RHEL, CentOS, Fedora, Alma, Rocky | concaténation |
preseed, seed | preseed Debian | concaténation |
cfg, ipxe | scripts de boot et autres configurations en lignes | concaté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 discriminant — name, 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
| Couches | la chaîne de groupes d’abord, le document machine en dernier — la machine gagne toujours |
| Maps | fusionnent récursivement, y compris les tables inline et pointées de TOML |
| Autres valeurs | remplacées intégralement par la couche supérieure |
| Tableaux | remplacent, ils ne concatènent pas |
| Formats texte | concaté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 base → rack-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 :
| Agencement | Débit |
|---|---|
| 2 000 documents machine, aucun groupe | 12 132 req/s |
| un groupe de 2 000 membres, aucun document machine | 13 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
| Placeholder | Rempli 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
- Validation — rendre avec de vrais faits, et vérifier l’ensemble.
- Capturer les requêtes — obtenir un vrai corps sur lequel rendre.
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
matchet 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
membersnimatchcomme atteignable seulement viaextends, 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
| Format | Outil |
|---|---|
toml | proxmox-auto-install-assistant validate-answer |
xml, autoyast, unattend | xmllint --noout |
ks | ksvalidator |
yaml, json, ign, preseed, cfg, ipxe | aucun 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.
L'exploiter
L’exploiter
rescriptum est un processus, configuré entièrement par l’environnement, qui n’écrit rien en dehors du store que vous lui indiquez. Bien l’exploiter consiste surtout à décider ce qu’il a le droit de servir, et à qui.
- Déploiement — une unité systemd, un conteneur, ou rien du tout.
- Synology DSM 7 — la cible d’origine : une installation par Package Center qui crée le partage, enregistre le port et démarre au boot.
- Sécurité — les deux jetons, pourquoi ils se comportent différemment, et ce qu’aucun des deux ne protège.
- Capturer les requêtes — enregistrer ce que les machines envoient réellement, et le rejouer hors ligne.
- Le store SQLite — pour un parc administré par outillage plutôt qu’à la main.
- L’API d’administration — gérer les réponses en HTTP, sur son propre listener, avec une écriture qui ne peut pas casser le parc.
- Dépannage — la ligne de log est tout le diagnostic disponible.
- Servir les médias de démarrage — le noyau, l’initrd et l’image de l’installeur, depuis le même serveur.
- Démarrer une machine par le réseau — TFTP, le chargeur, le menu, et les deux lignes de leur DHCP.
La forme d’un déploiement
| Un processus | pas d’arbre de supervision, pas de workers à dimensionner, pas de sidecar |
| Un port par défaut | plus un second, seulement si vous activez l’API d’administration |
| Aucune écriture | en dehors du répertoire de réponses ou de la base, et aucune du tout à moins d’activer l’API d’administration ou la capture |
| Aucun état | entre les requêtes. Un redémarrage ne perd rien |
| Arrêt propre | sur SIGTERM (ce qu’envoie le planificateur DSM) et Ctrl-C |
La configuration se fait par variables d’environnement uniquement. Une valeur numérique nulle ou impossible à parser retombe sur sa valeur par défaut plutôt que de démarrer un serveur qui accepte des connexions sans jamais répondre.
Ce dont il a besoin du réseau
Que l’installateur puisse l’atteindre, et c’est tout. Il n’ouvre aucune connexion sortante, n’a besoin d’aucun DNS, et se moque d’être derrière un NAT.
Le HTTP en clair est le choix normal sur un réseau de provisioning. Si vous avez besoin de TLS — certaines versions d’installateur demandent une empreinte de certificat — terminez-le devant avec nginx ou Caddy et pointez l’ISO dessus. Voir Sécurité.
Déploiement
Déploiement
Le binaire se suffit à lui-même : copiez-le quelque part, donnez-lui un répertoire de réponses, et lancez-le. Tout ce qui suit sert à le faire de façon reproductible.
Pour Synology DSM 7 — qui n’a pas de systemd — voir sa page dédiée.
Un fichier d’environnement
Gardez la configuration dans un fichier lisible par root seul, plutôt que dans une unité ou
sur une ligne de commande. Tout ce qui est sur une ligne de commande est visible par tous
les utilisateurs de la machine via ps, ce qui compte dès qu’un jeton entre en jeu :
# /etc/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_ANSWERS_DIR=/srv/answers
RESCRIPTUM_LISTEN_ADDR=0.0.0.0:8000
RESCRIPTUM_TIMEOUT_SECS=10
# RESCRIPTUM_ANSWER_TOKEN=…
Sous systemd, l’EnvironmentFile= ci-dessous le lit et vous n’avez besoin de rien d’autre.
Ailleurs — et sur DSM 7, qui n’a pas de systemd — pointez
RESCRIPTUM_ENV_FILE sur le même
fichier et le binaire le lit lui-même, en refusant de démarrer s’il n’y arrive pas.
Une unité systemd
# /etc/systemd/system/rescriptum.service
[Unit]
Description=rescriptum — per-machine answer files for unattended installs
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/rescriptum
EnvironmentFile=/etc/rescriptum.env
Restart=on-failure
RestartSec=2
# Il doit lire un répertoire et binder un port. Rien d'autre.
DynamicUser=yes
ReadOnlyPaths=/srv/answers
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
[Install]
WantedBy=multi-user.target
$ sudo systemctl enable --now rescriptum
$ curl -s http://localhost:8000/health
OK
Ajustez selon ce que vous activez réellement :
- Store SQLite — la base doit être accessible en écriture, donc
ReadWritePaths=/srvet retirezReadOnlyPaths. - Capture des requêtes —
ReadWritePaths=sur le répertoire de capture. - Un port sous 1024 — ajoutez
AmbientCapabilities=CAP_NET_BIND_SERVICE.
Les logs partent sur stderr, donc journalctl -u rescriptum -f est la vue en direct.
En conteneur
Il n’y a rien à installer, donc l’image est le binaire :
FROM scratch
COPY rescriptum /rescriptum
ENV RESCRIPTUM_ANSWERS_DIR=/answers RESCRIPTUM_LISTEN_ADDR=0.0.0.0:8000
EXPOSE 8000
ENTRYPOINT ["/rescriptum"]
Utilisez le build de la bonne architecture — les builds musl sont liés statiquement, ce qui est ce qui
fait fonctionner FROM scratch. Montez le répertoire de réponses en lecture seule.
Le dimensionner
Les valeurs par défaut sont déjà bonnes aux deux extrémités de la gamme pour laquelle il a été conçu.
| Réglage | Défaut | Le changer quand |
|---|---|---|
RESCRIPTUM_WORKERS | nombre de CPU | vous partagez une petite machine et voulez plafonner les threads |
RESCRIPTUM_MAX_CONNECTIONS | 2048 | vous voyez des 503 pendant une rafale — ou voulez délester plus tôt |
RESCRIPTUM_TIMEOUT_SECS | 10 | les clients sont sur un lien lent, ou vous voulez couper le slowloris plus tôt |
MAX_CONNECTIONS n’est pas une limite de débit. Au-delà du plafond, le serveur écrit un
503 immédiat et ferme plutôt que de mettre en file — un client à qui on dit de réessayer
s’en sort mieux qu’un client garé dans une file qui ne se videra pas.
Un déploiement de 2 000 machines se termine en moins de deux secondes au débit mesuré, donc le dimensionnement est rarement le problème intéressant. Le dépannage l’est généralement.
Remplacer une instance en cours
$ ./deploy.sh admin@nas
$ ./deploy.sh admin@nas /volume1/netboot # un autre répertoire distant
Ce qu’il fait, dans l’ordre :
- Construit pour la cible (
TARGET, par défautarmv7-unknown-linux-gnueabihf). - Vérifie les réponses locales avec
rescriptum checket refuse de continuer si quoi que ce soit échoue — expédier un jeu de réponses cassé est pire que ne pas déployer. - Copie le binaire sous un nom temporaire, puis le renomme en place. Remplacer un binaire en cours d’exécution sur place est la façon dont un fichier à moitié copié se fait exécuter.
- Arrête l’instance en cours, démarre la nouvelle en détaché, et confirme qu’elle est restée en vie.
- Confirme que
/healthrépond par le réseau, pour qu’un problème de pare-feu soit signalé comme tel plutôt que comme un silence mystérieux.
| Environnement | Défaut |
|---|---|
TARGET | armv7-unknown-linux-gnueabihf |
ANSWERS | <répertoire-distant>/answers |
PORT | 8000 |
Il remplace ce qui tourne ; il n’installe pas l’autostart. Sur DSM c’est une
entrée du planificateur de tâches ; avec systemd c’est
systemctl enable.
Mettre à jour
Les réponses sont des données, pas de l’état : rien n’est migré, et un nouveau binaire lit le même répertoire. Remplacez-le et redémarrez.
L’exception est le store SQLite, qui porte une version de schéma. Il n’y en a qu’une pour l’instant, donc rien à migrer ; ce que la version apporte, c’est l’autre sens : un binaire plus ancien refuse d’ouvrir une base écrite par un plus récent plutôt que de la deviner. Voir le store SQLite.
Synology DSM 7
Synology DSM 7
Un Synology DS416j est la raison d’être de ce projet : ARMv7, 512 Mo de RAM, DSM 7, pas de Docker. Un binaire statique sans runtime n’y est pas une préférence esthétique — c’est la seule chose qui rentre.
DSM 7 fait tourner systemd, mais il n’offre aucun endroit supporté pour une unité à vous :
les fichiers de /usr/lib/systemd/system appartiennent à Synology, et une mise à jour de
DSM est libre de les remplacer. La route supportée vers un service, c’est un paquet —
installez-en un et DSM génère pkgctl-rescriptum.service à partir de lui. C’est par là que
cette page commence ; la route par le planificateur de tâches fonctionne
toujours et reste en bas.
Installer le paquet
Téléchargez le .spk de votre modèle depuis la
page des releases :
| Fichier | Pour |
|---|---|
rescriptum-<version>-armv7.spk | DS416j et les autres modèles Marvell armada38x |
rescriptum-<version>-x86_64.spk | tous les modèles Intel |
Un doute ? Demandez à la machine :
$ ssh admin@nas synogetkeyvalue /etc.defaults/synoinfo.conf unique
synology_armada38x_ds416j
Puis Package Center → Installation manuelle, choisissez le fichier, et passez
l’avertissement disant que le paquet n’est pas vérifié par Synology. Cet avertissement ne
vise pas ce paquet en particulier : DSM 7 a supprimé la signature tierce et n’offre plus de
réglage de niveau de confiance, donc tout paquet non-Synology l’affiche. Notre vérification
à nous, c’est la somme SHA-256 publiée à côté du .spk :
$ shasum -a 256 -c rescriptum-0.2.0-1-armv7.spk.sha256
L’assistant pose deux questions — où vivent les réponses et sur quel port écouter — puis le paquet :
- crée un dossier partagé
rescriptumet s’accorde un accès lecture/écriture dessus (si vous en avez déjà un de ce nom, il est conservé et gagne simplement le droit) ; - crée le répertoire
answersdedans à chaque démarrage ; - enregistre le port auprès du pare-feu DSM, pour que le service soit sélectionnable par son nom ;
- lie
rescriptum-clidans/usr/local/bin; - démarre au boot, et s’arrête et redémarre depuis Package Center comme n’importe quoi d’autre.
Ce que le paquet ne fait pas
Cinq choses à savoir avant qu’elles ne vous surprennent.
- Il ne peut pas ouvrir le port 69 tout seul. DSM 7 n’autorise pas un paquet non signé à tourner en root, donc le TFTP vous demande une commande root, une seule fois — voir Le TFTP demande une commande root. Tant qu’elle n’est pas donnée, le serveur avertit, continue de répondre et de servir les images, et seule la livraison du chargeur est coupée.
- Il n’ouvre pas le pare-feu. Enregistrer le port fait apparaître rescriptum par son nom dans l’éditeur de règles au lieu d’un numéro à taper. Si votre pare-feu est actif avec une règle par défaut qui refuse, il faut toujours créer la règle.
- Il ne vous annonce pas les mises à jour. Il n’y a pas de source de paquets à
interroger — le modèle de distribution est : téléchargez le nouveau
.spkdepuis la page des releases et installez-le à la main, pour une mise à jour comme pour une première installation. Surveillez les releases. - Un chemin de réponses personnalisé, les permissions sont à vous. Le paquet tourne sans
privilèges et ne peut pas s’accorder l’accès à un dossier que vous nommez ; si vous
pointez hors du partage
rescriptum, donnez vous-même l’accès en lecture à l’utilisateurrescriptum. - Le droit sur le partage est réappliqué à chaque démarrage. Si vous le restreignez délibérément, vous le retrouverez rétabli au démarrage suivant du paquet.
Où vit quoi
| Quoi | Où | Survit à une mise à jour | Survit à la désinstallation |
|---|---|---|---|
binaire, rescriptum-cli, le fichier d’environnement d’exemple | /var/packages/rescriptum/target/ | non — remplacé | non |
| le fichier d’environnement | /var/packages/rescriptum/etc/rescriptum.env | oui | oui — voir ci-dessous |
| journal, pidfile, captures | /var/packages/rescriptum/var/ | oui | oui |
| les réponses | /var/packages/rescriptum/shares/rescriptum/answers/ | oui | oui — toujours |
| la base SQLite, si vous en utilisez une | à côté des réponses, dans le même partage | oui | oui — toujours |
Utilisez le chemin shares/ plutôt que /volume1/… : c’est un lien symbolique maintenu par
DSM, donc il continue de marcher sur un NAS dont les données ne sont pas sur le volume 1.
La désinstallation laisse le dossier partagé et tout ce qu’il contient tranquilles. C’est à la fois le comportement de DSM et le nôtre : quand le magasin est SQLite, la base est vos réponses.
Elle laisse aussi votre configuration derrière elle, et ça vaut d’être su. etc/ et
var/ sont des liens vers /volume1/@appconf/rescriptum et /volume1/@appdata/rescriptum,
que DSM conserve — le fichier d’environnement reste donc sur le volume après la disparition
du paquet, avec les jetons qu’il contient. Une réinstallation le reprend, ce qui est
généralement ce qu’on veut. Si vous retirez rescriptum pour de bon et qu’il portait un
jeton, supprimez /volume1/@appconf/rescriptum vous-même.
L’application de bureau
Le paquet installe une application sur le bureau DSM — l’icône est dans le menu principal, et le bouton Ouvrir de Package Center y mène. C’est une vraie application DSM, bâtie sur le framework d’interface du bureau : elle est dans le thème DSM et dans la langue de DSM. Le français d’un DSM en français est aussi celui de l’application.
Elle a trois onglets :
- Réglages — chaque variable de configuration, sous forme de formulaire. Chaque champ dit d’où vient sa valeur, et une valeur définie dans l’environnement est affichée mais verrouillée, parce que modifier le fichier n’y changerait rien. Enregistrer écrit le fichier et propose de redémarrer le paquet, le serveur ne lisant sa configuration qu’une fois, au démarrage.
- État — la version, si le paquet tourne, le dossier des réponses et s’il est vraiment
lisible par l’utilisateur du service, et la sortie de
check. - Journal — les dernières lignes du journal des requêtes et de
startup.log.
Trois propriétés valent mieux d’être sues que découvertes :
- Elle édite le fichier, pas le serveur qui tourne. Elle fonctionne donc encore quand le serveur refuse de démarrer, c’est-à-dire précisément quand un panneau de réglages sert à quelque chose. Une modification qui laisserait le serveur incapable de démarrer est refusée avant toute écriture, avec la raison affichée.
- Elle ne vous montre jamais un jeton.
RESCRIPTUM_ANSWER_TOKENetRESCRIPTUM_ADMIN_TOKENapparaissent comme défini ou non défini, et un champ vide veut dire « n’y touche pas », jamais « efface-le ». En saisir un nouveau le remplace. - Elle exige un administrateur DSM. Être connecté à DSM ne suffit pas. Voir sécurité pour pourquoi ce contrôle est toute la porte.
Redémarrer maintenant arrête et redémarre le paquet via DSM lui-même, donc DSM ferme la fenêtre pendant ce temps — rouvrez-la pour voir le nouvel état. L’application le dit à côté du bouton plutôt que de vous laisser la surprise.
Elle demande DSM 7.1 ou plus récent (os_min_ver="7.1-42661"). Elle est bâtie sur le
framework ExtJS de DSM, présent en 7.1.1 comme en 7.2.2 — les deux mesurés. DSM 7.2 embarque
un framework Vue plus récent, et le guide actuel de Synology ne documente que celui-là ; mais
le DS416j qui justifie ce projet plafonne en 7.1.1, où Vue n’existe pas. ExtJS couvre donc
tous les DSM que ce paquet prend en charge plutôt que les seuls récents. Le 7.0 n’est pas
revendiqué : rien n’y a jamais tourné.
Le configurer
L’application ci-dessus est la voie confortable. Tout ce qu’elle fait se fait aussi depuis un shell, et sur une machine où le bureau n’est pas à portée c’est plus rapide :
$ sudo rescriptum-cli config
env file: /var/packages/rescriptum/etc/rescriptum.env
RESCRIPTUM_STORE files default
RESCRIPTUM_ANSWERS_DIR /var/packages/rescriptum/shares/rescriptum/answers file
RESCRIPTUM_LISTEN_ADDR 0.0.0.0:8000 file
…
$ sudo rescriptum-cli config set RESCRIPTUM_LOG=problems
wrote /var/packages/rescriptum/etc/rescriptum.env
config set conserve les commentaires du fichier, décommente un réglage au lieu de le
dupliquer, et refuse une modification qui empêcherait le serveur de démarrer. Son code
de sortie dit si la configuration en est une sur laquelle le serveur démarrerait, ce qui le
rend utilisable depuis un script.
Dessous, c’est le même fichier, et l’éditer à la main reste parfaitement raisonnable :
$ sudo vi /var/packages/rescriptum/etc/rescriptum.env
postinst l’écrit complet à une installation neuve, avec les variables utilisées
décommentées et les autres commentées avec une ligne disant à quoi elles servent.
Arrêtez et redémarrez le paquet depuis Package Center pour appliquer un changement — le
serveur lit le fichier à chaque démarrage.
Une mise à jour n’y touche jamais. L’exemple complet pour la version que vous avez est dans
/var/packages/rescriptum/target/etc/rescriptum.env.example, réécrit à chaque installation
et chaque mise à jour : c’est ainsi qu’une nouvelle variable devient visible sans déranger
votre fichier vivant. Toutes les variables sont dans la
référence de configuration.
Le fichier est en chmod 600 et appartient à l’utilisateur du paquet. C’est là que vivent
RESCRIPTUM_ANSWER_TOKEN et RESCRIPTUM_ADMIN_TOKEN et — étant sous etc/ — c’est un
passager plausible d’une sauvegarde de configuration DSM. Mieux vaut le savoir que le
découvrir.
L’API d’administration est désactivée par défaut et, quand vous l’activez,
devrait rester sur la boucle locale et être atteinte par un tunnel SSH ; elle n’est
délibérément pas enregistrée auprès du pare-feu. Elle exige aussi
RESCRIPTUM_STORE=sqlite et un jeton d’au moins 16 caractères, deux erreurs de
démarrage — donc se tromper se manifeste par un paquet qui ne démarre pas, avec la raison
dans /var/log/packages/rescriptum.log.
Mettre les réponses en place
Déposez les fichiers dans le répertoire answers du dossier partagé rescriptum, via File
Station ou en SSH, exactement comme ailleurs — voir
écrire des réponses. Puis validez-les en tant qu’utilisateur du
paquet :
$ sudo -u rescriptum rescriptum-cli check
Le sudo -u compte. Lancé en root, il réussit quoi que disent les permissions du dossier
partagé, ce qui rend un succès dénué de sens. rescriptum-cli est l’enveloppe fournie par
le paquet : elle nomme le fichier d’environnement, pour que check et render regardent
les réponses de cette machine plutôt que /srv/answers.
Le pare-feu
Panneau de configuration → Sécurité → Pare-feu — créez une règle autorisant rescriptum depuis votre réseau de provisionnement. Le service apparaît par son nom parce que le paquet a enregistré son port.
Le pare-feu de DSM est la première raison pour laquelle une machine « ne contacte jamais le serveur ».
Si vous changez le port plus tard, modifiez RESCRIPTUM_LISTEN_ADDR dans le fichier
d’environnement puis déplacez l’entrée du pare-feu, qui ne suit pas toute seule :
$ sudo /usr/syno/sbin/synopkghelper update rescriptum port-config
Servir les médias d’installation, et le PXE
Le paquet sait aussi servir l’installeur lui-même — noyaux, initrds et images — depuis le NAS qui décide déjà la réponse. C’est éteint jusqu’à ce que vous l’allumiez :
- Décommentez
RESCRIPTUM_MEDIA_DIRdans le fichier d’environnement et redémarrez le paquet. - Posez une ISO dans le dossier
mediadu partagerescriptum, via File Station ou SMB. - Enregistrez-la, pour qu’elle soit vérifiée et analysée une fois plutôt qu’à chaque requête :
$ rescriptum-cli media add /volume1/rescriptum/media/proxmox-ve_8.4-1.iso \
--sha256 9f86d081884c7d65…
$ rescriptum-cli media list
Le listener média est sur le port 8001, déjà déclaré au pare-feu à côté du port de réponse — il reste à créer la règle.
Aucune image n’est livrée avec le paquet, et aucune ne le sera jamais : une ISO est l’artefact de quelqu’un d’autre, elle pèse des gigaoctets, et elle évolue à son rythme. Ce dossier est là où vous les gardez, et c’est l’archive — rien ici ne modifie une image après son arrivée. Préparer une image Proxmox produit un fichier compagnon de deux cents octets et une injection appliquée au fil de l’eau, donc les octets sur disque restent exactement ce que Proxmox a publié et leur somme reste vérifiable contre celle de Proxmox. Voir Servir les médias de démarrage.
Le TFTP demande une commande root
C’est rescriptum le serveur TFTP ici, pas DSM. Le port 69 est privilégié et DSM 7 refuse qu’un paquet non signé tourne en root : le paquet ne peut donc pas s’accorder le port lui-même — mais il n’a pas besoin de root pour s’en servir, seulement qu’on l’y autorise une fois :
$ sudo setcap cap_net_bind_service=+ep /volume1/@appstore/rescriptum/bin/rescriptum
$ sudo synopkg restart rescriptum
Après quoi le paquet ouvre udp/69 sous son propre utilisateur non privilégié
rescriptum, à côté de 8000 et 8001. Les trois sont enregistrés auprès du pare-feu.
Rendez-la durable, car une mise à jour la perd. Installer une nouvelle version remplace
le binaire, et les capacités de fichier appartiennent au fichier — elles partent donc avec
l’ancien. Panneau de configuration → Planificateur de tâches → Créer → Tâche déclenchée
→ Script défini par l’utilisateur, utilisateur root, événement Démarrage, avec la
ligne setcap comme script. Relancez-la depuis cette page après chaque mise à jour, ou
redémarrez.
Rien d’autre ne casse pendant ce temps. Un port TFTP qu’on ne peut pas ouvrir est le
seul écouteur de ce serveur dont l’échec n’est pas fatal, et c’est délibéré : les réponses
sont le produit, et une mise à jour ne doit pas couper les installations d’une flotte pour
signaler qu’un second port n’a pas pu être ouvert. Ce que vous obtenez à la place, c’est un
avertissement dans le journal, une ligne tftp: dans l’onglet État du panneau de
réglages, et :
$ rescriptum-cli boot check
BROKEN nothing answers on 0.0.0.0:69 and it cannot be bound either: Permission denied.
Port 69 is privileged: run as root and set RESCRIPTUM_USER to drop afterwards, or grant
the binary cap_net_bind_service with setcap — the server still answers and still serves
media, but a machine sent here by DHCP asks for a loader and gets nothing
Notez qu’il demande un chargeur au port plutôt que d’essayer de l’ouvrir. Ouvrir le port prouve le contraire de ce qu’on croit : une ouverture qui réussit signifie que personne n’écoute.
Les chargeurs sont dans le paquet. Le dossier boot du partage arrive rempli au
premier démarrage, et une mise à jour les rafraîchit — il n’y a pas de second
téléchargement. C’est iPXE, en GPLv2, des fichiers séparés servis à côté plutôt que soudés
dans quoi que ce soit, et le NOTICE posé avec eux nomme le commit amont exact dont ils
sont issus.
$ rescriptum-cli boot check
ok 0.0.0.0:69 handed over ipxe-undionly.kpxe
Les remplacer est possible, mais pas en modifiant ce dossier : une mise à jour réécrit les
noms de fichiers que ce paquet fournit. Pointez plutôt RESCRIPTUM_BOOT_DIR ailleurs, et
rien ici n’y écrira jamais.
Puis faites pointer le DHCP vers ce NAS — Panneau de configuration → Serveur DHCP → PXE si le NAS sert le DHCP, ou votre propre serveur avec ce qu’imprime :
$ rescriptum-cli boot dhcp-snippet --format dnsmasq
Si vous préférez éviter setcap
RESCRIPTUM_TFTP_ADDR accepte un port non privilégié, qui ne demande aucune capacité — il
faut alors le dire à votre serveur DHCP, puisqu’une ROM PXE a 69 gravé dedans et que seul
un premier étage de chaînage peut être redirigé. Ou mettez-le à off et laissez un autre
service de ce NAS livrer le chargeur ; DSM a son propre serveur TFTP sous Panneau de
configuration → Services de fichiers → Avancé, pointé sur le dossier boot du partage. Ce
sont deux contournements pour un déploiement qui les veut, pas ce que le paquet attend.
L’onglet Images
L’application a un quatrième onglet, et c’est là que les images d’installation se gèrent sans toucher à un terminal : ce qui est présent, un catalogue où piocher, et un champ URL pour ce que le catalogue ne propose pas.
Le catalogue n’est pas une liste livrée par ce paquet. Chaque entrée nomme l’index de sommes que l’éditeur publie déjà à côté de ses propres images ; en choisir un lit cet index par le réseau, donc les versions proposées sont celles que l’éditeur a aujourd’hui et l’empreinte vérifiée est la sienne. Cela veut dire aussi que cet onglet a besoin que le NAS atteigne Internet — la seule partie de ce paquet qui en a besoin.
Le téléchargement d’une image de 1,5 Go ne peut pas être tenu par une requête web : l’onglet
le lance et le suit. media add écrit dans un fichier .part à côté de sa destination et
ne le renomme qu’une fois l’empreinte vérifiée — la taille du fichier partiel est donc
l’avancement, et sa disparition est la fin. Fermer la fenêtre n’arrête pas le
téléchargement.
Préparer une image Proxmox est aussi un bouton. Toutes les autres familles prennent l’URL de leur réponse sur la ligne de commande du noyau : il n’y a rien à préparer, et l’onglet le dit plutôt que de proposer une étape sans effet.
Un réglage qui mérite d’être rempli
RESCRIPTUM_PUBLIC_HOST=192.168.1.10
Chaque script généré nomme cette adresse. Laissée vide, elle est déduite en interrogeant la table de routage, et le panneau de réglages affiche ce que cela a donné plutôt qu’une case vide — donc sur un NAS à une seule interface, il n’y a rien à remplir ici.
C’est le NAS à deux interfaces qui mérite la lecture. La déduction en retient une, et le journal de démarrage nomme les autres à côté :
warning: RESCRIPTUM_PUBLIC_HOST is not set — derived 192.168.1.10, which is what every
generated URL will name. This host also has 10.0.0.10. If the machines reach it on one of
those instead, set it explicitly.
Se tromper produit une machine qui démarre, enchaîne, et se bloque sur une adresse qui n’existe pas — long à diagnostiquer depuis la machine.
Le journal
RESCRIPTUM_LOG_FILE pointe le serveur vers
/var/packages/rescriptum/var/rescriptum.log, et le paquet installe une strophe logrotate
pour lui — hebdomadaire, huit conservés, copytruncate (le serveur ouvre son journal une
fois et ne le rouvre jamais, donc tout le reste arrêterait silencieusement la
journalisation). À côté, var/startup.log contient ce que le serveur dit avant de savoir où
vit son journal : une erreur de configuration, un fichier d’environnement mal formé.
Une fois qu’un déploiement devient routinier, RESCRIPTUM_LOG=problems garde les échecs et
laisse tomber les réponses réussies, seule chose à fort volume là-dedans.
Quand ça ne démarre pas
Trois endroits disent pourquoi, dans cet ordre :
$ cat /var/log/packages/rescriptum.log # la sortie des scripts du paquet
$ cat /var/packages/rescriptum/var/startup.log # ce que le serveur a dit avant d'avoir un journal
$ cat /var/packages/rescriptum/var/rescriptum.log
$ systemctl status pkgctl-rescriptum # ce qu'a vu le gestionnaire de services
Une configuration refusée — un jeton d’administration de moins de 16 caractères, un
magasin impossible à ouvrir — est signalée après que le serveur sait où vit son journal :
elle atterrit donc dans rescriptum.log ; un fichier d’environnement mal formé est signalé
avant, et atterrit dans startup.log. Le start du paquet affiche la fin des deux quand le
serveur sort immédiatement, pour que Package Center vous montre la raison et pas seulement
l’échec.
DSM ne relance pas le processus s’il meurt. L’unité qu’il génère est Type=oneshot avec
RemainAfterExit=yes et sans Restart= : un serveur qui sort reste arrêté jusqu’à ce que
vous le redémarriez depuis Package Center. Ce n’est pas une régression — la route par le
planificateur ne le relançait pas non plus — mais mieux vaut le savoir avant de compter
dessus.
Un paquet qui s’installe, démarre, puis répond 404 à tout, c’est presque toujours le
répertoire des réponses : vérifiez avec sudo -u rescriptum rescriptum-cli check. Sur un
NAS avec un dossier partagé chiffré, c’est aussi à cela que ressemble un démarrage avant que
le volume soit déverrouillé — déverrouillez-le et redémarrez le paquet.
Vérifier
$ curl http://IP_DU_NAS:8000/health
OK
Sans le paquet
La route manuelle fonctionne toujours, et c’est le choix honnête si vous préférez ne rien installer du tout.
Utilisez le build armv7-unknown-linux-gnueabihf (ou x86_64-unknown-linux-musl, ou
aarch64-unknown-linux-musl pour un modèle ARM plus récent) de la
page des releases, ou compilez-en un
vous-même (voir construire).
$ scp rescriptum admin@nas:/volume1/netboot/rescriptum
$ ssh admin@nas chmod +x /volume1/netboot/rescriptum
$ ssh admin@nas mkdir -p /volume1/netboot/answers
Si ARMv7 se comporte mal, confirmez la vraie architecture avant de supposer :
$ ssh admin@nas uname -m
armv7l
Prenez le build ARMv7 publié, pas un build musl que vous auriez fait vous-même. Le
binaire armv7 publié est lié à la glibc 2.17, que DSM possède ; un build musl du même code
s’installe, répond à --version, puis meurt dès qu’il veut l’heure. Les noyaux 3.10 de
Synology répondent EINVAL aux appels time64 là où musl 1.2 n’attend qu’ENOSYS pour se
replier — la page de build
porte la mesure. Les builds x86_64 et aarch64 sont en musl statique et ne sont pas
concernés.
$ file rescriptum
ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...
RESCRIPTUM_ANSWERS_DIR vaut /srv/answers par défaut, qui n’existe pas sur DSM : il faut
donc la définir explicitement. Le fichier d’environnement ci-dessous est l’endroit le plus
propre pour le faire.
Panneau de configuration → Planificateur de tâches → Créer → Tâche déclenchée → Script défini par l’utilisateur
| Champ | Valeur |
|---|---|
| Événement | Démarrage |
| Utilisateur | root |
| Commande | voir ci-dessous |
Si vous utilisez un jeton, ne le mettez pas dans cette case. Tout ce qui se trouve dans
les arguments d’un processus — et, dans le cas de DSM, dans la définition de la tâche — est
lisible par tous les utilisateurs de la machine via ps. Mettez la configuration dans un
fichier réservé à root et nommez-le :
# /volume1/netboot/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_ANSWERS_DIR=/volume1/netboot/answers
RESCRIPTUM_LOG_FILE=/volume1/netboot/rescriptum.log
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/volume1/netboot/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
RESCRIPTUM_ANSWER_TOKEN=…
# l'entrée du planificateur de tâches exécute ceci
RESCRIPTUM_ENV_FILE=/volume1/netboot/rescriptum.env exec /volume1/netboot/rescriptum
Préférez ceci au sourcing. La forme plus ancienne —
. /volume1/netboot/rescriptum.env && exec … — fonctionne, et fonctionne toujours, mais
elle échoue silencieusement : oubliez le . du début, tapez une ligne de travers, ou
ratez les permissions, et le shell ne source rien pendant que le serveur démarre sur ses
valeurs par défaut — le répertoire de réponses par défaut, aucun jeton d’administration,
et pas un mot dans le journal. Avec RESCRIPTUM_ENV_FILE, le binaire lit le fichier
lui-même et refuse de démarrer s’il ne peut pas. Il prévient aussi si le fichier est
lisible par quelqu’un d’autre que root, et nomme toute clé qu’il ne reconnaît pas, de sorte
qu’un RESCRIPTUM_ADMIN_TOKENN est attrapé plutôt qu’ignoré en silence.
Les détails du format sont dans la référence de configuration.
Lancez la tâche une fois à la main depuis le planificateur plutôt que d’attendre un reboot pour découvrir qu’elle ne marche pas. Puis ouvrez le port dans le pare-feu par son numéro, et faites tourner le journal vous-même — le serveur ne le fait pas, et rien d’autre non plus.
Remplacer une instance en cours
$ ./deploy.sh admin@nas
Il compile pour ARMv7, vérifie les réponses d’abord, copie le
binaire sous un nom temporaire pour qu’un fichier à moitié copié ne soit jamais exécuté, le
redémarre, et confirme que /health répond. Détails dans
déploiement.
L’entrée du planificateur reste ce qui le démarre après un reboot — deploy.sh ne remplace
que ce qui tourne maintenant. Sur une installation par paquet, passez par Package Center.
Arrêt
Les deux routes envoient SIGTERM, que le serveur gère : il arrête d’accepter et sort. Il
n’y a rien à perdre dans un cas comme dans l’autre.
Ce qu’on peut attendre d’un DS416j
512 Mo et un cœur ARMv7, ce n’est pas grand-chose, et il n’y a pas besoin que ça le soit. Mesuré sur un DS416j faisant tourner le paquet, à travers le réseau local : 3 à 4 ms pour composer et servir une réponse, aller-retour réseau compris, pour une machine revendiquée par un groupe et fusionnée avec son propre fichier. Une connexion coûte des kilooctets plutôt qu’un thread, le listing du répertoire est mis en cache et invalidé par la mtime plutôt que parcouru à chaque requête, et un groupe sans surcharge par machine est rendu une fois au chargement puis servi comme une chaîne préparée.
La chose qui vaut d’être sue : le travail sur le système de fichiers se fait sur un pool de
threads bloquants, parce que read_dir sur un NAS dont le disque dort n’est pas un appel
rapide, et bloquer un worker asynchrone bloquerait toutes les autres connexions qu’il
pilote.
Sécurité
Sécurité
Les documents de réponse portent root-password-hashed et root-ssh-keys. Qui peut les
lire peut se connecter à chaque machine que vous installez ; qui peut les écrire décide de
ces identifiants. C’est tout le modèle de menace, et il mérite d’être dit franchement.
L’endpoint de réponse est ouvert par défaut
Par défaut, quiconque atteint le port peut récupérer une réponse. Ce n’est pas un oubli :
la plupart des installateurs n’ont aucun identifiant à présenter. Un client kickstart qui
va chercher inst.ks=http://… n’a rien à offrir, et le refuser reviendrait à refuser
l’installation.
Le bon contrôle principal, c’est le réseau. Un VLAN de provisioning où ne se trouvent que des machines en cours de démarrage PXE vaut plus que n’importe quel jeton.
RESCRIPTUM_ANSWER_TOKEN
Proxmox peut présenter un identifiant quand son ISO a été préparée pour :
$ proxmox-auto-install-assistant prepare-iso … --answer-auth-token 'une-longue-chaine-aleatoire'
$ export RESCRIPTUM_ANSWER_TOKEN='une-longue-chaine-aleatoire'
L’installateur envoie alors Authorization: Bearer …, et le serveur refuse tout ce qui ne
l’a pas, en comparant en temps constant.
Les échecs ici sont journalisés mais jamais limités en débit. Une baie entière peut se trouver derrière une seule adresse, et l’exclure transformerait un mauvais jeton en déploiement raté. L’API d’administration, à laquelle aucun installateur ne parle, verrouille bel et bien.
Un jeton de moins de 16 caractères est un avertissement au démarrage, pas une erreur — refuser de démarrer laisserait un parc incapable de s’installer.
GET /health reste ouvert dans tous les cas, pour que la supervision ne s’éteigne pas.
Le jeton de l’API d’administration
RESCRIPTUM_ADMIN_TOKEN est une chose différente protégeant une surface différente, et il est
traité en conséquence. L’API d’administration décide du mot de passe root et des clés SSH de
chaque machine installée ensuite, donc :
- elle ne partage jamais le listener de l’endpoint de réponse — elle a son propre
RESCRIPTUM_ADMIN_ADDR; - le serveur refuse de démarrer sans jeton, avec un jeton de moins de 16 caractères, ou au-dessus du store fichiers — des erreurs, pas des avertissements ;
- une adresse qui insiste est exclue : cinq échecs en une minute valent un blocage, doublant à chaque récidive jusqu’à quinze minutes, et le blocage s’applique aussi à un jeton correct venant de cette adresse — sinon deviner jusqu’à tomber juste ne coûterait rien.
Générez-en un vrai. Pas un mot auquel vous avez pensé :
$ openssl rand -hex 24 # ou : head -c 24 /dev/urandom | base64
Détails complets sur la page de l’API d’administration.
Pourquoi une comparaison en temps constant
Un == ordinaire s’arrête dès que deux octets diffèrent, donc un mauvais jeton partageant un
préfixe plus long met mesurablement plus de temps à être rejeté. Cette différence suffit à
récupérer un jeton octet par octet — quelques milliers de requêtes plutôt qu’un nombre
impossible. Comparer tous les octets quoi qu’il arrive supprime le signal.
Sur un réseau, le timing se perd généralement dans la gigue, donc c’est une précaution. Ça coûte cinq lignes.
Ne mettez pas un jeton sur une ligne de commande
Tout ce qui se trouve dans les arguments d’un processus est visible par tous les autres
utilisateurs de la machine via ps. Cela inclut le mettre directement dans une tâche
planifiée DSM. Gardez-le dans un fichier réservé à root :
# /etc/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/srv/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
Puis donnez-le au serveur — EnvironmentFile=/etc/rescriptum.env sous systemd, ou
RESCRIPTUM_ENV_FILE=/etc/rescriptum.env
partout ailleurs. La seconde forme fait lire le fichier par le binaire : un fichier
illisible devient donc une erreur de démarrage plutôt qu’un serveur tournant discrètement
sans le jeton que vous croyiez avoir posé. Le fichier n’est jamais découvert tout seul — il
n’y a pas de ./.env, délibérément : ce processus tourne en root, et un fichier ramassé
dans le répertoire courant serait un moyen de donner le jeton admin à quelqu’un.
Ce que le serveur refuse de lui-même
| Traversée de chemin | un chemin de système de fichiers n’est jamais construit à partir de données de requête. Seules les entrées directes du répertoire de réponses sont lues. Les identifiants arrivant à l’API d’administration n’acceptent que lettres, chiffres et - _ . :, parce qu’export les retransforme en noms de fichiers |
| Corps surdimensionnés | un Content-Length invraisemblable est refusé depuis l’en-tête, avant toute lecture ; le corps est plafonné à 1 Mo quoi qu’il arrive |
| Clients lents | un délai de lecture des en-têtes et une échéance sur la connexion entière, pour qu’un client qui promet un corps sans l’envoyer ne puisse pas garer une connexion |
| Rafales | au-delà de RESCRIPTUM_MAX_CONNECTIONS en vol, un 503 immédiat et fermeture, plutôt qu’une mise en file jusqu’à l’épuisement mémoire |
| Entrées malformées | un échec de parsing est une réponse d’erreur et une ligne de log, jamais un panic emportant une connexion — ou un serveur — en pleine installation |
TLS
Le serveur parle HTTP en clair. Sur un réseau de provisioning de confiance c’est normalement très bien, et c’est ce qui garde le binaire petit et sans dépendance.
Si vous avez besoin de TLS — certaines versions d’installateur veulent une empreinte de certificat lors d’une récupération en HTTPS — terminez-le devant avec nginx ou Caddy et pointez l’ISO dessus. L’endpoint de réponse se moque de ce qu’il y a en amont.
L’API d’administration est le seul endroit où cela compte par défaut : elle parle aussi HTTP en clair, donc le jeton traverse le réseau en clair. Sur la boucle locale c’est sans objet. Ailleurs, mettez un proxy terminant TLS devant.
L’application de bureau
Sur Synology uniquement, et uniquement là — l’application DSM
fait partie du paquet, pas du serveur. Son backend est un CGI que DSM sert depuis
/webman/3rdparty/rescriptum/, et deux choses concernant ce chemin décident de tout son
modèle de sécurité. Les deux ont été mesurées sur une machine DSM 7.2.2 plutôt que lues
dans un guide, qui n’en mentionne aucune :
- Un CGI y tourne sous le propriétaire du script. DSM attribue l’arborescence d’un
paquet à l’utilisateur du paquet : le backend tourne donc en tant que
rescriptum, la même identité qui possède le fichier d’environnement en0600et le journal. C’est ce qui lui permet d’éditer la configuration et de lire le journal pendant que le serveur est arrêté, c’est-à-dire précisément quand un panneau de réglages sert à quelque chose. Ce n’est pas root, et il ne peut devenir personne : il n’a aucun droit de démarrer ou d’arrêter le paquet, et c’est pourquoi le redémarrage passe par l’API de DSM avec la session de l’administrateur. (Un script resté possédé par root, lui, tourne bien en root là-bas. Bon à savoir, et à ne jamais faire.) - DSM n’authentifie pas ce chemin. Une requête non authentifiée atteint le script et reçoit une réponse. DSM protège ses propres pages ; celles d’un paquet regardent le paquet.
Mis ensemble : les contrôles à l’intérieur du script sont la seule chose devant lui. Il en fait donc trois, dans cet ordre, avant de toucher à quoi que ce soit.
- Une session DSM. Il exécute l’
authenticate.cgide DSM, qui affiche le nom de l’utilisateur connecté et n’affiche rien du tout s’il n’y a pas de session. - Un administrateur. Être connecté ne suffit pas ; l’utilisateur doit appartenir à
administrators. Moins que cela laisserait n’importe quel compte du NAS fixer le mot de passe root de chaque machine qu’il installe. - L’intention, pour une écriture. Une écriture doit porter un en-tête que l’application
envoie et qu’un formulaire d’un autre site ne peut pas : un navigateur n’envoie pas un
en-tête inventé en cross-origin sans un préalable (preflight), et ce script n’y répond
pas. Le
SynoTokende DSM est envoyé en plus, ce qui garde l’application fonctionnelle avec la protection contre la falsification de requête inter-sites activée.
check-spk.sh vérifie que les deux premiers sont toujours dans le script, et
lifecycle-test.sh le pilote avec un authentificateur bouchonné pour prouver que les trois
refusent réellement. Ils ont été vus échouer : retirer le contrôle de session fait passer
quatre verts au rouge.
L’application ne reçoit jamais de jeton. RESCRIPTUM_ANSWER_TOKEN et
RESCRIPTUM_ADMIN_TOKEN lui parviennent comme défini ou non défini, et rien de plus —
la commande qu’elle appelle refuse d’afficher un identifiant, quoi qu’on lui demande.
Connu et accepté
- La limitation par adresse n’arrête pas un attaquant disposant de nombreuses adresses. C’est la longueur du jeton d’administration qui rend la devinette sans espoir — d’où le plancher de 16 caractères.
- L’endpoint de réponse n’est pas limité en débit du tout, délibérément, pour la raison ci-dessus.
- Binder l’API d’administration au-delà de la boucle locale est votre choix, et le
serveur le dit dans le log quand vous le faites.
127.0.0.1plus un tunnel SSH est le défaut sûr.
Capturer les requêtes
Capturer les requêtes
L’essentiel de ce que rescriptum sait des installateurs vient de leur documentation. Tant qu’un vrai installateur ne lui a pas parlé, c’est une affirmation plutôt qu’un fait — et quand un déploiement dérape, « qu’est-ce que node07 a réellement envoyé ? » est généralement la seule question qui vaille.
$ export RESCRIPTUM_CAPTURE_DIR=/var/log/rescriptum-captures
Désactivé sauf si défini.
Ce qu’il écrit
Deux fichiers par requête :
20260824T084337Z-10.0.0.42-0000.body le corps, verbatim
20260824T084337Z-10.0.0.42-0000.meta qui a demandé, et ce qu'il a reçu
time: 2026-08-24T08:43:37Z
peer: 10.0.0.42:51234
request: POST /proxmox/answer
body-bytes: 1876
outcome: 200 format=toml machine=98fa9b50d810 group=rack-a
Le .body est octet pour octet ce qui est arrivé, donc il se rejoue sans modification.
Le nom de fichier porte l’horodatage, l’adresse du pair (assainie — les deux-points d’un pair
IPv6 n’ont rien à faire dans un nom de fichier) et un numéro de séquence, pour que deux
requêtes dans la même seconde n’entrent pas en collision.
En rejouer une
$ rescriptum render --body /var/log/rescriptum-captures/20260824T084337Z-10.0.0.42-0000.body
Cela résout exactement comme le serveur l’a fait, hors ligne, sans aucune machine — ce qui rend une mauvaise réponse débogable à votre bureau plutôt que devant une baie.
C’est aussi la meilleure façon de construire des sélecteurs contre un format de corps que
vous n’avez jamais vu : capturez une vraie requête, puis itérez avec render --body jusqu’à
ce qu’elle se résolve comme vous le vouliez.
Les limites, et pourquoi
- Plafonné à 1000 captures. Un serveur de provisioning qui remplit son propre disque est pire qu’un qui ne capture rien. En atteignant le plafond, il le signale une fois et arrête d’écrire. Le compte porte sur les captures, pas sur les fichiers, et il survit à un redémarrage : le serveur compte ce qui se trouve déjà dans le répertoire avant d’écrire.
- Rien n’est jamais supprimé. Faire tourner ou vider le répertoire vous incombe ; le serveur compte ce qui s’y trouve déjà au démarrage pour qu’un redémarrage ne dépasse pas le plafond.
- Un échec de capture ne fait jamais échouer une requête. Il est journalisé, et l’installation continue. Perdre un diagnostic ne vaut pas de perdre une installation.
Avant d’en joindre une à un rapport de bug
Un corps capturé est un inventaire matériel : adresses MAC, numéros de série de disques, DMI.
Le .meta dit quelle réponse a été reçue. Ni l’un ni l’autre ne contient vos hachages de mot
de passe — mais la réponse, si. Nettoyez donc tout ce que vous collez à côté.
Voir aussi
- Dépannage — lire le log, et les causes habituelles.
- Validation —
rendersous ses autres formes.
Le store SQLite
Le store SQLite
Un répertoire de fichiers est le défaut, et c’est la bonne réponse pour une poignée de machines : greppable, diffable, dans git si vous voulez, sans base à faire tourner, sauvegarder ou migrer.
Pour un parc administré par outillage plutôt qu’à la main, les mêmes réponses peuvent vivre dans une base SQLite. Elle est compilée dans le binaire, donc il n’y a toujours rien à installer.
$ export RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db
$ rescriptum import /srv/answers # faire passer les fichiers
$ rescriptum check # les mêmes vérifications, sur la base
$ rescriptum # servir depuis elle
Pourquoi vous le feriez
- L’API d’administration en a besoin. Gérer les réponses en HTTP exige la base ; au-dessus de fichiers il y aurait deux façons de changer la même configuration — à la main et par le réseau — en concurrence l’une avec l’autre.
- Les écritures concurrentes sont sûres. Mode WAL, donc une écriture administrative ne bloque jamais une installation en cours.
- Un seul fichier à sauvegarder, et il se déplace atomiquement.
Pourquoi peut-être pas
- Un répertoire est lisible.
git log answers/répond à « qui a changé cette baie et pourquoi » ; une base non, à moins que votre outillage ne l’enregistre. - Un fichier s’édite avec n’importe quoi.
vi,scp, un Makefile. - C’est 1,2 Mo de binaire — 2,4 Mo avec SQLite contre 1,3 Mo sans, sur ARMv7. Construisez
avec
cargo build --no-default-featuressi cela compte et que vous n’en avez pas besoin.
Le comportement est identique
Correspondance, groupes, extends, fusion, templating, render, check — tout cela vit
au-dessus du store, qui est délibérément mince : il rend le texte brut des documents et un
jeton de version bon marché, et ne décide de rien.
Ce n’est pas affirmé mais imposé. tests/stores.rs fait tourner chaque cas de comportement
deux fois, une fois par store, et exige le résultat identique. Un nouveau comportement a sa
place dans cette suite, pas dans un test propre à un store.
Passer de l’un à l’autre
$ rescriptum import /srv/answers # répertoire → store configuré
$ rescriptum export /tmp/backup # store configuré → répertoire
$ RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db rescriptum import examples
copying files:examples -> sqlite:/srv/answers.db
10 group(s), 8 machine(s)
ok — now run `check` against the target
L’aller-retour est identique octet pour octet. Importez un répertoire, réexportez-le, et
diff -r ne signale rien — commentaires, mise en forme et tout le reste. C’est ce qui rend
la base sûre à adopter et sûre à quitter, et cela vaut la peine de rester vrai.
Les deux sens lancent check sur votre initiative plutôt qu’automatiquement ; la sortie
ci-dessus vous le dit.
Versions de schéma
La base porte une version de schéma (user_version). Il n’y en a qu’une pour l’instant, et
rien n’a été publié sous une plus ancienne : il n’y a donc rien à migrer.
Ce à quoi sert la version, c’est l’autre sens : un binaire plus ancien refuse d’ouvrir une base écrite par un plus récent plutôt que de deviner ce qui a changé.
database schema is version 2, this binary understands 1
Un retour arrière au-delà d’un futur changement de schéma a donc besoin de l’export d’avant
la mise à jour, ou d’un binaire assez récent pour lire la base. Gardez un export sous la
main quand vous en franchissez un.
Notes d’exploitation
version()est un atomique en mémoire, pas une requête, parce qu’il est appelé à chaque requête HTTP. Un changement fait par un autre processus est rattrapé par le filet de rechargement d’une seconde.- Le fichier de base et ses compagnons
-wal/-shmdoivent tous être accessibles en écriture, et appartiennent tous à la même sauvegarde. RESCRIPTUM_DB_PATHvaut par défaut/srv/answers.db, voisin du répertoire de réponses par défaut. La base contient le même contenu curé, pas de l’état d’exécution : elle a sa place dans le même arbre.- Le répertoire parent est créé s’il n’existe pas.
Voir aussi
- L’API d’administration — la raison pour laquelle la plupart des gens activent ceci.
- Comment les stores sont construits — le trait, et pourquoi il est mince.
L'API d'administration
L’API d’administration
Avec RESCRIPTUM_STORE=sqlite, les réponses peuvent être gérées en HTTP plutôt qu’en
éditant des fichiers. Elle est désactivée sauf si vous la configurez, et elle tourne sur
son propre listener.
$ export RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db
$ export RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
$ export RESCRIPTUM_ADMIN_TOKEN=$(openssl rand -hex 24)
$ rescriptum
2026-08-24T08:52:30Z - admin API listening on 127.0.0.1:8001
2026-08-24T08:52:30Z - rescriptum 0.1.0 listening on 0.0.0.0:8000 — store=sqlite:/srv/answers.db …
Trois propriétés porteuses
1. Son propre listener. L’endpoint de réponse n’est pas authentifié par nécessité — l’installateur n’a aucun identifiant à offrir. Cette API décide du mot de passe root et des clés SSH de chaque machine installée ensuite. Elle ne partage jamais ce port.
2. SQLite uniquement. Au-dessus d’un répertoire de fichiers il y aurait deux façons de changer la même configuration, à la main et par le réseau, en concurrence.
3. Une écriture ne peut jamais laisser le jeu de réponses cassé. Chaque écriture prend un
instantané des problèmes courants, s’applique, puis compare. Tout ce qui est nouvellement
cassé est annulé et répondu 409.
Le serveur refuse de démarrer — en erreur, pas en avertissement — si vous pointez l’API d’administration sur le store fichiers, omettez le jeton, ou définissez un jeton de moins de 16 caractères.
Endpoints
| Requête | Rôle |
|---|---|
GET /machines, GET /groups | lister les identifiants |
GET /machines/{id}, GET /groups/{name}, GET /default | le document stocké, tel qu’écrit — commentaires et mise en forme intacts |
PUT /machines/{id}, PUT /groups/{name}, PUT /default | stocker un document (le corps est le document) |
DELETE /machines/{id}, DELETE /groups/{name}, DELETE /default | en supprimer un |
GET /resolve/{id} | la réponse fusionnée que cette machine recevrait |
GET /check | les problèmes actuels, le même jeu que la sous-commande check |
GET /health | vivacité — le seul endpoint sans jeton, et jamais bloqué |
Chaque endpoint nommant un document prend ?format= — l’extension du document. Il vaut
toml par défaut, ce que ce serveur servait à ses débuts :
$ curl -H "$AUTH" -X PUT --data-binary @base.preseed \
'http://127.0.0.1:8001/groups/base?format=preseed'
Comme la clé d’un document est (identifiant, format), un identifiant
apparaît dans GET /machines une fois par format dans lequel il existe — une machine qui
est à la fois un nœud Proxmox et un nœud Debian est listée deux fois.
Exemples
$ AUTH="Authorization: Bearer $RESCRIPTUM_ADMIN_TOKEN"
$ curl -s -H "$AUTH" http://127.0.0.1:8001/groups
{"group":["base","example-rack","rhel-compute","ubuntu-web"]}
$ curl -s -H "$AUTH" -X PUT --data-binary @rack-a.toml \
http://127.0.0.1:8001/groups/rack-a
{"status":"stored","problems":[]}
$ curl -s -H "$AUTH" http://127.0.0.1:8001/resolve/98:fa:9b:50:d8:10
[global]
country = "fr"
keyboard = "fr"
…
GET /resolve renvoie aussi l’en-tête de réponse X-Answer-Source, portant la même
description que la ligne de log :
x-answer-source: format=toml machine=98fa9b50d810 group=example-rack
Répéter une vraie requête
GET /resolve accepte les mêmes étiquettes que porterait une vraie requête, ce qui permet de
répéter une URL particulière — la différence entre /user-data et /meta-data, par exemple :
$ curl -s -H "$AUTH" 'http://127.0.0.1:8001/resolve?path=/rhel/ks&serial=7ABC123'
Quand une query string est présente, l’identifiant du chemin est ignoré — les faits
viennent de la query seule. Donc GET /resolve/98:fa:9b:50:d8:10?format=toml ne résout
rien, parce que format=toml n’est pas une identité. Utilisez la forme sans query, ou
mettez l’identité dans la query : ?mac=98:fa:9b:50:d8:10.
Elle ne vous laissera pas casser le parc
Chaque écriture est vérifiée après application. Si elle a introduit un problème — un cycle
entre groupes, un document référençant un groupe qui n’existe plus — l’écriture est
annulée et vous recevez un 409 disant ce que vous avez cassé :
$ curl -s -H "$AUTH" -X DELETE 'http://127.0.0.1:8001/groups/base?format=preseed'
{"error":"refused: this would break the answer set (rolled back)",
"problems":["machine \"98fa9b50d810\": extends unknown group \"base\""]}
Deux choses découlent de ce fonctionnement :
- Une écriture réussie signale quand même les problèmes préexistants, dans le tableau
problems. Une réponse propre n’implique jamais que tout le jeu est sain — seulement que vous ne l’avez pas aggravé. - C’est pourquoi un
extendsde machine pointant sur un groupe manquant est détecté au chargement plutôt qu’au moment où cette machine demande. Le garde-fou ne peut attraper que ce que le rapport de problèmes connaît.
Les documents malformés sont également refusés à l’écriture, plutôt que de devenir un 500
la prochaine fois qu’une machine les demande :
$ curl -s -H "$AUTH" -X PUT --data-binary 'x = = 1' http://127.0.0.1:8001/machines/aa-bb-cc-dd-ee-01
{"error":"document: invalid TOML: TOML parse error at line 1, column 5 …"}
Identifiants
Lettres, chiffres et - _ . : uniquement. Ils deviennent des noms de répertoires sous
export et dans le store fichiers, donc tout ce qui pourrait traverser un répertoire est
rejeté — à la frontière de l’API et dans les deux stores. groups et default sont
réservés comme identifiants de machine pour la même raison : ce sont les répertoires que
l’agencement garde pour lui, et une base qui en accepterait un exporterait vers un répertoire
incapable de le contenir.
Codes de statut
| Code | Signifie |
|---|---|
200 | fait |
400 | document malformé, identifiant invalide, ou corps non-UTF-8 |
401 | jeton manquant ou faux |
404 | document ou endpoint inexistant ; rien ne se résout pour cet identifiant |
409 | l’écriture aurait cassé le jeu de réponses (annulée), ou un resolve qui n’a pas pu rendre |
413 | document de plus de 256 Ko |
429 | cette adresse est bloquée après des échecs d’authentification répétés |
500 | le store n’a pas pu être lu ou écrit |
Veiller sur le jeton
Le jeton constitue toute l’authentification, et ce qu’il protège mérite d’être dit
franchement : les documents de réponse portent root-password-hashed et root-ssh-keys,
donc quiconque peut écrire sur cette API décide des identifiants root de chaque machine que
vous installerez ensuite.
Générez-en un vrai — pas un mot auquel vous avez pensé :
$ openssl rand -hex 24 # ou : head -c 24 /dev/urandom | base64
Ne le mettez pas sur une ligne de commande. Tout ce qui se trouve dans les arguments d’un
processus est visible par tous les autres utilisateurs via ps, ce qui inclut le mettre
directement dans une tâche planifiée DSM. Gardez-le dans un fichier réservé à root et
sourcez-le — voir
Sécurité.
Ce que le serveur fait de son côté :
- Compare le jeton en temps constant, pour qu’il ne puisse pas être récupéré octet par octet par qui chronomètre les réponses.
- Exclut une adresse qui insiste. Cinq échecs en une minute valent un blocage, doublant à chaque récidive jusqu’à un maximum de quinze minutes, et chaque tentative est journalisée. Le blocage s’applique aussi à un jeton correct venant de cette adresse — sinon deviner jusqu’à tomber juste ne coûterait rien.
- Borne sa propre comptabilité à 4096 adresses suivies, pour que le garde-fou ne puisse pas lui-même être transformé en fuite de mémoire.
- Laisse
GET /healthnon authentifié et non bloqué, pour que la supervision ne s’éteigne pas pendant une attaque.
2026-08-24T08:52:32Z - admin: 10.0.0.9 failed authentication 5 times — blocked for 60s
Deux limites à prévoir
- Elle parle HTTP en clair, donc le jeton traverse le réseau en clair. Sur la boucle locale c’est sans objet. Ailleurs, mettez un reverse proxy terminant TLS devant.
- Le blocage par adresse n’arrête pas un attaquant disposant de nombreuses adresses. C’est la longueur du jeton qui rend la devinette sans espoir — d’où le plancher de 16 caractères au démarrage.
Binder au-delà de la boucle locale est votre choix, et le serveur le dit dans le log quand vous le faites :
2026-08-24T08:52:30Z - warning: the admin API is not bound to loopback — it rewrites what gets installed on every machine, so restrict it to a management network
127.0.0.1 plus un tunnel SSH est le défaut sûr.
Voir aussi
- Le store SQLite — le prérequis.
- Comment le garde-fou est construit — les internes.
Dépannage
Dépannage
Quand une installation PXE ne démarre pas, le log est le seul diagnostic dont dispose qui que
ce soit — il est donc délibérément ennuyeux et greppable : une ligne par requête, sur
stderr. Les deux moitiés se règlent :
RESCRIPTUM_LOG jette les requêtes qui ont
abouti, et RESCRIPTUM_LOG_FILE envoie les lignes dans un fichier.
2026-08-24T08:43:36Z - rescriptum 0.1.0 listening on 127.0.0.1:8999 — store=files:answers workers=10 max_conn=2048 timeout=10s
2026-08-24T08:43:37Z 127.0.0.1:61720 GET /health 200
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=example-rack bytes=431
2026-08-24T08:43:37Z 127.0.0.1:61722 GET /rhel/ks?serial=7ABC123 body=0 200 format=text group=rhel-compute bytes=747
2026-08-24T08:43:37Z 127.0.0.1:61723 POST /answer body=27 404 no answer file applies
Lire une ligne
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=example-rack bytes=431
└─ horodatage UTC └─ pair └─ requête └─ corps └─ statut
└─ comment la réponse a été composée └─ octets envoyés
Les lignes portant - à la place d’une adresse de pair sont au niveau serveur : démarrage,
échecs d’accept, délestage, problèmes de chargement du jeu de réponses.
format=… nomme la famille (toml, yaml, json, xml, text) plutôt que
l’extension — ks et preseed remontent tous deux comme text.
Échecs courants
| Symptôme | Cause probable |
|---|---|
404 no answer file applies | Rien n’a revendiqué la requête et il n’y a pas de default pour le format demandé. Capturez le corps et vérifiez que la MAC y est vraiment |
404 sur une URL qui marchait | L’URL nomme maintenant un alias de format qui exclut votre document — /ubuntu/answer ne servira pas un .toml |
500 … extends unknown group | Un document référence un groupe inexistant. Délibéré : servir une configuration dont la base manque installerait la machine à moitié configurée |
500 sur une seule machine | Le document de cette machine, ou son groupe, ne parse pas. La raison est sur la même ligne de log. rescriptum check le trouve sans attendre que la machine demande |
500 template needs {{ … }} | Un placeholder que la requête n’a pas pu remplir. Jamais servi comme chaîne vide, volontairement |
401 bad or missing token | RESCRIPTUM_ANSWER_TOKEN est défini mais l’ISO n’a pas été préparée avec le même --answer-auth-token |
413 | Un corps de plus de 1 Mo, ou un Content-Length qui en annonce un. Refusé depuis l’en-tête, avant toute lecture |
503 | Plus de connexions simultanées que RESCRIPTUM_MAX_CONNECTIONS. Augmentez-le, ou trouvez qui se connecte |
| Réponse servie, installation quand même ratée | Le document est du TOML valide mais pas du Proxmox valide. Passez render dans validate-answer |
| L’installateur ne contacte jamais le serveur | L’URL de l’ISO ou un pare-feu, pas ce serveur. curl http://SERVER:8000/health depuis le même réseau |
Le serveur démarre mais tout part en 404
Regardez la ligne de démarrage. Les deux causes habituelles s’annoncent :
warning: /srv/answers does not exist yet — every request will 404 until it does
warning: /srv/answers cannot be read: Permission denied (os error 13) — every request
will 404 until that is fixed; check the directory's owner against the user this server
runs as
Le second est ce que vous obtenez quand le répertoire existe mais que le processus ne peut pas le lister — la cause habituelle est un répertoire créé en root et un serveur tournant sous quelqu’un d’autre. La question est posée au système de fichiers plutôt que déduite des bits de permission, donc elle tient compte du propriétaire, du groupe, des ACL et du montage.
… store=files:/srv/answers …
— cette seconde est le répertoire de réponses par défaut. Si vous en vouliez un autre,
RESCRIPTUM_ANSWERS_DIR n’a pas atteint le processus. Une valeur vide ou
composée d’espaces est traitée comme non définie, et un nombre nul ou impossible à parser
retombe sur sa valeur par défaut.
Le reproduire hors ligne
C’est le chemin le plus rapide entre « une machine a reçu la mauvaise chose » et un correctif :
$ export RESCRIPTUM_CAPTURE_DIR=/var/log/rescriptum-captures # puis laissez-le échouer une fois de plus
$ rescriptum render --body /var/log/rescriptum-captures/2026…-0000.body
render résout exactement comme le serveur, donc ce qu’il affiche est ce que cette machine
aurait reçu. Pas besoin de baie. Voir capturer les requêtes.
Sans capture, répétez depuis l’identité et l’URL :
$ rescriptum render --query "path=/rhel/ks&mac=98:fa:9b:50:d8:10&serial=7ABC123"
Ajoutez path= — sans lui, la résolution n’est pas contrainte par le format et peut choisir
un document que la vraie URL aurait exclu, ce qui est exactement le bug que vous pourriez être
en train de chasser.
Vérifier l’ensemble
$ rescriptum check
Problèmes de chargement, chaque machine et chaque membre de groupe rendus, et le validateur de l’installateur lancé là où il est dans le PATH. Détails dans validation.
Signaler quelque chose
Pour une mauvaise réponse, le rapport utile est ce que la machine a envoyé et ce qu’elle a
reçu — le .body et le .meta d’une capture, plus la ligne de log.
Nettoyez les hachages de mot de passe et les clés SSH avant de joindre quoi que ce soit :
la réponse n’est dans le outcome de la capture que par son nom, mais si vous collez aussi le
document rendu, il porte de vrais identifiants.
Ouvrez un ticket avec cela et la version tirée de la ligne de démarrage.
Servir les médias de démarrage
Servir les médias de démarrage
Une réponse dit à une machine comment s’installer. Elle ne dit rien de l’endroit d’où vient l’installeur — et jusqu’ici c’était le serveur web de quelqu’un d’autre, hébergeant des images que personne ne confrontait aux réponses écrites pour elles.
Avec un répertoire de médias, le même serveur fait les deux. La MAC d’une machine choisit sa réponse et l’image pour laquelle cette réponse a été écrite, et les deux ne peuvent plus diverger puisqu’un seul composant décide des deux.
$ export RESCRIPTUM_MEDIA_DIR=/srv/media
Non défini, tout est éteint. Rien ne change pour un déploiement existant tant que vous ne la définissez pas.
Où vivent les images de base
Aucune image d’installation n’est dans ce projet, ni dans une version publiée. Une
ISO est l’artefact de quelqu’un d’autre, elle pèse un à quatre gigaoctets, et elle change
à son propre rythme — trois raisons distinctes pour qu’elle vive sur votre disque plutôt
que dans le nôtre. RESCRIPTUM_MEDIA_DIR est l’endroit où vous les gardez, et ce
répertoire est l’archive : ce que l’éditeur a publié, sur disque, jamais modifié
ensuite.
Ce dernier point est une propriété, pas une promesse. Rien ici ne réécrit une image —
en préparer une produit un fichier compagnon et une injection appliquée au fil de
l’eau (voir Préparer une image Proxmox), de sorte que les
octets sur disque restent exactement ce que l’éditeur a publié et que leur empreinte
reste vérifiable contre le SHA256SUMS de l’éditeur. media list dit quelles entrées
sont l’archive et lesquelles en dérivent.
Faire entrer une image
Trois façons, et la première est celle à privilégier.
Choisir dans un catalogue
$ rescriptum media sources
SOURCE NAME WHAT IT INSTALLS
proxmox-ve Proxmox VE the founding case — answers come from a file injected into the image
debian Debian netinst images; the answer is a preseed on the kernel command line
ubuntu Ubuntu LTS autoinstall, via a cloud-init datasource on the kernel command line
almalinux AlmaLinux 9 kickstart, named on the kernel command line
rocky Rocky Linux 9 kickstart, named on the kernel command line
$ rescriptum media sources proxmox-ve
reading https://enterprise.proxmox.com/iso/SHA256SUMS …
Proxmox VE — the founding case — answers come from a file injected into the image
proxmox-ve_9.2-1.iso
proxmox-ve_9.2-1-arm64.iso
proxmox-ve_9.1-1.iso
$ rescriptum media add --from proxmox-ve proxmox-ve_9.2-1.iso
Rien concernant une image précise n’est stocké dans ce serveur. Chaque catalogue nomme l’index de sommes que l’éditeur publie déjà à côté de ses propres images, et les noms comme les empreintes en sont lus au moment où vous demandez — la liste est donc celle que cet éditeur a aujourd’hui, et l’empreinte est la sienne. Une table d’URL figée dans une version publiée proposerait les images du trimestre dernier, dont certaines supprimées depuis.
Ce que cela vaut, dit franchement. Prendre l’empreinte sur le serveur qui sert l’image n’est pas une vérification de signature. En HTTPS cela authentifie le domaine de l’éditeur et cela attrape un téléchargement tronqué, un miroir corrompu et un fichier qui a changé en dessous — l’essentiel de ce qui arrive vraiment — et rien de plus. Si vous voulez davantage, utilisez la section suivante avec une empreinte que vous avez obtenue vous-même.
Laisser le serveur la récupérer
$ rescriptum media add https://enterprise.proxmox.com/iso/proxmox-ve_8.4-1.iso \
--sha256 9f86d081884c7d65…
fetching https://enterprise.proxmox.com/iso/proxmox-ve_8.4-1.iso
with curl, into /srv/media/proxmox-ve_8.4-1.iso.part
######################################################################## 100.0%
verifying 1.5G …
fetched 1.5G via curl, digest verified
Elle atterrit sous un nom en .part et n’est renommée qu’une fois l’empreinte
vérifiée : un téléchargement partiel ne devient jamais une entrée du catalogue — le
catalogue analyse ce qu’il trouve, et une ISO tronquée s’analyse comme une image inconnue
qu’une machine essaierait ensuite de démarrer. Une récupération interrompue laisse le
.part en place, et relancer la commande la reprend.
--sha256 est obligatoire ici, parce que rien d’autre ne vérifierait ce qui est
arrivé. Les éditeurs publient un SHA256SUMS à côté de l’image. Si vous voulez vraiment
vous en passer, dites --unverified — l’important est que sauter cette vérification soit
un acte délibéré et non le défaut, puisque cela décide ce que chaque machine du réseau
installe.
--as NOM.iso choisit le nom de fichier quand l’URL n’en implique pas d’utilisable.
::: tip Il n’y a pas de TLS dans ce binaire
rustls et un magasin de racines, c’est une quarantaine de crates et plus d’un mégaoctet
sur ARMv7, pour un travail dont chaque hôte a déjà l’outil. Donc ceci lance curl, ou
wget si c’est lui qui est installé, et le dit franchement s’il n’en trouve aucun — auquel
cas la réponse est celle ci-dessous.
:::
Ou la poser vous-même
En SMB, en scp, depuis là où l’ISO se trouve déjà — le geste naturel sur un NAS — puis
l’enregistrer :
$ rescriptum media add /srv/media/pve-8.4.iso --sha256 9f86d081884c7d65…
hashing /srv/media/pve-8.4.iso …
10% (152.0M of 1.5G)
…
pve-8.4 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
proxmox Proxmox Virtual Environment 8.4-1
kernel /boot/linux26
initrd /boot/initrd.img
wrote /srv/media/pve-8.4.media
--sha256 est facultatif et mérite d’être fourni : une empreinte qui ne correspond pas,
c’est soit un téléchargement tronqué soit le mauvais fichier, et les deux installeraient
la mauvaise chose sur chaque machine qui demande. Rien n’est enregistré en cas d’écart.
Rien n’est copié et l’image n’est jamais modifiée. Ce que media add écrit, c’est le
fichier compagnon .media posé à côté, qui retient l’empreinte et ce que la détection a
trouvé. C’est tout l’intérêt : hacher 1,5 Go prend près d’une minute, et le serveur ne
doit jamais passer une minute dans une requête.
Une image sans compagnon apparaît quand même et est servie quand même — elle n’a simplement pas d’empreinte à revérifier, et elle est analysée à la volée.
Ce qu’il sait dire d’une image
$ rescriptum media list
ID FAMILY ARCH VERSION SIZE PINNED
pve-8.4 proxmox x86_64 Proxmox Virtual Environment… 1.5G 9f86d0818
ubuntu-24.04 ubuntu x86_64 Ubuntu-Server 24.04.1 LTS 2.1G —
gparted-1.6 unknown — GPARTED-LIVE 420.0M —
Six familles sont reconnues — Proxmox, Debian, Ubuntu, RHEL et ses dérivés, SUSE et Fedora CoreOS — à partir d’une table de marqueurs situés dans l’image. Là où un éditeur a laissé une chaîne de version, elle est reprise ; l’identifiant de volume sert de repli.
Une image que rien ne reconnaît est quand même listée et quand même servie. Ne pas
savoir la décrire n’est pas la même chose que ne pas savoir s’en servir : elle peut être
sanbootée, écrite sur une clé, ou récupérée entière par le firmware. Ce qu’elle ne peut
pas faire, c’est produire une strophe de démarrage, et le serveur le dit plutôt que de
deviner.
Les points d’entrée
Le listener média a sa propre socket, sur 0.0.0.0:8001 par défaut.
| Route | Ce qui revient |
|---|---|
GET / | le catalogue en texte, ou en JSON avec Accept: application/json |
GET /<id>/iso | l’image |
GET /<id>/kernel | le noyau, diffusé depuis l’intérieur de l’image |
GET /<id>/initrd | l’initrd, de même |
GET /<id>/initrd+iso | l’initrd avec l’image ajoutée, pour les vieux chargeurs |
GET /<id>/file/<chemin> | n’importe quel fichier dans l’image |
GET /health | 200 OK |
Rien n’est extrait et rien n’est décompressé. Un fichier dans une image ISO9660 est une
plage d’octets contiguë : servir /pve-8.4/kernel est donc un positionnement et une
longueur — le même travail de quelques kilo-octets que l’image fasse 400 Mo ou 4 Go.
Les plages (Range), ETag, If-Range et HEAD sont tous traités, parce que les vrais
clients en ont besoin : casper d’Ubuntu et anaconda de Red Hat récupèrent tous deux par
plages, et le démarrage HTTP UEFI envoie un HEAD avant de récupérer quoi que ce soit.
Pourquoi c’est un second listener
Ce n’est pas une préférence — trois raisons distinctes, dont une seule suffirait :
- Le point de réponse répond sur n’importe quel chemin, puisque l’URL est gravée dans
une ISO. Un préfixe
/media/…découperait un espace réservé dans un espace délibérément ouvert. RESCRIPTUM_TIMEOUT_SECSest une échéance de connexion entière de dix secondes. Un transfert de 1,5 Go dure quinze secondes en gigabit et deux minutes en 100 Mbit : tous les téléchargements seraient tués en vol — et cela ressemblerait à un réseau instable, pas à un réglage.- Un téléchargement retient un jeton de connexion pendant des minutes. Partager ce budget avec les réponses, c’est un déploiement qui affame ses propres installations.
Les deux ont des budgets séparés, et un test le prouve au lieu de l’espérer : les réponses continuent d’aboutir avec quatre transferts en cours.
Démarrer une machine depuis tout ça
media ipxe écrit la strophe de démarrage d’une image :
$ rescriptum media ipxe pve-8.4
#!ipxe
# Proxmox Virtual Environment 8.4-1 — generated by `rescriptum media ipxe pve-8.4`.
# An ordinary answer document: selection, layering and templating all apply.
kernel http://192.0.2.10:8001/pve-8.4/kernel ramdisk_size=16777216 rw quiet initrd=initrd.img \
splash=silent proxmox-start-auto-installer
initrd http://192.0.2.10:8001/pve-8.4/initrd initrd.img
initrd http://192.0.2.10:8001/pve-8.4/iso proxmox.iso
boot
Il imprime un script, il n’en installe pas. Enregistrez-le dans le répertoire des réponses et c’est un document de réponse ordinaire — sélectionné, superposé et gabarisé comme n’importe quel autre :
$ rescriptum media ipxe pve-8.4 > /srv/answers/groups/rack-a/boot.ipxe
C’est bien le point. Le serveur ne devient pas malin sur le démarrage ; il gagne un
générateur, et le moteur de composition que vous avez déjà fait le reste. Un {{ mac }}
dans l’URL de réponse générée est rempli à chaque requête depuis les faits de la machine.
Chaque famille reçoit ce dont elle a réellement besoin, et elles ne se ressemblent pas :
| Famille | Comment la réponse lui parvient |
|---|---|
| Proxmox VE | dans l’image, via auto-installer-mode.toml — et proxmox-start-auto-installer sur la ligne de commande pour choisir la voie automatisée |
| Debian | preseed/url=… |
| Ubuntu | ds=nocloud-net;s=…/, d’où cloud-init récupère user-data et meta-data |
| Famille RHEL | inst.ks=… |
| SUSE | autoyast=… |
| Fedora CoreOS | ignition.config.url=… |
Proxmox est le cas à part, et il vaut la peine de savoir pourquoi : c’est le seul qui
porte l’emplacement de la réponse à l’intérieur de l’image plutôt que sur la ligne de
commande du noyau. C’est aussi pour cela que c’est le seul à devoir passer une fois par
prepare-iso — voir Préparer les médias d’installation.
::: tip Vous avez déjà lancé prepare-iso --pxe ?
Cela laisse un répertoire contenant vmlinuz, initrd.img et une ISO allégée. Pointez
RESCRIPTUM_MEDIA_DIR dessus et cela fonctionne tel quel : l’image allégée est toujours
reconnue comme Proxmox, et le noyau et l’initrd posés à côté sont trouvés et servis.
:::
Préparer une image Proxmox
Proxmox est la seule famille à porter l’emplacement de la réponse à l’intérieur de
l’image, dans /auto-installer-mode.toml. Cela imposait jusqu’ici de lancer
proxmox-auto-install-assistant prepare-iso ailleurs d’abord.
$ rescriptum media prepare pve-8.4
pve-8.4-http prepared from pve-8.4
answer http://192.0.2.10:8000/proxmox
injects /auto-installer-mode.toml (198 bytes)
image 1610612736 bytes (source 1610610688 + 2048 appended)
wrote /srv/media/pve-8.4-http.media
Nothing was copied. Serve it as /pve-8.4-http/iso, or write it to a stick with
rescriptum media export pve-8.4-http /tmp/pve-8.4-http.iso
Ce qui vient d’être écrit est un fichier compagnon : environ deux cents octets qui
tiennent lieu de 1,5 Go. La source n’est jamais modifiée, jamais copiée, et son
empreinte publiée reste vérifiable. Le fichier est injecté au fil de l’eau, donc
changer plus tard l’URL de réponse réécrit ces deux cents octets plutôt qu’un gigaoctet —
et les deux entrées apparaissent dans media list, adossées à une seule image sur disque.
--as NOM choisit le nom de l’entrée dérivée, et --url, --cert-fingerprint et
--token disent ce qui va dans le fichier.
Pour une clé USB
$ rescriptum media export pve-8.4-http /tmp/pve-auto.iso
Matérialise exactement ce que le listener aurait servi, par le même chemin de code. Une clé écrite autrement serait une seconde implémentation à maintenir honnête, et l’écart ne se verrait que sur le bureau de quelqu’un.
Quand il refuse
Refuser est ici une réponse complète, parce que le repli tient en une commande sur n’importe quelle Debian et que ce serveur sert très bien son résultat :
$ proxmox-auto-install-assistant prepare-iso pve.iso --fetch-from http --url …
Il refuse quand l’image n’a ni Rock Ridge ni Joliet — le fichier ne pourrait alors
exister que sous un nom 8.3 tronqué comme AUTO_INS.TOM;1, et l’installeur ne le
trouverait jamais. Il refuse une image UDF, parce qu’une ISO Windows ne garde ses gros
fichiers que dans l’arbre UDF et que patcher l’arbre ISO9660 produirait quelque chose qui
a l’air juste et ne l’est pas. Et il refuse quand le répertoire racine n’a pas de mou
dans aucun de ses secteurs : déplacer l’extent entraînerait les tables de chemins, ce qui
n’est délibérément pas fait.
Il refuse aussi de préparer une image non-Proxmox, en nommant l’alternative : toutes les
autres familles prennent l’URL sur la ligne de commande du noyau, là où media ipxe la
met déjà.
Si la source change en dessous
Les décalages d’injection sont calculés contre une image donnée. Une source qui aurait changé serait patchée au mauvais endroit, produisant une image qui se monte et qui est fausse — le fichier compagnon retient donc la taille de la source, et le catalogue refuse quand elle ne correspond plus :
problem: pve-8.4-http.media: pve-8.4 was 1610610688 bytes when this was prepared and
is 1610612736 now. The injection offsets no longer apply — re-run `media prepare`.
Dire au serveur son propre nom
Dès qu’il écrit des URL dans les scripts qu’il sert, le serveur a besoin d’un nom pour
lui-même qu’une machine puisse réellement atteindre. 0.0.0.0:8001 n’en est pas un.
$ export RESCRIPTUM_PUBLIC_HOST=192.0.2.10
Un hôte, jamais une URL. Pas de schéma, pas de port, pas de chemin — le serveur écrit des URL pour deux listeners, et une valeur portant un port épinglerait chaque script généré sur l’un d’eux. Chaque URL ajoute le port de son propre listener. Une valeur portant l’un des trois est refusée au démarrage, en nommant lequel.
Laissée vide, elle demande à la table de routage laquelle des adresses de cet hôte fait face à l’extérieur — et sur un segment sans route par défaut, se rabat sur la liste des interfaces, ce qui sur un hôte à une seule adresse n’est pas une déduction du tout. Dans les deux cas elle dit au démarrage ce qu’elle a retenu, et s’il y avait quelque chose à trancher :
RESCRIPTUM_PUBLIC_HOST is not set — using 192.0.2.10, the only address this host has.
Every generated URL will name it.
warning: RESCRIPTUM_PUBLIC_HOST is not set — derived 192.0.2.10, which is what every
generated URL will name. This host also has 10.8.0.4. If the machines reach it on one of
those instead, set it explicitly.
C’est la seconde qu’il faut prendre au sérieux : une mauvaise déduction produit une machine qui démarre, enchaîne, et se bloque sur une adresse qui n’existe pas. Nommer les autres adresses est ce qui rend la question tranchable depuis le journal lui-même, plutôt qu’en allant regarder l’hôte. Le NAT est le cas qu’aucune des deux lignes ne peut attraper : l’adresse est bien celle de cet hôte, et bien celle que les machines n’atteignent pas.
Le garder honnête
$ rescriptum media check
checking media in /srv/media
2 image(s), 1 verified against a recorded digest
note: ubuntu-24.04 has no recorded digest — `media add` records one
ok — everything recorded still matches
Son code de sortie est un contrat, comme celui de check : zéro quand tout ce qui a été
enregistré correspond toujours, un quand quelque chose a dérivé. deploy.sh s’y fie.
Une image qui a changé sous une empreinte enregistrée est la seule panne qui installe silencieusement quelque chose que personne n’a relu, donc elle est bruyante :
FAIL pve-8.4: the image no longer matches what was recorded
recorded 9f86d081884c7d65…
found 7d793037a0760186…
Ce que cela prouve, c’est l’intégrité, pas l’authenticité : ce qui est servi est ce
qui a été enregistré. Savoir si ce qui a été enregistré est bien ce que l’éditeur a
publié relève de ses propres signatures, et --sha256 au moment du media add est
l’endroit où cette vérification se place.
Qui a le droit de récupérer
Le trafic de démarrage n’est pas authentifié, et forcément : une ROM PXE n’a aucun identifiant — la même nécessité qui gouverne déjà le point de réponse. Les contrôles sont donc structurels : lecture seule, borné au catalogue, et aucun chemin de système de fichiers n’est jamais construit à partir d’une requête. Plus un qui peut dire pas vous :
$ export RESCRIPTUM_BOOT_ALLOW=10.0.0.0/8,192.168.0.0/16
Non définie, n’importe qui pouvant atteindre le port, ce qui sur un VLAN de provisionnement est la configuration honnête. Un VLAN de démarrage est la recommandation qui fonctionne vraiment ; voir Sécurité.
Réglages
| Variable | Défaut | À quoi elle sert |
|---|---|---|
RESCRIPTUM_MEDIA_ADDR | 0.0.0.0:8001 | Le listener |
RESCRIPTUM_MEDIA_TIMEOUT_SECS | 600 | Échéance du transfert entier |
RESCRIPTUM_MEDIA_MAX_CONNECTIONS | 16 | Transferts simultanés |
Seize, c’est bas volontairement. Chaque transfert retient son jeton pendant des minutes, et le petit bout de ce sur quoi cela doit tourner est un NAS avec un disque mécanique : seize transferts à 64 Kio par morceau font environ deux méga-octets de tampons, une arithmétique qui doit tenir dans 512 Mo de RAM.
Sur une machine de datacenter, montez-la. Le point de réponse a son propre budget et n’est touché dans aucun des deux cas.
Démarrer une machine par le réseau
Démarrer une machine par le réseau
Une machine s’allume. Quatre maillons plus tard, elle s’installe comme quelqu’un l’a décidé — ou, si personne n’a encore rien décidé à son sujet, elle attend dans un menu où un humain peut le faire.
mise sous tension
│
(1) ├── le DHCP dit d'où démarrer ............ À EUX. Deux options, et nous
│ générons l'extrait qui les pose.
▼
(2) ├── TFTP livre un chargeur ............... À NOUS
│ un iPXE adapté à l'architecture, qui enchaîne via ${next-server}
▼
(3) ├── iPXE demande quoi faire .............. À NOUS
│ machine connue → sa propre réponse sans surveillance
│ machine inconnue → le menu
▼
(4) └── les octets arrivent .................. À NOUS
noyau, initrd, l'image elle-même — HTTP avec plages
Le maillon 1 appartient à quelqu’un d’autre et cela ne changera pas. rescriptum ne parle pas DHCP du tout — ni serveur, ni proxy, ni derrière un drapeau. Les sites qui déploient ceci en ont déjà un, et le faire pointer vers un serveur de démarrage est un problème résolu depuis trente ans.
Mise en route
$ export RESCRIPTUM_MEDIA_DIR=/srv/media # les images
$ export RESCRIPTUM_BOOT_DIR=/srv/boot # les chargeurs
$ export RESCRIPTUM_PUBLIC_HOST=192.0.2.10 # ce que nommeront les scripts générés
RESCRIPTUM_BOOT_DIR dit où sont les chargeurs : non définie, il n’y a aucun listener
TFTP et rien sur /boot/…. La nommer démarre TFTP sur 0.0.0.0:69 sauf si vous dites le
contraire.
Le port 69 est privilégié, et c’est le seul port privilégié que ce serveur demandera jamais — sans répondeur DHCP, il n’y a rien après 67 ni 4011. Quatre façons de traiter la question, toutes portables :
$ export RESCRIPTUM_USER=rescriptum # démarrer en root, lier, puis abandonner
$ setcap cap_net_bind_service=+ep rescriptum # ou n'accorder que cette capacité
$ export RESCRIPTUM_TFTP_ADDR=0.0.0.0:6969 # ou le déplacer, si leur DHCP sait le dire
$ export RESCRIPTUM_TFTP_ADDR=off # ou n'avoir aucun listener du tout
off est une valeur, pas une absence — c’est ainsi qu’on dit qu’un autre service de
cette machine livre le chargeur pendant que rescriptum sert le reste de la chaîne. Les
chargeurs restent servis en HTTP sur /boot/… et restent vérifiés par boot check ; seul
le listener disparaît. C’est un contournement de déploiement pour qui le veut, jamais la
façon dont quoi que ce soit est livré ici : c’est rescriptum le serveur TFTP, et une
version qui le couperait par défaut aurait cédé la chose même qu’elle est. Le paquet
Synology ouvre le port 69 avec un setcap.
Un port TFTP qu’on ne peut pas lier n’arrête pas le serveur, et c’est le seul endroit
où la règle « un listener qui ne peut pas se lier est fatal » s’inverse dans ce projet. Le
port 69 est le seul port privilégié de la conception, donc le seul bind qui puisse échouer
pour quelque chose que personne n’a configuré — une capacité qu’une mise à jour a
discrètement perdue, le plus souvent. Les réponses sont le produit ; mourir ici ferait
échouer toutes les installations en cours pour signaler qu’un second port n’a pas pu être
ouvert. Donc il avertit, continue de servir, et boot check sort en non-zéro :
$ rescriptum boot check
BROKEN nothing answers on 0.0.0.0:69 and it cannot be bound either: Permission denied.
Port 69 is privileged: run as root and set RESCRIPTUM_USER to drop afterwards, or grant
the binary cap_net_bind_service with setcap — the server still answers and still serves
media, but a machine sent here by DHCP asks for a loader and gets nothing
Il demande un vrai chargeur au port plutôt que d’essayer de le lier, car lier prouve le contraire de ce qu’on croit : un bind qui réussit signifie que personne n’écoute, et un bind qui échoue ne distingue pas ce serveur d’un autre service qui squatterait le port.
On lie d’abord, on abandonne ensuite, toujours. L’ordre inverse fonctionne en test sous root et échoue au déploiement, à un redémarrage — le seul moment où personne ne regarde.
Les deux lignes de leur serveur DHCP
$ rescriptum boot dhcp-snippet --format dnsmasq
# rescriptum 0.2.0 - boot handoff for 192.0.2.10
# Architecture values are IANA option 93 codes; see docs/guide/boot/dhcp.
# Generated from the same table the TFTP server serves from.
dhcp-match=set:bios,option:client-arch,0
dhcp-match=set:efi64,option:client-arch,7
dhcp-match=set:efi64,option:client-arch,9
dhcp-match=set:efiarm64,option:client-arch,11
…
--format couvre dnsmasq, isc, kea, powershell, pfsense et mikrotik ;
--one-loader produit la forme d’une seule ligne pour un parc d’une seule architecture.
L’extrait et le serveur TFTP sont générés depuis une même table, de sorte que ce que vous collez et ce que le serveur distribue ne peuvent pas diverger. Ce qu’ils peuvent faire, c’est nommer un chargeur que personne n’a encore téléchargé, et cela échoue silencieusement au niveau de la ROM : la machine demande, ne reçoit rien, et s’arrête sans un message sur aucune console. Une commande l’attrape :
$ rescriptum boot check
checking boot assets in /srv/boot
ok ipxe-arm64.efi (1.0M)
MISSING ipxe-undionly.kpxe — every machine the snippet sends here will ask for it,
get nothing, and stop
Son code de sortie est un contrat, comme celui de check. Placez-le au même endroit.
Quatre détails que l’extrait généré traite correctement
Chacun est une façon d’échouer sans bruit sur le réseau de quelqu’un d’autre, et aucun n’est évident :
- Le champ BOOTP
fileet l’option 67. Certaines ROM ne lisent que l’un des deux, et lequel n’est pas prévisible d’après le fournisseur. - Un défaut sans étiquette à la fin. Chaque ligne d’architecture est étiquetée : une ROM qui n’envoie pas d’option 93 ne correspondrait à rien et n’obtiendrait aucun fichier de démarrage.
HTTPClientrenvoyé dans l’option 60 pour les clients UEFI HTTP Boot. Le firmware filtre les offres dessus : une réponse ne portant que l’URL est écartée, en silence, ce qui est indiscernable d’une absence de serveur DHCP.- Un next-server pour ces clients aussi, bien qu’ils récupèrent en HTTP. Sans lui, le
script embarqué du chargeur lit un
${next-server}vide et enchaîne vers nulle part.
::: tip Windows Server
Une stratégie DHCP ne peut pas se conditionner sur l’option 93 — les types de
condition sont la classe fournisseur, la classe utilisateur, la MAC, l’identifiant
client, le FQDN et les informations de relais. L’architecture n’atteint une stratégie
qu’à l’intérieur de la chaîne de l’option 60, donc le PowerShell généré définit des
classes fournisseur sur PXEClient:Arch:00007* et y accroche les stratégies. Même
résultat, mécanisme différent, et c’est exactement le genre de chose qu’on retient à
moitié.
:::
Le chargeur
TFTP livre un fichier, et la règle est écrite dans le code :
TFTP livre le chargeur. Tout ce qui suit passe en HTTP.
À 1468 octets par aller-retour, TFTP déplace environ 1,4 Mo/s sur une milliseconde de latence. Le chargeur fait un mégaoctet : deux secondes. Une image de 1,5 Go prendrait près de vingt minutes, contre quinze secondes en HTTP sur le même câble.
Quel chargeur dépend de ce que le firmware a annoncé :
| Option 93 | Client | Servi |
|---|---|---|
0x0000 | BIOS PXE | ipxe-undionly.kpxe |
0x0007, 0x0009 | UEFI x86-64 | ipxe-x86_64.efi, plus -snp / -snponly |
0x000b | UEFI ARM64 | ipxe-arm64.efi |
0x0010, 0x0013 | UEFI HTTP Boot | les mêmes fichiers, en HTTP, sans TFTP du tout |
| tout le reste | UEFI 32 bits, EBC, U-Boot | refusé, avec la raison |
0x0009 mérite un mot. La RFC 4578 le définissait comme « EFI x86-64 » ; le registre
IANA, réécrit par la RFC 5970, le liste comme « EBC ». Les vrais firmwares x64 envoient
l’un ou l’autre, donc les deux pointent vers x64 — une table produite depuis le seul
registre ne donnerait rien à la moitié d’un parc.
snponly existe parce que la construction UEFI ordinaire ne voit pas toujours la carte
réseau. Toutes les variantes sont servies et la table choisit ; c’est précisément le
savoir qu’un exploitant ne devrait pas avoir à acquérir.
Se les procurer
Chaque version publiée attache rescriptum-boot-assets-<version>.tar.gz. Décompressez-le
là où le serveur peut le lire, nommez le répertoire, et vérifiez-le :
$ tar -xzf rescriptum-boot-assets-0.2.0.tar.gz -C /srv
$ export RESCRIPTUM_BOOT_DIR=/srv/rescriptum-boot-assets-0.2.0
$ rescriptum boot check
Il contient les huit chargeurs, un SHA256SUMS, un ipxe.iso et un ipxe.usb
démarrables pour une machine sans ROM PXE utilisable, et un NOTICE — c’est iPXE, en
GPLv2, construit depuis un commit amont épinglé. C’est un téléchargement séparé, et il
ne fait partie d’aucune archive binaire ni d’aucun .spk, délibérément : des fichiers
séparés servis à côté relèvent de la simple agrégation, et packaging/ipxe/ est l’offre
écrite qui les accompagne.
Pour les construire vous-même à la place — le même script que la release exécute, depuis le même épinglage :
$ packaging/ipxe/build.sh --out /srv/boot
Un chargeur venu d’ailleurs convient aussi, à condition qu’il enchaîne vers ce serveur plutôt que vers Internet — voir ci-dessous pourquoi un chargeur d’origine ne le fait pas.
Ce qui se passe au deuxième démarrage
La première question que tout le monde se pose après une installation réussie, et elle a une vraie réponse.
Une machine qui vient d’être installée redémarre, et si le démarrage réseau est encore premier dans son BIOS, elle revient ici. Ce qui suit est décidé par un seul réglage :
RESCRIPTUM_BOOT_UNCLAIMED | Une machine qu’aucune réponse ne revendique |
|---|---|
menu (défaut) | reçoit le menu, dont la première entrée est le disque local et dont le délai y retombe — quinze secondes, puis le disque |
local | est rendue directement à son firmware, qui passe au périphérique suivant |
Ce sont deux lectures opposées de ce que signifie un fichier de réponse, et le choix appartient au déploiement.
Avec le menu, un fichier qui revendique une machine est la façon de dire laisse celle-ci tranquille — car sans lui elle atterrit dans un menu que quelqu’un pourrait cliquer. C’est juste pendant qu’on provisionne, et c’est la thèse du projet : une machine dont personne n’a rien décidé doit finir là où un humain peut décider.
Avec local, un fichier de réponse veut dire installe celle-ci, et son absence est
l’état sûr. Il n’arrive rien à une machine pour laquelle vous n’avez pas écrit de fichier —
elle démarre sur son disque, à chaque fois, sans menu à cliquer par accident. C’est la
lecture dont un parc en production a besoin, et c’est celle qui passe à l’échelle : le
nombre de machines qu’on veut réinstaller est toujours plus petit que celui des autres.
Le bénéfice, c’est que le démarrage réseau peut rester premier dans le BIOS pour toujours. Réinstaller une machine devient ajouter un fichier, redémarrer — sans console, sans menu de démarrage, sans toucher au matériel. Retirer le fichier est ce qui empêche que cela se reproduise.
$ rescriptum config set RESCRIPTUM_BOOT_UNCLAIMED=local
Dans les deux cas l’identité de la machine part d’abord. Le réglage décide seulement de ce qui arrive quand rien ne l’a revendiquée — pas s’il faut demander.
Installer une machine une fois, et une seule
Une machine revendiquée par une réponse .ipxe s’installe, redémarre, est revendiquée de
nouveau, et se réinstalle — en effaçant son disque à chaque tour. Tous les systèmes de
provisionnement répondent pareil : une machine est armée pour l’installation, et quelque
chose la désarme ensuite.
C’est la machine qui sait. Proxmox appelle un webhook après une installation réussie et avant le redémarrage, avec ses interfaces réseau dans le corps :
[post-installation-webhook]
url = "http://192.0.2.10:8000/installed"
auth-token = "nas:s3cr3t"
$ rescriptum config set RESCRIPTUM_INSTALLED_TOKEN=nas:s3cr3t
C’est tout. La machine termine, elle le dit, et son .ipxe passe de 98fa9b50d810/ à
installed-98fa9b50d810/ — un nom de répertoire qui ne lui correspond plus, le préfixe
faisant partie du nom comparé. Elle démarre sur son disque désormais, et la réarmer consiste
à remettre le document en place.
Le document désarmé va dans un répertoire frère plutôt que de rester dans celui de la
machine, pour que 98fa9b50d810/ continue de vouloir dire « la configuration de cette
machine » et que rien de ce qu’il contient n’ait à se lire comme désactivé.
Pas de jeton, pas d’endpoint — absent plutôt qu’ouvert. Sans lui, /installed est une
demande de réponse ordinaire comme n’importe quel chemin, ce qui permet à une URL de rester
gravable dans une ISO.
Trois choses qu’il ne fait pas, et chacune est délibérée :
- Il ne touche jamais un groupe. Un groupe revendique un rack entier, et une machine qui finit son installation ne doit pas désarmer ses voisines. La recherche ne consulte pas les groupes du tout, plutôt que de les écarter après coup.
- Il ne touche rien d’autre que le
.ipxe. Le.tomlde la machine, à côté dans le même répertoire, est ce que l’installateur a lu pour la construire, et il reste comme trace de la manière. - Il déplace, il ne supprime pas. C’est le seul chemin où quelque chose venu du réseau modifie le jeu de réponses : rien de ce qu’il fait n’est irréversible.
Arriver deux fois n’est pas une erreur — un webhook peut être réessayé, et une machine
installée depuis le menu n’a jamais été revendiquée. Un désarmement qui échoue est
journalisé en still armed, parce que sa conséquence est autrement silencieuse : la
machine se réinstalle au démarrage suivant et rien d’autre ne le dirait.
Toutes les autres familles rapportent aussi
Proxmox est le seul à avoir son propre webhook. La revendication n’est pas propre à
Proxmox — c’est un document .ipxe, qui concerne le chargeur et non le système
d’exploitation — donc toutes les familles ont besoin du même désarmement, et toutes ont un
endroit où lancer une ligne à la fin de leur installation :
curl -fsS -X POST -H "Authorization: Bearer nas:s3cr3t" \
"http://192.0.2.10:8000/installed?mac=$(cat /sys/class/net/*/address | head -1)"
Pas de corps, pas de JSON : la query dit quelle machine, l’en-tête dit qu’elle en a le droit. Où mettre cette ligne :
| Famille | Où |
|---|---|
| Proxmox | [post-installation-webhook] — natif, rien à écrire |
| Debian | d-i preseed/late_command string in-target sh -c '…' |
| Ubuntu | late-commands: dans le document autoinstall |
| RHEL, AlmaLinux, Rocky | la section %post du kickstart |
| SUSE | <scripts><chroot-scripts> du profil AutoYaST |
Prenez la MAC de l’interface qui a démarré, pas la première par ordre alphabétique.
L’exemple ci-dessus prend la première entrée de /sys/class/net, ce qui va sur une machine
à une carte et se trompe sur une machine à quatre — et une mauvaise MAC désarme la mauvaise
machine, ou personne. Sur une machine à plusieurs cartes, nommez l’interface.
L’endpoint accepte les deux formes du justificatif : l’auth-token de Proxmox arrive dans
le corps JSON parce que c’est ce que Proxmox envoie, et un en-tête bearer parce que c’est
ce qu’envoie un script shell. Même secret, même comparaison en temps constant.
Quand tout est juste et que la machine refuse quand même de s’installer
La chaîne peut être parfaite et échouer à la dernière marche, côté machine et non côté serveur. Deux cas réellement rencontrés, tous deux sur un Lenovo vPro :
Intel AMT en adresse statique, sur une carte partagée avec le système. Le dhclient de
l’installateur envoie deux requêtes à onze secondes d’intervalle puis abandonne ; si le
Management Engine tient l’interface avec une configuration statique pendant que l’hôte
demande du DHCP, ces onze secondes passent sans offre et l’installation s’arrête sur
Fetching answer file via HTTP failed: Network is unreachable. Mettez l’AMT en DHCP
aussi. Lancer dhclient -v eno1 à la main depuis le shell de l’installateur réussit
ensuite immédiatement, et c’est ce qui rend le diagnostic déroutant : le réseau va bien,
c’est la temporisation qui ne va pas.
Un port de commutateur qui ne transmet pas tout de suite, pour la même raison et avec le même symptôme — RSTP en convergence, ou un lien encore en négociation après que le noyau a repris la carte des mains d’iPXE. Ce serveur n’y peut rien dans les deux cas : les onze secondes sont la fenêtre de l’installateur, pas la nôtre.
L’installateur ouvre un shell root quand il abandonne, et ce shell est le diagnostic le plus rapide qui soit :
# ip link # l'interface est-elle seulement montée ?
# dhclient -v eno1 # une offre revient-elle quand on la demande à la main ?
# ip addr show eno1
Une adresse qui apparaît là et pas pendant l’installation veut dire que le réseau fonctionne et que la machine a simplement demandé trop tôt.
Comment iPXE finit par parler à nous
La question qu’on ne s’attend pas à devoir trancher. Quel que soit le livreur du chargeur :
- Un
undionly.kpxeordinaire venu d’ipxe.org fait du DHCP, se fait dire de charger iPXE, et se recharge lui-même indéfiniment — la boucle d’enchaînement documentée par iPXE. - Un binaire netboot.xyz d’origine embarque un script qui va droit au menu public
boot.netboot.xyz. Pas de boucle, mais votre menu et vos réponses ne sont jamais consultés.
Les chargeurs que rescriptum distribue portent un script de trois lignes qui enchaîne via
${next-server} — la valeur que l’option 66 a déjà posée, puisque c’est ainsi que le
chargeur est arrivé. Une seule construction générique fonctionne donc dans tous les
déploiements, sans seconde condition dans un fichier de configuration qui appartient à
quelqu’un d’autre.
Le script enchaîne vers le port 8001, et c’est un contrat plutôt qu’une préférence :
il est gravé dans le chargeur avant qu’aucun déploiement n’existe et ne peut lire aucune
configuration. Déplacer RESCRIPTUM_MEDIA_ADDR est permis, et boot check le signale.
Ce qu’une machine voit
L’étape deux met l’identité de la machine dans la chaîne de requête, la seule chose que
DHCP ne peut pas faire — une option DHCP ne peut pas porter ${net0/mac} :
$ rescriptum boot bootstrap
#!ipxe
chain http://192.0.2.10:8000/ipxe/boot?mac=${netX/mac}&uuid=${uuid}\
&serial=${serial:uristring}&asset=${asset:uristring}\
…
|| chain http://192.0.2.10:8001/ipxe/menu
Deux détails y sont porteurs. netX, pas net0 — net0 n’est que la première
interface, donc un serveur démarrant par son second port s’identifierait par le premier,
inutilisé. Et :uristring sur chaque chaîne SMBIOS, parce que ${manufacturer}
s’étend en Dell Inc. avec l’espace et qu’iPXE n’encode rien de lui-même.
Ce || final, c’est tout « un menu est la réponse par défaut » : une machine que quelque
chose réclame reçoit sa propre réponse sans surveillance, et une machine que rien ne
réclame retombe sur le menu. C’est la description de poste de default/, mot pour
mot, appliquée à un autre format.
Le menu
$ rescriptum boot menu
Rendu depuis le catalogue au moment de la requête, et non maintenu comme un fichier : posez une ISO dans le répertoire de médias et elle est dans le menu à la requête suivante.
- « Boot from the local disk » est en premier, et le délai y retombe. Une machine qui démarre en PXE par accident, et que rien ne réclame, finit sur son propre disque au bout de quinze secondes. Elle n’attend jamais un humain qui ne vient pas, et elle n’installe jamais rien. Avec la règle qui veut qu’une machine non réclamée reçoive un menu plutôt qu’une installation, le pire cas d’une erreur sur le périmètre des machines qui atteignent ce serveur est quelques secondes ajoutées à un démarrage.
- Les entrées sont filtrées sur l’architecture du client : une image ARM64 n’est pas proposée à une machine x86 — ce serait une entrée qui démarre le mauvais noyau.
- Une image qu’aucune détection n’a su placer est quand même proposée, comme un CD.
- Les entrées de diagnostic — un shell,
netinfo, et une qui démarre un autre rescriptum — sont ce dont tout serveur de démarrage finit par avoir besoin. La dernière sert à tester un serveur candidat sur site, depuis celui qui tourne, sans toucher au DHCP ni aux chargeurs.
RESCRIPTUM_BOOT_TIMEOUT_SECS (15 par défaut) règle l’attente, et
RESCRIPTUM_BOOT_TITLE la barre de titre. Le logo est récupéré par
console --picture … ||, qui tolère son propre échec : une console série via IPMI
n’a pas de framebuffer, et c’est ainsi que la moitié des installations en datacenter sont
suivies.
Ce qui casse quand ce serveur est arrêté
Cela mérite d’être dit franchement, parce que « serveur de démarrage » sonne critique et ne l’est pas :
| rescriptum arrêté | |
|---|---|
| Adressage DHCP, DNS, routage | inchangés — il ne parle aucun de ces protocoles |
| Machines déjà installées et en service | inchangées |
| Machines qui redémarrent | inchangées — elles démarrent sur disque |
| Une machine qui démarre en PXE par accident | passe au périphérique suivant, comme elle l’aurait fait |
| Démarrer une nouvelle installation | s’arrête |
Rien de ce que rescriptum installe ne dépend de rescriptum ensuite. Le point de réponse est consulté pendant une installation et plus jamais.
Sécurité
Le trafic de démarrage n’est pas authentifié, et forcément — une ROM PXE n’a aucun identifiant, la même nécessité qui gouverne déjà le point de réponse. Les contrôles sont donc structurels, et l’un d’eux peut dire pas vous :
$ export RESCRIPTUM_BOOT_ALLOW=10.0.0.0/8 # partagée par TFTP et les médias
UDP est falsifiable et TFTP est de l’UDP, donc le serveur ne répond jamais à une destination de diffusion ou de multidiffusion — de l’hygiène anti-amplification plutôt que de la politesse —, plafonne les transferts au total et par pair, et journalise chacun d’eux. Il est en lecture seule : une requête d’écriture est refusée comme violation d’accès, car écrire un chargeur en UDP non authentifié serait un moyen de changer ce que démarre chaque machine du segment.
Un VLAN de démarrage est la recommandation honnête et celle qui fonctionne vraiment. Voir Sécurité.
::: tip Secure Boot Nos chargeurs ne sont pas signés, et shim ne charge que ce que la clé de son distributeur a signé — servir un shim à côté d’un iPXE non signé n’est donc pas un support de Secure Boot, c’est un démarrage qui s’arrête sur une erreur de signature. Ce qui fonctionne : désactiver Secure Boot, enrôler une MOK, ou laisser le firmware démarrer en PXE le shim et le GRUB signés de la distribution cible, servis par le listener média comme n’importe quel fichier. Nous ne signons rien, ne retirons rien, et rien ici n’affaiblit une machine dont Secure Boot est actif. :::
Quand leur DHCP est vraiment intouchable
Rien de tout cela ne coûte une ligne de code, et les trois fonctionnent :
- UEFI HTTP Boot avec une URL saisie dans le firmware. Les firmwares serveur récents permettent d’entrer directement une URL de démarrage. La chaîne commence alors sur le listener média, sans aucune option DHCP.
- iPXE depuis un média virtuel IPMI, une clé USB ou la ROM de la carte réseau, portant l’adresse de ce serveur. Une image d’un mégaoctet, montée une fois par machine.
- dnsmasq en mode proxy-DHCP, pour un site qui a vraiment un serveur DHCP qu’il ne peut pas modifier. Il existe, il est mature, il tient en trois lignes de configuration, et ce n’est pas à nous de le réécrire. Le nommer est la réponse honnête.
Référence
Référence
La contrepartie exhaustive du reste du guide. Tables et contrats, une page par surface :
- Configuration — chaque variable d’environnement, sa valeur par défaut, et ce que fait une valeur invalide.
- Surface HTTP — méthodes, codes de statut, en-têtes, limites.
- Formats et alias d’endpoint — quelle extension est quel format, et quel segment d’URL le demande.
- Ligne de commande — chaque sous-commande et chaque option.
Vous cherchez le récit plutôt ? Commencez par ce qu’est rescriptum.
Configuration
Configuration
Des variables d’environnement — et, en option, un fichier d’où les lire, dans l’une de deux formes. Il n’y a pas de ligne de commande à se tromper, et les variables sont toute la configuration : les deux formats de fichier règlent exactement les mêmes choses sous exactement les mêmes règles, donc rien de ce qu’on écrit dans un fichier ne peut signifier quelque chose que l’environnement ne dirait pas.
Les variables
| Variable | Défaut | Signification |
|---|---|---|
RESCRIPTUM_CONFIG | non défini | Lire les valeurs par défaut depuis ce fichier TOML — voir plus bas |
RESCRIPTUM_ENV_FILE | non défini | Lire les valeurs par défaut depuis ce fichier CLÉ=valeur — voir plus bas |
RESCRIPTUM_STORE | files | files (un répertoire) ou sqlite (une base) |
RESCRIPTUM_ANSWERS_DIR | /srv/answers | Répertoire des documents de réponse |
RESCRIPTUM_DB_PATH | /srv/answers.db | Chemin de la base, quand RESCRIPTUM_STORE=sqlite |
RESCRIPTUM_LISTEN_ADDR | 0.0.0.0:8000 | Adresse d’écoute. :0 choisit un port libre, et celui qui est bindé est affiché |
RESCRIPTUM_WORKERS | nombre de CPU | Threads du runtime asynchrone. Pas une limite de concurrence |
RESCRIPTUM_MAX_CONNECTIONS | 2048 | Connexions en vol avant délestage en 503 |
RESCRIPTUM_TIMEOUT_SECS | 10 | Délai de lecture des en-têtes et échéance de la connexion entière |
RESCRIPTUM_ANSWER_TOKEN | non défini | Jeton exigé par l’endpoint de réponse. Non défini = ouvert |
RESCRIPTUM_ADMIN_ADDR | non défini | Listener de l’API d’administration. Non défini = API désactivée |
RESCRIPTUM_ADMIN_TOKEN | non défini | Jeton d’administration, 16+ caractères. Obligatoire avec RESCRIPTUM_ADMIN_ADDR |
RESCRIPTUM_CAPTURE_DIR | non défini | Enregistre les corps de requête ici. Non défini = pas de capture |
RESCRIPTUM_LOG | all | all, problems ou off — voir plus bas |
RESCRIPTUM_LOG_FILE | non défini | Un fichier où ajouter, ou stdout / stderr. Non défini = stderr |
RESCRIPTUM_MEDIA_DIR | non défini | Images d’installation. Non défini = pas de média et pas de listener média |
RESCRIPTUM_MEDIA_ADDR | 0.0.0.0:8001 | Le listener média, quand un répertoire de médias existe |
RESCRIPTUM_MEDIA_TIMEOUT_SECS | 600 | Échéance du transfert entier. Volontairement pas les 10 s du point de réponse |
RESCRIPTUM_MEDIA_MAX_CONNECTIONS | 16 | Transferts simultanés. Bas exprès : chacun retient son jeton des minutes durant |
RESCRIPTUM_PUBLIC_HOST | déduit | L’hôte que nomment les URL générées. Un hôte, jamais une URL |
RESCRIPTUM_BOOT_ALLOW | non défini | CIDR clients autorisés à récupérer les médias. Non défini = quiconque atteint le port |
RESCRIPTUM_BOOT_DIR | non défini | Chargeurs et menus, distribués en TFTP. Non défini = pas de TFTP du tout |
RESCRIPTUM_TFTP_ADDR | 0.0.0.0:69 | Le listener TFTP, ou off pour aucun. Le port 69 est privilégié ; voir RESCRIPTUM_USER |
RESCRIPTUM_TFTP_PORT_RANGE | non défini | Les ports depuis lesquels un transfert répond, en premier-dernier. Un transfert TFTP quitte le port 69 aussitôt — le serveur répond depuis un port neuf et le client acquitte vers celui-là — donc un pare-feu n’autorisant que 69 jette l’acquittement, et la machine semble s’être désintéressée. Épinglez la plage pour pouvoir l’ouvrir. Non définie, le noyau choisit |
RESCRIPTUM_TFTP_BLKSIZE | 1468 | Le plus grand bloc TFTP accepté. 1468 remplit exactement un chemin de 1500 octets — 1468 de charge, 4 TFTP, 8 UDP, 20 IP — donc un tag VLAN ou un tunnel rend la trame trop grande et une ROM PXE s’arrête en général sans rien dire. À baisser (1400, ou 512) quand un démarrage cale au premier bloc |
RESCRIPTUM_BOOT_TIMEOUT_SECS | 15 | Secondes avant que le menu ne retombe sur le disque local |
RESCRIPTUM_BOOT_UNCLAIMED | menu | Ce que reçoit une machine qu’aucune réponse ne revendique. local la rend à son firmware, ce qui inverse le sens d’un fichier de réponse : présent veut dire installe celle-ci plutôt que laisse celle-ci tranquille |
RESCRIPTUM_INSTALLED_TOKEN | non défini | Le jeton du [post-installation-webhook] de Proxmox. Défini, POST /installed existe et retire la revendication d’installation d’une machine quand elle signale sa réussite. Non défini, il n’y a pas d’endpoint |
RESCRIPTUM_BOOT_LOGO | intégré | Un PNG à afficher derrière le menu |
RESCRIPTUM_BOOT_TITLE | intégré | La barre de titre du menu |
RESCRIPTUM_USER / _GROUP | non défini | Basculer dessus après avoir lié. L’ordre inverse échoue au déploiement |
/srv est l’endroit où la norme de hiérarchie des fichiers range les données servies par le
système, ce qu’est précisément un répertoire de réponses. Les deux valeurs par défaut y vivent,
pour qu’un rescriptum lancé sans rien fasse quelque chose de plausible sur n’importe quel
hôte Linux. Rien ne crée le répertoire pour vous, et le serveur le signale au démarrage s’il
manque.
Journalisation
Une ligne par événement, sur stderr par défaut. Deux réglages, parce que les deux questions sont différentes.
Quoi — RESCRIPTUM_LOG :
| Valeur | Garde |
|---|---|
all (défaut) | chaque requête, plus le démarrage, les avertissements et les erreurs |
problems | démarrage, avertissements, erreurs, et seulement les requêtes qui n’ont pas abouti |
off / none | rien du tout |
Une réponse réussie fait une ligne, et à treize mille requêtes par seconde c’est la seule
chose ici qui ait du volume. problems est ce que vous voulez quand un déploiement devient
routinier et que le disque, lui, ne l’est pas. Tout le reste est peu volumineux et
diagnostique, donc conservé dans les deux cas.
Une requête qui n’a jamais atteint de statut — une connexion expirée en plein corps —
compte comme un problème. Une valeur non reconnue retombe sur all avec un avertissement :
une faute de frappe ne doit pas être la raison pour laquelle personne ne voit pourquoi un
déploiement a échoué. Le niveau est nommé dans la ligne de démarrage (log=problems), donc
un log vide s’explique lui-même.
Où — RESCRIPTUM_LOG_FILE :
| Valeur | Va vers |
|---|---|
non définie, ou stderr | stderr, ce que lit un superviseur |
stdout | stdout |
| toute autre valeur | ce fichier, en ajout ; les répertoires parents sont créés |
Un fichier impossible à ouvrir est une erreur de démarrage, pas un repli sur stderr — ce serait une surprise silencieuse découverte bien plus tard. Une écriture qui échoue une fois le serveur lancé est abandonnée : un serveur de provisioning qui mourrait parce que son disque de logs est plein ferait échouer toutes les installations en cours pour signaler qu’il ne peut pas signaler quelque chose.
La rotation vous incombe. Sous systemd il n’y a rien à faire, le log part dans le journal ;
avec un fichier, pointez logrotate dessus avec copytruncate.
Le fichier TOML
RESCRIPTUM_CONFIG nomme un fichier en TOML qui règle les mêmes variables, dans une forme
faite pour être lue. C’est celui vers lequel se tourner quand une personne édite le
fichier à la main — sur un NAS, dans File Station ou via SMB — c’est-à-dire exactement là
où RESCRIPTUM_ANSWERS_DIR=… sur chaque ligne se lit mal, et où le mot « environnement »
envoie chercher un shell qui n’existe pas.
# /etc/rescriptum.toml (chmod 600, appartenant à root)
answers_dir = "/srv/answers"
listen_addr = "0.0.0.0:8000"
log = "problems" # all | problems | off
[store]
kind = "sqlite"
db_path = "/srv/answers.db"
[server]
workers = 2
max_connections = 2048
timeout_secs = 10
[admin]
addr = "127.0.0.1:8001"
token = "…"
[answer]
token = "…"
capture_dir = "/var/lib/rescriptum/captures"
$ RESCRIPTUM_CONFIG=/etc/rescriptum.toml rescriptum
2026-08-29T12:42:02Z - reading configuration defaults from /etc/rescriptum.toml (8 set)
Toutes les règles du fichier d’environnement valent ici aussi : jamais découvert,
seulement nommé (il n’y a pas de ./rescriptum.toml) ; l’environnement réel gagne ;
et un fichier demandé et illisible est une erreur de démarrage, jamais un
avertissement.
Placez-le hors du répertoire de réponses. Tout .toml servable à la racine de ce
répertoire est un document réponse, et ce format partage l’extension — un fichier de
configuration déposé là est signalé par check comme une réponse mal placée, et migrate
propose de le déplacer. /etc est le foyer évident ; sur une installation empaquetée, le
paquet en choisit un.
Les noms
Le préfixe disparaît et les tables font le regroupement. Rien d’autre ne change : chaque
ligne ci-dessous est la variable du même nom, et rescriptum config affiche les deux
orthographes.
| Dans le fichier | Variable |
|---|---|
answers_dir | RESCRIPTUM_ANSWERS_DIR |
listen_addr | RESCRIPTUM_LISTEN_ADDR |
log, log_file | RESCRIPTUM_LOG, RESCRIPTUM_LOG_FILE |
public_host | RESCRIPTUM_PUBLIC_HOST |
user, group | RESCRIPTUM_USER, RESCRIPTUM_GROUP |
store.kind, store.db_path | RESCRIPTUM_STORE, RESCRIPTUM_DB_PATH |
server.workers, server.max_connections, server.timeout_secs | RESCRIPTUM_WORKERS, RESCRIPTUM_MAX_CONNECTIONS, RESCRIPTUM_TIMEOUT_SECS |
admin.addr, admin.token | RESCRIPTUM_ADMIN_ADDR, RESCRIPTUM_ADMIN_TOKEN |
answer.token, answer.capture_dir | RESCRIPTUM_ANSWER_TOKEN, RESCRIPTUM_CAPTURE_DIR |
media.dir, media.addr, media.timeout_secs, media.max_connections | les quatre RESCRIPTUM_MEDIA_* |
boot.dir, boot.allow, boot.unclaimed, boot.timeout_secs, boot.logo, boot.title | les six RESCRIPTUM_BOOT_* |
tftp.addr, tftp.port_range, tftp.blksize | les trois RESCRIPTUM_TFTP_* |
installed.token | RESCRIPTUM_INSTALLED_TOKEN |
Le format
| N’importe quel scalaire TOML | un nombre peut s’écrire en nombre (workers = 2) ou en chaîne ; les deux arrivent au serveur comme le même réglage |
Un commentaire # | n’importe où, y compris en fin de ligne — contrairement au fichier d’environnement, qui n’a pas d’échappements et ne peut donc pas en avoir |
Une valeur avec un #, un guillemet ou une espace | sans problème, échappée comme TOML échappe, et relue à l’identique |
"" | non défini — la même règle qu’une variable exportée mais vide, et c’est ce qui permet à config unset de vider une ligne au lieu de supprimer le paragraphe qui la documente |
| La même clé deux fois | refusée par TOML lui-même, donc le fichier ne se charge pas |
| Une clé que ce programme ne lit pas | un avertissement la nommant : admin.tokenn est attrapé au lieu d’être ignoré |
| Une liste ou une table là où une valeur est attendue | une erreur de démarrage : contrairement à une faute de frappe, elle visait un réglage réel, et servir la valeur par défaut alors que le fichier dit le contraire serait silencieux |
| Un fichier lisible par d’autres | un avertissement avec son mode, parce qu’il peut contenir admin.token |
Les avertissements nomment les clés et les chemins, jamais les valeurs.
Les deux fichiers à la fois
Nommer les deux est une transition plutôt qu’un état stable : rien n’est refusé et l’ordre
est annoncé au démarrage — l’environnement bat le fichier TOML, qui bat le fichier
d’environnement. rescriptum config montre lequel des trois a mis chaque valeur en
vigueur, et config set écrit dans le fichier TOML — celui que le serveur lit en premier,
pour qu’une écriture ne puisse pas être une modification qui ne change rien en silence.
Le fichier d’environnement
RESCRIPTUM_ENV_FILE nomme un fichier contenant les mêmes variables. Il existe pour les
déploiements qui n’ont nulle part où mettre un jeton — au premier chef Synology DSM 7,
qui n’a pas de systemd. Sous systemd, EnvironmentFile= fait déjà cela et vous n’en avez
pas besoin.
# /etc/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/srv/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
$ RESCRIPTUM_ENV_FILE=/etc/rescriptum.env rescriptum
2026-08-24T12:42:02Z - reading configuration defaults from /etc/rescriptum.env (4 set)
Il n’est jamais découvert, seulement nommé. Il n’y a pas de ./.env. Ce binaire tourne
en root : s’il ramassait un fichier dans le répertoire d’où il a été lancé, quiconque peut
y écrire posséderait RESCRIPTUM_ADMIN_TOKEN — et avec lui le mot de passe root de chaque
machine installée ensuite.
L’environnement réel gagne. Le fichier fournit des valeurs par défaut, donc ce qui est exporté délibérément au lancement n’est jamais écrasé en douce. Une variable exportée mais vide compte comme non définie, donc le fichier s’applique quand même.
Un fichier demandé et illisible est une erreur de démarrage, pas un avertissement. C’est tout l’intérêt : l’échec qu’il remplace est un serveur qui démarre sur ses valeurs par défaut — mauvais répertoire de réponses, pas de jeton admin — sans un mot dans le log.
Le format
CLÉ=valeur, une par ligne | un export en tête est accepté, pour que le même fichier puisse aussi être sourceé |
# en début de ligne | un commentaire |
# ailleurs | fait partie de la valeur. Pas de commentaires en fin de ligne : tronquer un jeton sur un # qu’il contient légitimement serait silencieux, alors qu’un commentaire atterrissant dans une valeur est bruyant |
"guillemets" ou 'guillemets' | les guillemets sont retirés et les espaces internes conservés ; une valeur sans guillemets est trimée |
$HOME, ${x} | non développés. Ce n’est pas un shell — pas de substitution, pas de lignes de continuation |
| la même clé deux fois | une erreur de démarrage, plutôt qu’une supposition sur celle qui était voulue |
| une clé que ce programme ne lit pas | un avertissement nommant la clé — RESCRIPTUM_ADMIN_TOKENN est donc attrapé au lieu d’être ignoré |
| un fichier lisible par d’autres | un avertissement avec son mode, parce qu’il peut contenir le jeton admin |
Les avertissements nomment les clés et les chemins, jamais les valeurs.
Le lire et le modifier
rescriptum config affiche chaque variable, sa valeur, et qui des fichiers ou de
l’environnement l’y a mise — la distinction qui compte, puisque les fichiers fournissent
des valeurs par défaut et que l’environnement réel l’emporte. config set modifie le
fichier comme on voudrait qu’il le soit, dans l’un ou l’autre format : commentaires
conservés, réglage commenté décommenté sur place plutôt que dupliqué (fichier
d’environnement) ou valeur remplacée là où elle est (TOML), et refus avant toute écriture
d’une modification qui laisserait
un serveur incapable de démarrer. C’est documenté dans la
référence de la ligne de commande, et c’est ce que
l’application DSM pilote dessous.
Valeurs invalides
| Cas | Ce qui se passe |
|---|---|
Exportée mais vide (RESCRIPTUM_LISTEN_ADDR=) | traitée comme non définie — une valeur vide est une erreur, pas une instruction |
| Uniquement des espaces | pareil, et les valeurs sont trimées |
| Un nombre nul ou impossible à parser | retombe sur la valeur par défaut, plutôt que de démarrer un serveur qui accepte des connexions sans jamais répondre |
RESCRIPTUM_STORE avec toute autre valeur | un avertissement, et files est utilisé |
RESCRIPTUM_ENV_FILE ou RESCRIPTUM_CONFIG nommant un fichier absent, illisible ou malformé | une erreur de démarrage |
| Un réglage TOML recevant une liste ou une table | une erreur de démarrage, contrairement à une clé mal orthographiée, qui avertit |
RESCRIPTUM_STORE=sqlite sur un binaire construit sans la feature | une erreur au démarrage |
Erreurs de démarrage
Celles-ci arrêtent le serveur au lieu d’avertir, parce que démarrer quand même serait pire :
| Condition | Pourquoi c’est fatal |
|---|---|
RESCRIPTUM_ADMIN_ADDR défini avec RESCRIPTUM_STORE autre que sqlite | deux façons de changer la même configuration, en concurrence |
RESCRIPTUM_ADMIN_ADDR défini sans RESCRIPTUM_ADMIN_TOKEN | une API ouverte qui réécrit les identifiants root |
RESCRIPTUM_ADMIN_TOKEN de moins de 16 caractères | assez court pour être deviné |
| L’adresse d’écoute ne peut pas être bindée | rien à faire |
| Le store ne peut pas être ouvert | rien à servir |
RESCRIPTUM_MEDIA_ADDR défini sans RESCRIPTUM_MEDIA_DIR | un listener sans rien à servir |
RESCRIPTUM_MEDIA_ADDR égal à l’adresse de réponse ou d’administration | le second bind perd, et lequel dépend de l’ordre de démarrage |
RESCRIPTUM_PUBLIC_HOST portant un schéma, un port ou un chemin | il est écrit dans les URL de deux listeners ; un port dans la valeur épingle chaque script généré sur l’un d’eux |
RESCRIPTUM_TFTP_ADDR défini sans RESCRIPTUM_BOOT_DIR | un listener sans chargeur à distribuer |
| Le répertoire de démarrage ne peut pas être résolu | chaque contrôle de chemin s’y compare |
RESCRIPTUM_USER nomme un compte inexistant | rien à devenir |
Avertissements de démarrage
Ceux-ci sont affichés et le serveur continue :
| Condition | Ligne |
|---|---|
| Répertoire de réponses absent | warning: … does not exist yet — every request will 404 until it does |
| Le chemin existe mais n’est pas un répertoire | warning: … is not a directory — every request will 404 until it is |
| Répertoire de réponses présent mais illisible | warning: … cannot be read: … — every request will 404 until that is fixed. La cause la plus probable est un serveur tournant sous un utilisateur qui n’est pas le propriétaire du répertoire |
| API d’administration hors boucle locale | warning: the admin API is not bound to loopback — … |
RESCRIPTUM_ANSWER_TOKEN de moins de 16 caractères | un avertissement, pas une erreur — refuser de démarrer laisserait un parc incapable de s’installer |
| Tout problème dans le jeu de réponses | une ligne warning: chacun, le même jeu que signale check |
RESCRIPTUM_PUBLIC_HOST non défini | La réponse de la table de routage, ou l’unique adresse d’interface s’il n’y a pas de route par défaut. Journalisé dans les deux cas, en avertissement nommant les autres adresses s’il y en a. Un hôte derrière du NAT se trompe toujours en silence |
| TFTP ne peut pas se lier | warning: cannot bind TFTP on … — le seul listener dont l’échec de liaison n’est pas fatal. Le port 69 est le seul port privilégié de la conception, donc le seul bind qui puisse échouer pour quelque chose que personne n’a configuré ; les réponses sont le produit, et mourir ferait échouer toutes les installations en cours pour signaler qu’un second port n’a pas pu être ouvert. boot check sort en non-zéro et le message nomme les façons d’obtenir le port |
| Répertoire de médias absent ou illisible | une ligne warning: media: … — un parc ne doit jamais être incapable de s’installer parce qu’une image est bizarre |
Options de compilation
| Feature | Défaut | Effet |
|---|---|---|
sqlite | activée | Le store SQLite et l’API d’administration |
boot | activée | Le catalogue de médias, le lecteur ISO et le listener média |
Mesuré sur ARMv7 (gnueabihf, plancher glibc 2.17), les quatre d’affilée le 2026-08-29. Remesurez plutôt que de citer ces chiffres : ils ont bougé d’environ 375 Ko quand cette cible est passée de musl à glibc, et le jeu qu’ils remplacent ici avait dérivé d’environ 200 Ko.
| Build | Octets |
|---|---|
| les deux (défaut) | 2 813 712 |
sqlite seule | 2 557 592 |
boot seule | 1 649 048 |
| aucune | 1 392 544 |
Limites fixes
Non configurables, et délibérément :
| Limite | Valeur | Où |
|---|---|---|
| Corps de requête | 1 Mo | endpoint de réponse — un Content-Length invraisemblable est refusé depuis l’en-tête |
| Taille d’un document | 256 Ko | PUT de l’API d’administration |
| Requêtes capturées | 1000 captures | comptées depuis le répertoire au démarrage, donc un redémarrage ne repart pas de zéro |
| Échecs d’administration avant blocage | 5 en 60 s | le blocage double jusqu’à un maximum de 900 s |
| Adresses suivies par le garde-fou | 4096 | pour qu’il ne puisse pas être transformé en fuite mémoire |
| Filet de rechargement du listing | 1 s | force une relecture même quand le mtime du répertoire semble inchangé |
Surface HTTP
Surface HTTP
Deux listeners, et ils ne partagent jamais un port. L’endpoint de réponse est ce à quoi parlent les installateurs ; l’API d’administration est désactivée sauf configuration.
L’endpoint de réponse
| Requête | Réponse |
|---|---|
POST n’importe quel chemin | la réponse, typée par son format |
GET n’importe quel chemin | la même chose |
GET /health | 200 OK, corps OK\n — pas de jeton nécessaire, jamais limité en débit |
| toute autre méthode | 405 |
N’importe quel chemin, parce que l’URL est gravée dans une ISO et que ce serveur n’a pas
son mot à dire. Le chemin n’est pas ignoré pour autant : les segments nommant un
alias de format restreignent les documents pouvant répondre, et le chemin
fournit aussi les faits
path, file et segment.
Codes de statut
| Code | Quand |
|---|---|
200 | une réponse s’est appliquée |
400 | le corps n’a pas pu être lu |
401 | RESCRIPTUM_ANSWER_TOKEN est défini et la requête ne l’a pas présenté |
404 | rien n’a revendiqué la requête et il n’y a pas de default pour le format demandé |
405 | une méthode autre que GET ou POST |
413 | corps de plus de 1 Mo, ou Content-Length en annonçant un |
500 | un document ne parse pas, un groupe manque, un template n’a pas pu être rempli, ou la recherche a paniqué |
503 | à RESCRIPTUM_MAX_CONNECTIONS — écrit immédiatement, puis la connexion se ferme |
En-têtes de réponse
| En-tête | Valeur |
|---|---|
Content-Type | selon le format de la réponse — voir la table ci-dessous |
Content-Length | toujours défini |
Connection | close |
WWW-Authenticate | Bearer, sur un 401 |
| Format | Content-Type |
|---|---|
toml, et tous les formats texte (ks, preseed, cfg, seed, ipxe) | text/plain; charset=utf-8 |
yaml, yml | text/yaml; charset=utf-8 |
json, ign | application/json |
xml, autoyast, unattend | application/xml; charset=utf-8 |
Le TOML est servi en text/plain plutôt qu’en application/toml parce que c’est ce
qu’attend l’installateur Proxmox.
Limites de traitement des requêtes
| Plafond du corps | 1 Mo. Un Content-Length invraisemblable est refusé depuis l’en-tête, avant toute lecture — plutôt que d’allouer pour lui et de faire sauter une limite plus tard |
| Délai de lecture des en-têtes | RESCRIPTUM_TIMEOUT_SECS, 10 s par défaut |
| Échéance de la connexion entière | la même valeur. Les deux sont nécessaires : le délai d’en-têtes s’arrête à la fin des en-têtes, donc un client qui promet un corps sans l’envoyer garerait sinon une connexion indéfiniment |
| Concurrence | RESCRIPTUM_MAX_CONNECTIONS en vol ; au-delà, un 503 et fermeture plutôt qu’une mise en file |
| Authentification | seulement quand RESCRIPTUM_ANSWER_TOKEN est défini. Comparée en temps constant. Les échecs sont journalisés et jamais limités en débit |
Authentification
Authorization: Bearer <RESCRIPTUM_ANSWER_TOKEN>
Proxmox l’envoie quand son ISO a été préparée avec --answer-auth-token. Rien d’autre ne le
peut, ce qui est pourquoi c’est désactivé par défaut. Voir
Sécurité.
L’API d’administration
Un listener séparé, RESCRIPTUM_ADMIN_ADDR, en SQLite uniquement. Détails complets sur
sa propre page.
| Requête | Rôle |
|---|---|
GET /machines, GET /groups | lister les identifiants |
GET /machines/{id}, GET /groups/{name}, GET /default | le document stocké, tel qu’écrit |
PUT /machines/{id}, PUT /groups/{name}, PUT /default | stocker un document |
DELETE /machines/{id}, DELETE /groups/{name}, DELETE /default | en supprimer un |
GET /resolve/{id} | la réponse fusionnée que cette machine recevrait |
GET /check | les problèmes actuels |
GET /health | vivacité — pas de jeton, jamais bloqué |
Tous les endpoints de document prennent ?format=<ext>, toml par défaut.
| Code | Quand |
|---|---|
200 | fait |
400 | document malformé, identifiant invalide, ou corps non-UTF-8 |
401 | jeton manquant ou faux |
404 | document ou endpoint inexistant ; rien ne se résout pour cet identifiant |
409 | l’écriture aurait cassé le jeu de réponses (annulée), ou un resolve qui n’a pas pu rendre |
413 | document de plus de 256 Ko |
429 | cette adresse est bloquée ; Retry-After dit pour combien de temps |
500 | le store n’a pas pu être lu ou écrit |
Chaque réponse d’administration définit Connection: close. Un GET /resolve réussi définit
aussi X-Answer-Source, portant la même description que la ligne de log.
Journalisation
Une ligne par requête, sur stderr par défaut. RESCRIPTUM_LOG choisit ce qui est gardé
et RESCRIPTUM_LOG_FILE où cela va — voir
journalisation.
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=example-rack bytes=431
Les lignes de niveau serveur portent - là où serait l’adresse du pair. Voir
dépannage.
Formats et alias d'endpoint
Formats et alias d’endpoint
Trois tables. La version narrative est dans un document par système d’exploitation.
Extensions de document
La liste blanche des extensions que rescriptum ramassera dans un store. Tout le reste est
ignoré — txt n’y est délibérément pas, pour qu’un fichier de notes égaré à côté de vos
réponses ne devienne jamais un candidat.
| Extension | Famille | Superposition | Content-Type |
|---|---|---|---|
toml | TOML | fusion structurelle | text/plain; charset=utf-8 |
yaml, yml | YAML | fusion structurelle | text/yaml; charset=utf-8 |
json, ign | JSON | fusion structurelle | application/json |
xml, autoyast, unattend | XML | fusion structurelle, par élément | application/xml; charset=utf-8 |
ks, cfg, preseed, seed, ipxe | texte | concaténation dans l’ordre des couches | text/plain; charset=utf-8 |
La famille est ce que remonte le champ format= de la ligne de log, donc ks et
preseed apparaissent tous deux en format=text. L’extension est ce sur quoi un endpoint
filtre, et ce dont check a besoin pour choisir le bon validateur.
Alias d’endpoint
Un segment de chemin nommant l’un de ceux-ci restreint la réponse aux documents portant les
extensions listées. N’importe quel segment du chemin peut le nommer, donc /rhel/ks,
/ks et /provision/rhel/node.cfg restreignent tous au kickstart.
| Segment | Sert | Usage typique |
|---|---|---|
proxmox, pve, toml | .toml | Proxmox VE |
debian, preseed | .preseed, .seed | preseed Debian |
rhel, centos, fedora, alma, rocky, kickstart, ks | .ks | kickstart |
ubuntu, autoinstall, cloudinit, nocloud, yaml, yml | .yaml, .yml | autoinstall Ubuntu, cloud-init |
flatcar, coreos, ignition, ign | .ign, .json | Ignition |
suse, opensuse, autoyast | .autoyast, .xml | AutoYaST |
windows, unattend | .unattend, .xml | unattend.xml Windows |
json | .json, .ign | |
xml | .xml | |
cfg | .cfg | |
ipxe | .ipxe |
Un segment n’en nommant aucun ne contraint rien, ce qui est pourquoi /answer continue
de fonctionner exactement comme avant.
Deux pièges dans cette table
- Le filtrage porte sur l’extension, pas sur la famille.
.kset.preseedsont tous deux des documents texte ; filtrer par famille laisserait un preseed répondre à/rhel/ks. seedn’est délibérément pas un alias.s=http://server/seed/est une URL de seed NoCloud parfaitement ordinaire, et elle sert du YAML. Un alias doit être assez spécifique pour que personne ne l’atteigne par accident. (L’extension.seedexiste toujours, et/debian/la sert.)
Clés de contrôle, par format
Retirées avant que la réponse ne soit envoyée.
| Format | Écriture |
|---|---|
| TOML | extends = "base", members = […], table [match], au premier niveau |
| YAML | extends:, members:, match: au premier niveau |
| JSON | "extends", "members", "match" au premier niveau |
| XML | <answer-meta extends="base"><member>…</member><match k="v" /></answer-meta> |
| Texte | # answer: extends <nom> · # answer: member a, b · # answer: match k=v k2=v2 |
Les directives texte acceptent aussi // comme marqueur de commentaire. match prend des
paires clé=motif séparées par des espaces ; member une liste séparée par des virgules.
Les commentaires ordinaires d’un document texte sont servis — seules les lignes
# answer: sont retirées.
Sémantique de fusion
| Formats structurés | Formats texte | |
|---|---|---|
| Maps / objets / éléments | fusionnent récursivement | — |
| Scalaires | la couche supérieure remplace | — |
| Tableaux / listes | remplacent, jamais de concaténation | — |
| Document entier | — | concaténé dans l’ordre des couches |
XML apparie les frères par nom d’élément plus un attribut discriminant — name, id, key,
alias, pass — et respecte config:type="list". Déclarations, doctypes, espaces de noms et
attributs survivent à une fusion ; l’indentation d’origine et le placement des commentaires,
non.
Validateurs que check peut appeler
| Format | Outil | Invoqué comme |
|---|---|---|
toml | proxmox-auto-install-assistant | validate-answer <fichier> |
xml, autoyast, unattend | xmllint | --noout <fichier> |
ks | ksvalidator | <fichier> |
| tout le reste | — | aucun n’existe |
Un outil absent du PATH est signalé une fois comme note, jamais comme un échec.
Ligne de commande
Ligne de commande
Sans argument, rescriptum lance le serveur. Tout le reste est une sous-commande.
| Commande | Rôle |
|---|---|
rescriptum | lancer le serveur |
rescriptum render <id> | afficher la réponse que cet identifiant recevrait |
rescriptum render --body FICHIER | …pour un corps de requête capturé |
rescriptum render --query Q | …pour des étiquettes, p. ex. "mac=aa:bb&serial=7ABC1" |
rescriptum check | rendre tout le store configuré et signaler ce qui casse |
rescriptum import <dir> | copier un répertoire de documents dans le store configuré |
rescriptum export <dir> | écrire le store configuré comme un répertoire de documents |
rescriptum migrate [<dir>] | montrer ce que deviendrait un répertoire de réponses plat |
rescriptum migrate --apply | déplacer ces documents dans un répertoire chacun |
rescriptum config | afficher la configuration, et d’où vient chaque valeur |
rescriptum config --json | la même chose, pour un panneau de réglages |
rescriptum config --value CLÉ | une valeur, pour un script — jamais un identifiant |
rescriptum config set C=V … | éditer le fichier que RESCRIPTUM_CONFIG ou RESCRIPTUM_ENV_FILE nomme |
rescriptum config unset CLÉ … | y retirer un réglage |
rescriptum --help | usage et variables d’environnement |
Toutes lisent les mêmes variables d’environnement, dont
RESCRIPTUM_CONFIG et
RESCRIPTUM_ENV_FILE — résolus en premier,
donc un fichier illisible arrête toute commande ayant besoin de la configuration. --help
et --version répondent avant sa lecture, parce que ce sont les commandes qu’on lance
quand quelque chose ne va pas. Il n’y a pas d’options globales.
render
$ rescriptum render 98:fa:9b:50:d8:10
$ rescriptum render --query "serial=7ABC123&mac=98:fa:9b:50:d8:10"
$ rescriptum render --query "path=/rhel/ks&serial=7ABC123"
$ rescriptum render --body /var/log/rescriptum-captures/2026…-0000.body
| Forme | Faits fournis |
|---|---|
<id> | l’identifiant comme botte de foin, et rien d’autre — assez pour correspondre par nom, pas assez pour un sélecteur sur serial |
--query "k=v&k2=v2" | ces étiquettes, décodées. path= fournit aussi file et segment, et contraint le format comme le ferait une vraie URL |
--body FICHIER | le fichier verbatim : botte de foin, plus le JSON aplati s’il parse comme du JSON |
- Le document part sur stdout ; la ligne
# format=… machine=… group=…expliquant comment il a été obtenu part sur stderr. Doncrender … > answer.tomlne donne que le document. - Les problèmes de chargement sont d’abord affichés comme lignes
warning:. - Sortie 0 quand quelque chose s’est résolu, 1 quand rien ne s’appliquait (le serveur
aurait renvoyé un
404) ou que le rendu a échoué.
check
$ rescriptum check
Signale les problèmes de chargement, rend chaque document machine et chaque membre de groupe,
nomme les groupes qui sélectionnent sur un bloc match (qu’il ne peut pas essayer sans vraie
requête), et appelle le validateur de l’installateur là où il est dans le PATH.
Sortie 0 quand tout se rend, 1 dès que quelque chose a échoué — il tombe donc tel quel dans une CI. Voir validation.
import / export
$ RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db rescriptum import /srv/answers
$ RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db rescriptum export /tmp/backup
import lit un répertoire et écrit dans le store configuré ; export fait l’inverse.
L’aller-retour est identique octet pour octet, chemins compris. Aucun des deux ne lance
check pour vous — la sortie vous le dit.
migrate
Les réponses étaient des fichiers à la racine du répertoire de réponses — 98fa9b50d810.toml
à côté de 98fa9b50d810.preseed. Elles ont désormais un répertoire chacune, et un document
resté à plat est signalé et non servi. Cette commande les déplace :
$ rescriptum migrate
migrating /srv/answers
98fa9b50d810.toml -> 98fa9b50d810/proxmox.toml
98fa9b50d810.ipxe -> 98fa9b50d810/boot.ipxe
groups/rack-a.toml -> groups/rack-a/proxmox.toml
default.toml -> default/proxmox.toml
4 document(s) to move — nothing has been changed. Re-run with --apply.
Elle montre par défaut et ne déplace que si on le lui demande. Le répertoire de réponses est ce à partir de quoi une baie s’installe ; taper la commande pour savoir ce qu’elle ferait ne doit pas le réorganiser.
--apply effectue les déplacements, chacun un rename dans le même répertoire, si bien
qu’aucun document n’est jamais réécrit. Si une destination est déjà prise, rien ne bouge du
tout — y compris les documents qui auraient pu — et les collisions sont nommées : un
répertoire à moitié migré est l’état sur lequel personne ne peut raisonner. Elle prend un
répertoire en argument, RESCRIPTUM_ANSWERS_DIR par défaut, et sur un répertoire déjà migré
elle dit qu’il n’y a rien à déplacer.
config
La configuration, ce sont des variables d’environnement, et sur un déploiement qui les lit depuis un fichier — une installation par paquet, surtout — voici comment les voir et les changer sans ouvrir d’éditeur. C’est aussi ce que l’application DSM exécute dessous.
$ rescriptum config
env file: /var/packages/rescriptum/etc/rescriptum.env
RESCRIPTUM_STORE files default
RESCRIPTUM_ANSWERS_DIR /volume1/netboot/answers file
RESCRIPTUM_LISTEN_ADDR 0.0.0.0:9000 environment
RESCRIPTUM_ADMIN_TOKEN (set) file
La troisième colonne est l’essentiel. Les fichiers fournissent des valeurs par défaut
et l’environnement réel l’emporte : une valeur marquée environment ne peut donc pas être
changée en éditant un fichier — et config set le dit, plutôt que de vous laisser écrire
quelque chose que le serveur en cours continuera d’ignorer. Avec un fichier TOML la colonne
affiche toml file, et nommer les deux fichiers affiche les deux chemins ainsi que l’ordre
dans lequel ils l’emportent.
config set écrit dans le fichier TOML quand les deux sont nommés, parce que c’est
celui que le serveur lit en premier : écrire l’autre serait une modification qui ne change
rien en silence.
Un identifiant n’est jamais affiché, sous aucune forme de cette commande. Un jeton
apparaît comme (set) ou (not set) ; --value refuse tout net.
$ rescriptum config set RESCRIPTUM_LOG=problems RESCRIPTUM_CAPTURE_DIR=/srv/captures
wrote /var/packages/rescriptum/etc/rescriptum.env
L’écriture laisse le fichier tel qu’il est par ailleurs : les commentaires restent, un
réglage est remplacé là où il se trouve, et un réglage commenté est décommenté sur place
plutôt qu’ajouté en dessous — ce qui compte quand le commentaire au-dessus est la seule
documentation qu’a le fichier. Dans un fichier TOML, le même soin s’applique au document :
la valeur est remplacée là où elle est, son commentaire de fin de ligne survit, et config unset vide la valeur au lieu de supprimer la ligne, pour que le paragraphe qui
explique le réglage reste en place.
Deux refus sont délibérés :
- Une modification qui laisserait un serveur incapable de démarrer est refusée, en bloc, avant toute écriture. Activer l’API d’administration sans jeton, ou avec un jeton de moins de 16 caractères, vous vaut la raison plutôt qu’un prochain démarrage cassé.
- Une variable mal orthographiée est refusée. Écrite, elle serait relue comme une inconnue et signalée au démarrage suivant, quand plus personne ne fait le lien.
Contrairement à toutes les autres sous-commandes, celle-ci fonctionne quand la configuration est trop cassée pour démarrer un serveur — un fichier qui ne parse pas, un jeton d’un caractère trop court. C’est l’état dont on se sert d’elle pour sortir.
media
Les médias de démarrage : les images d’installation que ce serveur détient. Chacune de
ces commandes exige RESCRIPTUM_MEDIA_DIR ; sans elle, elles le disent et sortent en
1. Voir Servir les médias de démarrage.
$ rescriptum media list # ce qui est détenu : famille, architecture, version, empreinte
$ rescriptum media add FILE [--sha256 D] # enregistrer une image déjà dans le répertoire
$ rescriptum media add URL --sha256 D # la récupérer dedans, puis l'enregistrer
$ rescriptum media check # revérifier chaque empreinte enregistrée
$ rescriptum media ipxe ID # imprimer la réponse .ipxe qui démarre une image
$ rescriptum media prepare ID [--url URL] # une image Proxmox avec son URL de réponse dedans
$ rescriptum media export ID FICHIER # matérialiser une entrée préparée, pour une clé
media add prend un fichier déjà dans le répertoire de médias — rien n’est
téléchargé et rien n’est copié. Il le hache avec une progression, l’analyse, et écrit un
fichier compagnon .media à côté. --sha256 est vérifié avant tout enregistrement :
un écart n’écrit rien et sort en 1.
Le code de sortie de media check est un contrat, comme celui de check. deploy.sh
s’y fie.
media ipxe imprime sur stdout et met tout le reste sur stderr, de sorte que
rescriptum media ipxe pve-8.4 > groups/rack-a/boot.ipxe produit un document de réponse
utilisable — ce qu’il est, rien de plus. Il imprime un script, il n’en installe pas.
boot
La moitié démarrage réseau : les chargeurs de TFTP, la configuration DHCP générée, et les deux scripts qu’une machine exécute. Voir Démarrer une machine par le réseau.
$ rescriptum boot dhcp-snippet [--format F] [--one-loader]
$ rescriptum boot check # les chargeurs qu'un extrait nomme sont-ils sur le disque ?
$ rescriptum boot bootstrap # imprimer le script de l'étape deux
$ rescriptum boot menu # imprimer le menu intégré
--format vaut dnsmasq (par défaut), isc, kea, powershell, pfsense ou
mikrotik. L’extrait part sur stdout et les avertissements sur stderr, de sorte que
boot dhcp-snippet > dhcpd.conf produit un fichier incluable tel quel.
Le code de sortie de boot check est un contrat, comme celui de check. Ce qu’il
attrape est la panne la moins diagnosticable de la chaîne : un extrait nommant un
chargeur absent du disque échoue silencieusement au niveau de la ROM, sans rien sur
aucune console. Il signale aussi que le listener média a quitté le port que les chargeurs
distribués ont gravé.
boot bootstrap et boot menu impriment ce qu’une machine exécutera, pour la même
raison que render imprime une réponse : tout ce qu’une baie exécute devrait d’abord
être lisible par un humain.
Codes de sortie
| Code | Signifie |
|---|---|
0 | succès |
1 | la commande a échoué — rien ne s’est résolu, un document ne parse pas, le store n’a pas pu être ouvert |
config est la seule à avoir un second sens : 0 dit que la configuration en est une sur
laquelle le serveur démarrerait, 1 qu’elle ne l’est pas — ou qu’une écriture a été
refusée. Cela la rend utilisable depuis un script, comme check.
Le serveur lui-même sort en 0 sur SIGTERM ou Ctrl-C, et en 1 s’il ne peut pas binder ou
ouvrir le store.