Wheelhouse docs

Installing without a keyboard#

A Wheelhouse machine can install itself. Give it a seed — a small filesystem holding a cloud-config, or a URL that serves one — and on the live boot the installer picks up the answers before it draws anything, installs, and reboots into a configured router. The same seed can carry a whole configuration, so a replacement box comes up with the addresses, forwards, reservations and firewall of the one it replaces.

Everything a seed answers is a question from Every installer question. Read that page first: an unattended install is not a different install, it is the same one with the answers supplied up front.

The short version#

bash
tools/wheelhouse-seed.py --user-data seed.yaml --out seed.iso

Write seed.iso to a USB stick with dd, or attach it as a second disk on a VM. Boot the Wheelhouse image with it plugged in. The installer finds it, installs to the machine's only other disk, and reboots.

--out needs xorrisofs, genisoimage or mkisofs. --out-dir writes the files instead, for a stick you formatted and labelled yourself.

Where the installer looks#

In order, stopping at the first seed it finds:

  1. The kernel command line. wheelhouse.autoinstall=<value>, where the value is a URL, a device (/dev/sdb1), label:NAME, a path on the live image, auto for the normal search, or off to insist on a person.
  2. A labelled filesystem. Any partition labelled WHEELHOUSE or CIDATA — cloud-init's NoCloud name — holding wheelhouse.conf, wheelhouse.yaml, wheelhouse.yml or user-data. Names are matched without regard to case, and with the version suffix ISO9660 adds (USER-DATA.;1), because a seed built with plain mkisofs looks like that.
  3. cloud-init's own seed, when cloud-init has already found one: /var/lib/cloud/seed/nocloud, nocloud-net, or the current instance.
  4. /usr/share/wheelhouse/autoinstall.conf, for images built to install a fleet the same way every time.

The seed finder is a program in its own right. Run it to ask which seed would be used, without installing anything:

bash
sudo wheelhouse-autoinstall

It prints the path of an answers file and exits 0, or exits 1 when the machine should be installed by a person. Nothing is written to any disk: it only reads.

The cloud-config#

yaml
#cloud-config
wheelhouse:
  disk: auto                  # auto = the one disk that is not the boot medium
  hostname: edge
  timezone: America/Los_Angeles
  wan:
    interface: eth1
    addressing: dhcp          # or static
    address: 203.0.113.2/24   # static only
    gateway: 203.0.113.1      # static only
    dns: [9.9.9.9, 1.1.1.1]   # static only
  lan:
    interface: eth0
    address: 10.0.0.1/16
    dhcp_server: true         # hand out addresses and answer DNS
    firewall: true            # block from the WAN, allow the LAN out
    nat: true                 # share the WAN address
  passwords:
    admin: "the web UI password"
    console: "the vyos password"
  config:
    commands: config.commands # a file on the seed, a URL, or inline text
    boot: config.boot         # or a whole VyOS configuration to load
  ssh_authorized_keys:
    - ssh-ed25519 AAAA... you@laptop
  reboot: true                # false stops after installing
  poweroff: false             # true powers off instead of rebooting
  keep_previous: false
  overwrite: false            # reinstall over a disk that already has a system
  on_error: halt              # halt | poweroff | menu | shell | exit

Everything is optional except the two passwords and a disk. A seed that only sets config.commands gets its addresses from the restored configuration, so the wan: and lan: sections can be left out entirely.

Three defaults are decided rather than copied, and they are worth knowing:

  • firewall follows the WAN, not the LAN. A seed with a WAN and no LAN describes a router facing the internet with nothing behind it, which is the one that most needs a closed front door. lan: {firewall: ...} still works, because seeds already written say it that way.
  • A seed that carries a configuration does not get the installer's firewall unless it asks for one. Two writers would number the same rules differently and the router would refuse the result, so whichever brings a firewall writes it.
  • management_source sits at the top level, not under lan:. On a box with no LAN it is the difference between an install you can reach afterwards and one you can only reach at the console.

config.commands is a file of set lines — the same lines show configuration commands prints, and what the OPNsense importer writes. config.boot is a whole VyOS configuration file, which VyOS loads and validates itself. The installer applies either after the passwords are set, so a restored firewall can be as closed as you like without locking the installer out of the agent.

Plain answers instead of YAML#

A seed can also be KEY=VALUE lines, which is what --answers has always taken:

DISK=sda
HOSTNAME=edge
TIMEZONE=Europe/Berlin
WAN_IFACE=eth1
WAN_ADDRESSING=dhcp
LAN_IFACE=eth0
LAN_ADDRESS=10.0.0.1/16
LAN_DHCP=yes
FIREWALL=yes
NAT=yes
CONFIG_COMMANDS=config.commands
ADMIN_PASSWORD=...
VYOS_PASSWORD=...
REBOOT=yes
ON_ERROR=halt

Without ON_ERROR, a seeded install halts the machine and a hand-run --answers install just exits — which is what each one wants. A seed with nobody watching should stop the machine; a person running the installer by hand should get their shell back rather than a halted box.

The three guards#

An unattended installer that can erase a disk needs to be unable to do it twice. Three things stop that.

At most once per boot. The installer marks the attempt before it starts. If it cannot finish, the next start of the installer shows the menu instead of erasing a disk again.

It will not reinstall over an existing system. If the target disk already holds a system and the seed does not say overwrite: true, the installer says so and leaves the disk alone. The common way to meet this is the seed medium left plugged in when the machine reboots after its first install, and erasing the install it just made would be the worst possible reading of "unattended".

disk: auto refuses to guess. It means the one disk that is not the boot medium. With more than one candidate the installer refuses and tells you to name a disk in the seed, because picking the wrong one erases the wrong machine.

Seeds over the network#

A seed sets this router's passwords and its configuration, so a seed URL is fetched over HTTPS or not at all. Two things on the kernel command line change that:

Kernel parameterEffect
wheelhouse.autoinstall=https://…fetch the seed from that URL
wheelhouse.autoinstall.sha256=<hex>the fetched seed must have that SHA-256, or it is refused
wheelhouse.insecure=1plain HTTP and FTP are permitted; the log records that it was asked for

The same rule covers the payloads a seed names: an http:// or ftp:// config.commands is refused for the same reason, unless wheelhouse.insecure=1 is on the command line.

The seed is a credential#

It holds both passwords in the clear, because the installer has to be able to set them. Hand it to the machine, then wipe it. wheelhouse-seed.py says so on every run.

Public keys are the way to avoid a password living on a stick longer than it has to:

bash
tools/wheelhouse-seed.py --user-data seed.yaml --ssh-key ~/.ssh/id_ed25519.pub --out seed.iso

Keys are placed on the vyos console account. SSH itself is still off — the keys are there for when you turn it on.

Watching an install#

The console shows each stage, and everything lands in /var/log/wheelhouse-install.log on the live system. On failure the log is the first place to look, and the installer names it in its own error message.

To try a seed by hand on a live system without rebooting:

bash
sudo wheelhouse-install --seed              # find a seed and use it
sudo wheelhouse-autoinstall                 # just say which seed would be used
sudo wheelhouse-install --answers /path/to/answers.conf

And to see what a set of answers would write, changing nothing:

bash
wheelhouse-install --answers /path/to/answers.conf --commands

What on_error does#

ValueWhat happens when an unattended install cannot finish
halthalt the machine. The default for a seeded install.
poweroffpower it off
menufall through to the interactive installer
shelldrop to a login shell, naming the command to run the installer again
exitexit with status 1. The default for a hand-run --answers install.

Whatever it does, it says first that the install did not finish and where the log is.

See also#


Checked against docs/unattended-install.md, packaging/wheelhouse-autoinstall, packaging/seed-to-answers.py, packaging/wheelhouse-install, tools/wheelhouse-seed.py, tools/tests/test_seed.py, packaging/README.md.

Updated 2026-09-02 unattended seed cloud-config autoinstall