Skip to content

Running via systemd

cloud-init-pve doesn't do anything at boot on its own — something has to invoke it. contrib/systemd/ has two example units for that, split around the one thing in a cloud-init-pve pass that actually needs a network: the package upgrade.

Why two units

Everything except the package upgrade — renaming/addressing network interfaces, setting the hostname, creating the user and its SSH keys — is local-only and should run as early in boot as possible, ideally before systemd-networkd itself starts, so the interface config it writes is picked up on networkd's first start rather than needing a reload.

The package upgrade is the opposite: it needs a working, resolvable network connection, which plainly isn't available yet at that point in boot, and it can be slow — running apt-get dist-upgrade inline with the rest would either fail (no network yet) or hold up reaching a login prompt (if ordered later, but still required by boot-critical targets).

cloud-init-pve's -stage flag splits these two concerns so they can run as separate, independently-ordered systemd units:

Stage Flag Does Needs
boot -stage=boot network, FQDN, user nothing (local only)
final -stage=final package upgrade a working network
both (no flag) everything above

Run with no -stage flag (the default) for a one-shot manual invocation — it does both in sequence, same as before this split existed.

The example units

  • cloud-init-pve.service-stage=boot, ordered Before=systemd-networkd.service.
  • cloud-init-pve-final.service-stage=final, ordered After=network-online.target, only Wants-ed (not Requires-d) by multi-user.target so a slow or failed upgrade never blocks reaching a login prompt.

The .deb release installs both to /lib/systemd/system/ and enables them on install (systemctl enable, no --now) — so they're wired up to run on the next boot, but nothing starts immediately as a side effect of apt install. That matters when this package gets installed as part of building a VM template: the template's chroot install won't itself trigger a cloud-init-pve run, only the template's eventual real first boot will. Removing the package disables both units again first, so no dangling enable symlinks are left behind. If you don't want them enabled at all (e.g. you're not ready to review them yet), disable after install:

systemctl disable cloud-init-pve.service cloud-init-pve-final.service

Building from source, or using the .tar.gz release, install either (or both) by copying to /etc/systemd/system/ and enabling manually:

systemctl enable cloud-init-pve.service cloud-init-pve-final.service

See the comments in each file for the full reasoning behind its ordering, including a caveat about interface renaming racing against very early driver loading.

How the split stays safe across reboots

Both fqdn and package_upgrade are only honored on the guest's first boot (see Cloud-Init Data) — but "first boot" means two different things for the two stages once they're split across separate processes:

  • The boot stage's own run creates cloud-init-pve's state file. By the time the final stage runs later that same boot, the state file already exists.
  • If "first run" just meant "state file doesn't exist yet", the final stage would see a state file (written moments earlier by the boot stage) and conclude it's not the first boot — and skip the package upgrade entirely, even on the actual first boot.

So the package upgrade tracks its own package_upgrade_applied flag in the state file, set only once the upgrade has actually run, independently of whether the state file exists at all. The boot stage never touches it.