← Back to site Loading…

rescriptum

Groups and merging

Guide

Generated on August 30, 2026

Groups and merging

Groups and merging

A rack of machines usually shares everything except its MAC addresses. Writing that out once per machine is how a fleet’s configuration drifts. So answers compose.

answers/
├── groups/
│   ├── base/
│   │   └── proxmox.toml       shared by everything
│   └── rack-a/
│       └── proxmox.toml       extends = "base"; members = [ … ]
├── 98-fa-9b-50-d8-10/
│   └── proxmox.toml           one machine's overrides (optional)
└── default/
    └── proxmox.toml           only when nothing else matches

The shared part

# 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"]

The difference

A machine that differs gets a document with only the difference in it:

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

[disk-setup]
zfs.raid  = "raid10"                       # this one has four disks
disk-list = ["sda", "sdb", "sdc", "sdd"]

…and receives the two merged, with its own values winning:

$ 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"]

Merge rules

Layersgroup chain first, machine document last — the machine always wins
Mapsmerge recursively, including TOML’s inline and dotted tables
Other valuesreplaced outright by the higher layer
Arraysreplace, they do not append
Text formatsconcatenated in layer order — see formats

Why arrays replace. Appending is the intuitive choice right until you need to shorten a list. disk-list = ["sda", "sdb"] in a group and ["sda"] in a machine document has exactly one sensible meaning — this one has a single disk — and appending cannot express it. The same rule holds in every format, so you never have to remember which one you are in.

extends

A group may extend another group, giving a chain — what every rack shares in one file, per-rack differences in another:

# 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"

Layers then apply baserack-a → machine document.

extends in a machine document overrides membership. It is the escape hatch for a machine that needs a group it is not listed in:

# answers/98-fa-9b-50-d8-99/proxmox.toml
extends = "rack-a"          # even though rack-a does not list this MAC

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

extends resolves within one format — layering a preseed onto a TOML base is meaningless, and the merge would refuse it anyway.

Only the first matching group applies

If two groups both claim a machine, one of them applies — the most specific, ties broken on sorted name. Compose with extends rather than relying on several groups matching at once: the order between them would be arbitrary, and an arbitrary order is how a machine quietly gets the wrong disk layout.

When a group is broken

Cycles and missing parents are detected when the store is read, reported in the log once, and the broken group is dropped rather than half-applied:

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

One bad group does not stop the other racks from installing. A machine that needed that group gets a loud 500 rather than a half-built answer — serving a configuration whose base is missing would install the machine half-configured, and nobody would find out until it was running.

rescriptum check reports the same problems, which is a better place to find out than the log at 3am.

Grouping is the fast path

Measured at 2,000 machines, 3,000 requests at 100 concurrent:

LayoutThroughput
2,000 machine documents, no group12,132 req/s
one group of 2,000 members, no machine documents13,036 req/s
2,000 machine documents plus a group (a merge per request)8,816 req/s

A group with no machine overrides and no placeholders is rendered once, when the store is read, and served afterwards as a prepared string. The common datacenter case parses nothing per request. Adding a per-machine override buys a merge per request — worth it where it is needed, and worth avoiding where it is not.

The other half of the same argument is what a read costs. The whole store is re-read at most once a second, and with a directory per identity that read is a readdir per machine on top of the file it already opened — measured at 2,000 machines on an M1 Pro, 28 ms before the layout changed and 63 ms after. It is amortised over a second’s worth of requests either way, and the throughput figures above did not move measurably; but a group that needs no per-machine directory avoids that cost too.

Next

  • Templating{{ serial }} removes the remaining reason for a directory per machine.
  • Validating — a merged answer is a document nobody wrote; look at it before a rack does.