explain

Readiness protocol (custom images & bootstrap)

Full contract: guest readiness files, agent health fields, wait=bootstrap, and when a sandbox is considered ready.

Custom images and long first-boot setup need a shared contract with grain: how to report what is happening, when the sandbox is done, and when it failed—so grain new, create streams, grain health, and grain status stay accurate.

Ready means: the VM is up, the guest agent is healthy, and bootstrap completed with zero failures (state=ready). Failure or timeout does not report ready.

This page is the full contract. Stock goldens already satisfy a compatible subset without custom files. Sandbox recipe files (grain new --recipe) implement this protocol for you: they generate cloud-init that stamps readiness as each step runs.

Goals

  1. Guest-authoritative — The guest declares progress and done; the host polls and surfaces it.
  2. Compatible by defaultgrain-ubuntu and current cloud-init keep working with --wait agent / userdata.
  3. Progress is first-class — Create progress and status show human-readable bootstrap text.
  4. Fail closed for explicit waits--wait bootstrap does not return success until the guest says ready (or fails hard on failed / timeout).

Guest files

Under /var/lib/grain/readiness/ (create the directory as needed):

FileRequiredMeaning
stateyes (for custom bootstrap)pending | running | ready | failed
phaseoptionalShort step id, e.g. packages, configure
messageoptionalOne-line human status
ready_nameoptionalNamed condition, e.g. node20-dev
updated_atoptionalRFC3339 timestamp
erroroptionalDetail when state=failed

Existing markers (keep writing them):

PathRole
/var/lib/grain/userdata-ranCloud-init / userdata finished (Health.userdata_ran)
/var/lib/grain-readyLegacy ready stamp

State machine

text
(absent files) ── stock images: host treats as "no protocol"
   pending ──► running ──► ready
                  └──► failed

Custom bootstrap rule:

  1. On start of custom work: write state=running, set phase / message.
  2. On each major step: update phase, message, optionally updated_at.
  3. On success: state=ready (optional ready_name=…).
  4. On failure: state=failed and error=….

Shell helper (no extra binary required)

bash
# grain-ready-report <state> [phase] [message]
grain-ready-report() {
  local state="$1" phase="${2:-}" message="${3:-}"
  local dir=/var/lib/grain/readiness
  mkdir -p "$dir"
  printf '%s\n' "$state" >"$dir/state"
  [ -n "$phase" ] && printf '%s\n' "$phase" >"$dir/phase" || rm -f "$dir/phase"
  [ -n "$message" ] && printf '%s\n' "$message" >"$dir/message" || rm -f "$dir/message"
  date -u +"%Y-%m-%dT%H:%M:%SZ" >"$dir/updated_at"
  if [ "$state" = "failed" ] && [ -n "$message" ]; then
    printf '%s\n' "$message" >"$dir/error"
  fi
}

grain-ready-report running packages "apt-get install …"
# … work …
grain-ready-report ready
# or: grain-ready-report failed "" "setup.sh exit 1"

cloud-init example

Append runcmd that drives readiness, and only stamp userdata-ran after success (or stamp userdata early and keep bootstrap in readiness/ until ready):

yaml
#cloud-config
runcmd:
  - |
    set -e
    dir=/var/lib/grain/readiness
    mkdir -p "$dir" /var/lib/grain
    echo running >"$dir/state"
    echo packages >"$dir/phase"
    echo "installing packages" >"$dir/message"
    export DEBIAN_FRONTEND=noninteractive
    apt-get update -qq
    apt-get install -y -qq git
    echo ready >"$dir/state"
    echo node20-dev >"$dir/ready_name"
    touch /var/lib/grain/userdata-ran    

Agent API

GET /health (and grain health / daemon proxy) includes an optional readiness object when files are present:

json
{
  "hostname": "sbox-1",
  "agent_version": "0.3.0",
  "agent_uptime_sec": 42,
  "userdata_ran": true,
  "readiness": {
    "state": "running",
    "phase": "packages",
    "message": "installing git",
    "ready_name": "node20-dev",
    "updated_at": "2026-07-29T12:00:01Z",
    "error": ""
  }
}

GET /readiness returns the same object alone (empty object if no protocol files).

Host behavior

Create phases

text
image → disk → seed → qemu → wait_ssh | wait_agent → [userdata] → [bootstrap] → ready | error
PhaseMeaning
userdataWaiting for userdata_ran
bootstrapWaiting for readiness state=ready (messages come from the guest)
readyCreate succeeded

Wait modes (grain new --wait, API wait=)

ModeBehavior
auto / ssh / agentUnchanged (no forced bootstrap wait)
userdataAgent + userdata_ran
bootstrapAgent, then poll readiness until ready, or fail on failed / timeout

Missing readiness/ files when wait=bootstrap: treated as pending until timeout (authors must stamp ready).

On state=failed or timeout: create returns an error; the VM is left running so you can grain logs, grain status, grain sh, or grain rm.

CLI

bash
grain new --wait bootstrap -n lab
grain health lab          # full JSON including readiness
grain status lab          # one-liner: status + agent + readiness

Example status line:

text
lab  status=running  image=grain-ubuntu  agent=up  userdata=ran  readiness=running  phase=packages  "installing git"

Stock images

Golden grain-ubuntu continues to stamp userdata-ran quickly and does not require readiness/ files. Use --wait agent (default for goldens) as today.

Use --wait bootstrap only when your image or userdata implements this protocol.

Relation to profiles, presets, and docs recipes

ConceptRole
ProfileNamed create defaults in config
PresetEmbedded cloud-init fragments (docker, k3s, act)
Docs “recipes”Human guides / one-shot CLIs (grain act)
Readiness protocolGuest contract for progress + done (this page)
Sandbox recipe filePortable YAML: grain new --recipe → create + bootstrap using this protocol

See also