← Blog/linux

Units That Do Not Exist Until Boot: systemd Generators

Every unit file you have written is static - it sits on disk and systemd reads it. A generator produces units at boot based on logic: read a config, check for hardware, inspect the kernel command line, and build the unit that fits. This is how systemd turns legacy formats like fstab into native units, and you can do the same with any format you like.

A unit file is a static declaration. It describes a service, a mount, a target - and it sits on disk unchanged between boots. That is fine for most things, but it means you cannot make decisions. You cannot say "if this hardware exists, start this service" or "read this config file and create a service per line" in a unit file, because a unit file has no control flow.

Generators are the answer, and they are one of the strongest and least known extension points systemd offers.

What a generator actually is

A generator is an executable - systemd's own are compiled binaries, yours can just as well be a shell script - that systemd runs very early at boot, before unit files are loaded, and again on every daemon-reload. Its job is to produce .mount, .service, or .target files and drop them in a designated directory. systemd then loads those generated units exactly like any other unit - and, like any other unit, starts one only if something pulls it in.

That is the whole mechanism. A generator is a layer that builds units at runtime instead of requiring them on disk ahead of time.

Why this is more powerful than it looks

The difference from a regular unit is fundamental: a unit is data, a generator is code. It can read the environment, make decisions, and produce different units depending on what it finds.

Some examples that make this concrete:

  • Read a custom config file and create one service per line.
  • Detect whether specific hardware is present and enable a service conditionally.
  • Parse the kernel command line and alter boot behaviour based on a parameter.

systemd itself uses this mechanism everywhere. systemd-fstab-generator reads /etc/fstab - a format that predates systemd by decades - and converts each line into a native .mount or .swap unit. You can do the same with any format you invent - though, as the man page is quick to warn, it is often better to deprecate an old format than to write a generator that keeps it alive.

Three output directories, three priorities

systemd passes three directory paths as arguments to every generator. Which one you write to determines your unit's priority:

$1 → /run/systemd/generator/         # normal - overrides /usr/lib, not /etc
$2 → /run/systemd/generator.early/   # early  - overrides even /etc
$3 → /run/systemd/generator.late/    # late   - only if nothing else provides it

Normal is the right choice almost every time: a generated unit beats a distro unit of the same name, but an administrator's file in /etc still beats it. Early takes that escape hatch away - it beats all vendor and admin configuration - so save it for the rare unit that must win. (Only /run/systemd/system.control/ and transient units outrank even that.)

If you have ever run systemctl cat on a mount unit and seen /run/systemd/generator/ as the source, that is fstab-generator writing into the normal-priority directory.

All three paths live under /run/, which is a tmpfs - it exists only in memory and is rebuilt every boot. That is intentional: generators run fresh on every boot, so their output is always current.

Where the generator itself lives

There are four generator directories, searched in this priority order:

/run/systemd/system-generators/         # runtime, highest
/etc/systemd/system-generators/          # yours
/usr/local/lib/systemd/system-generators/
/usr/lib/systemd/system-generators/      # the distro's

Drop yours in /etc/, where it overrides a distro generator of the same name. Note the twist: for generators, /run/ outranks /etc/ - the reverse of the order systemd uses when loading units. It rarely matters, but it is the kind of detail that costs an hour when it does.

A working example

A generator that creates a oneshot service and hooks it into boot:

#!/bin/bash
# Install: /etc/systemd/system-generators/my-generator

# systemd passes three arguments: the normal, early and late output directories
NORMAL_DIR="$1"

cat > "$NORMAL_DIR/hello-generated.service" <<EOF
[Unit]
Description=A service created by my generator

[Service]
Type=oneshot
ExecStart=/bin/echo "I was generated at boot"
EOF

# a generated unit is not enabled on its own - pull it into multi-user.target
mkdir -p "$NORMAL_DIR/multi-user.target.wants"
ln -sf ../hello-generated.service "$NORMAL_DIR/multi-user.target.wants/hello-generated.service"

Make it executable and test without rebooting:

chmod +x /etc/systemd/system-generators/my-generator
systemctl daemon-reload
systemctl cat hello-generated.service

That daemon-reload re-runs all generators, which is why you can test without a reboot. The unit appears in /run/systemd/generator/ as if it had always been there.

The .wants/ symlink is what makes it run at boot. Leave it out and the unit exists, systemctl cat shows it, and nothing ever starts it - exactly like a unit file you wrote and never enabled. After the next boot, journalctl -u hello-generated.service shows the echo.

The rules are strict, and for good reason

Generators run in a severely constrained environment - early boot, before the system is ready. Three rules follow from that:

They must be fast. The entire boot blocks on generators completing. No network calls, no heavy computation, no waiting on anything. Read a file, write a unit, exit.

They cannot depend on services - or on every filesystem. A generator may not talk to any other process: not syslog, not systemd itself, so no systemctl. It can count on /usr, /run, /proc, /sys and /dev, but non-essential filesystems like /var and /home are mounted after generators run. A config file in /etc is a safe input; one in /var may not be there yet.

They must not fail. A generator runs on every boot and every reload with nobody watching. If it chokes on a missing file or bad input, the units it should have produced are missing or half-written - and if it writes a broken unit that boot depends on, such as a mount for a device that isn't there, boot can end in emergency mode. Defensive coding is not optional - handle missing files, bad input, and unexpected state gracefully.

This is why generators are almost always short and simple: read one file, write one or more units, and get out.

Where generators appear in the real world

This mechanism shows up in production infrastructure more than most people realise:

cloud-init ships a real generator, and it is a good illustration of the rules above. An instance learns its hostname, network configuration and mounts from a datasource - on most public clouds, a metadata service at 169.254.169.254 - but a generator cannot fetch anything: it may not talk to other processes, and the real network is not up. So cloud-init's generator makes only the decision that needs no network. It runs ds-identify, which works out from local information whether cloud-init should run at all - no supported datasource, or cloud-init disabled on the kernel command line, means no. If the answer is yes, the generator links cloud-init.target into multi-user.target.wants, in the early directory. The datasource is read later by cloud-init's own services - and on clouds like EC2 its local stage takes a temporary DHCP lease to reach the metadata service before the real network is configured.

OS images and appliances use generators to defer decisions from build time to boot time. Instead of baking a fixed set of units into an image, a generator can read the kernel command line or a config file and produce the units that fit the machine it finds itself on.

systemd itself ships most of the generators already running on your machine. systemd-cryptsetup-generator turns /etc/crypttab into units, systemd-getty-generator puts a login prompt on the kernel's serial consoles and on virtualizer consoles, and systemd-gpt-auto-generator discovers partitions on the boot disk by their GPT partition type and mounts the ones fstab doesn't already configure (and whose mount points aren't already populated). ls /usr/lib/systemd/system-generators/ shows the ones your distribution installed.

The design insight

The architecture here is worth pausing on. systemd could have said "everything must be a unit file, learn the format." Instead, it provided an extension point: anyone can take any configuration format and write a small program that translates it into units. The generator is a bridge between the world as it is and the world as systemd models it.

Combined with drop-in overrides and custom targets, generators make systemd not just an init system but a platform - one you extend rather than work around. That is also why the man page cautions against reaching for a generator too eagerly: translating every legacy format into units keeps the old format alive when deprecating it might serve everyone better. The extension point is powerful precisely because it is general, and the discipline is in not using it for everything you could.

The takeaway

A generator is code that runs before the system exists and produces the units that define it. It is how systemd bridges static configuration and dynamic reality - and the fact that the same mechanism that turns fstab into mount units is available to you, as a script in /etc/systemd/system-generators/, is one of those features that separates operators who configure systemd from operators who extend it.

References

← All posts