Skip to content

rodeo-plan.yaml reference

rodeo-plan.yaml is the resource and deployment configuration for a lab. It answers: how big are the VMs, where are they stored, what are the credentials, and what deployment target is this running on?

It does not describe topology (nodes, network, services). That lives in definition.yaml.

Both files together form a profile — the complete description of a lab that rodeo up, plan, and deploy consume.


Full annotated example

# The pipeline this plan uses. Determines which phases run.
# suse-virt: Harvester HCI (with or without Rancher)
# rancher:   Rancher Prime on K3s only (no Harvester, no PXE)
type: suse-virt

# Used as the libvirt object prefix, state file name, and log label.
# Must be unique per host if you run multiple labs.
name: my-lab

# Where this lab runs. Controls whether the finalise phase is deferred,
# and (for aws) whether the laptop provisions an EC2 host then remote-deploys.
# baremetal: full deploy including VM autostart on host reboot
# instruqt:  skips finalise until you run `rodeo deploy --from finalise --finalise`
# aws:       laptop control plane — provision EC2, then remote `rodeo up --target baremetal`
deployment_target: baremetal

# Virtual machine resource allocation.
# These are the per-VM values. total host RAM = (harvester nodes × memory_mib) + rancher.memory_mib.
resources:
  harvester:
    memory_mib: 16384   # RAM per Harvester node in MiB (16 GiB)
    vcpu: 8             # vCPUs per Harvester node
    disk_gb: 320        # disk per Harvester node — do not go below ~250 (see note)

  rancher:
    memory_mib: 8192    # RAM for the Rancher VM in MiB (8 GiB)
    vcpu: 4             # vCPUs for Rancher
    disk_gb: 60         # disk for the Rancher VM

# Credentials. Values starting with ?? are resolved at deploy time — never hardcoded here.
#
# ?? resolution sources (in order of preference):
#   ??key            → ~/.rodeo/secrets.yaml key named "key"
#   ??env:VAR_NAME   → environment variable VAR_NAME
#   ??file:/path     → contents of a file
#   ??cmd:command    → stdout of a shell command (trimmed)
#
# rodeo up and rodeo init generate ~/.rodeo/secrets.yaml automatically.
# Deploy fails immediately if any ?? placeholder cannot be resolved.
credentials:
  harvester_os_password: "??harvester_os_password"     # OS login for Harvester nodes (rancher user)
  harvester_admin_password: "??harvester_admin_password" # Harvester web UI admin password
  rancher_admin_password: "??rancher_admin_password"   # Rancher web UI admin password
  harvester_token: "??harvester_token"                 # RKE2 cluster join token (internal)

# Network settings at the plan level. Most network config lives in definition.yaml.
# Override here only if your host uses a non-default VIP or gateway.
network:
  vip: 192.168.122.10     # Harvester cluster VIP (kube-vip floating IP)

# Storage configuration for the host.
storage:
  device: ""                          # "" = use the default single-disk path
                                      # "/dev/nvme1n1" = dedicate a second disk
  image_dir: /var/lib/libvirt/images  # where VM disks and ISOs are stored

# Software versions. Pin these to reproduce a specific lab.
versions:
  harvester: "1.8.1"          # Harvester ISO version to download and install
  rancher: "2.14.1"           # Rancher Prime Helm chart version
  k3s: "v1.35.3+k3s1"        # K3s version for the Rancher VM
  cert_manager: "v1.16.2"     # cert-manager Helm chart version (Rancher dependency)

# Jinja2 templating: define variables used in this file.
# Values can be overridden with -P or --paramfile at deploy time.
parameters:
  harvester_mem: 16384

# Then use them:
# resources:
#   harvester:
#     memory_mib: {{ harvester_mem }}

Field reference

type

Value Pipeline Phases
suse-virt Harvester HCI (with or without Rancher) kvm_hostvmspxe_serverclusterrancherfinalise
rancher Rancher Prime on K3s kvm_hostvmsbootrancherfinalise
suse-edge SUSE Edge 3.6 (Rancher + Elemental + EIB + edge nodes) in development on feature/suse-edge

Required. No default.

name

String. Used as the prefix for all libvirt objects (<name>-harvester1, etc.), the state file name (~/.rodeo/state/<name>.yaml), and log labels.

Must be unique per host. Changing name after deploy creates orphaned resources — run rodeo clean first.

deployment_target

Value Behaviour
baremetal Full deploy. finalise enables VM autostart on host reboot.
instruqt finalise is skipped automatically. Run it after the Instruqt snapshot: rodeo deploy --from finalise --finalise
aws Laptop control plane: require a provider: block, provision/reuse one EC2 KVM host, wait for SSH, remote-run rodeo up on that host. Tear down with rodeo destroy --cloud --yes. BYO: create the instance yourself, keep deployment_target: aws (or set storage.backend: nvme) and deploy on the box.

Required. Defaults to baremetal when omitted. Plugins can add targets via host_context.register_host_context() (see the architecture doc's extension points) — a registered target is accepted here, by --target, and by the rodeo up prompt.

On instruqt, rodeo up / lab seeding also applies host-aware resources presets so Σ guest vCPU stays near ~70% of the builder (Harvester typically 6–8 vCPU / 20 GiB). Existing plans are not rewritten on re-deploy; only seeded plans get the presets. rodeo doctor / rodeo deploy --check warn (non-fatal) when a plan still exceeds the budget.

On aws, apply_host_context() (seed + deploy) raises resources.harvester.disk_gb to a flat 500 GB per Harvester node and resources.rancher.disk_gb to 60 GB (never scaled by node count — the rest of the NVMe device is deliberately left free, matching real-world/Instruqt sizing). Also sets storage.backend: nvme and mounts the largest non-root NVMe on image_dir. Prefer i7i.8xlarge for the generic 3-node harvester profile (needs its 256 GiB RAM, BYO/non-AWS-tuned); the AWS-specific harvester-2n and harvester-aws profiles both use m8id.8xlarge (32 vCPU / 128 GiB / a single ~1.9 TiB NVMe device — unlike i7i.8xlarge, which splits its NVMe across two ~3.4 TiB devices and rodeo only mounts one). Nested virt is enabled by default on non-metal types.

provider (when deployment_target: aws)

Same shape as Fleet workshop.yaml provider. Required fields for AWS: type, region, subnet_id, and either instance_type or instance_tier.

Security group is auto-managed unless you pin one. Omit security_group_ids and rodeo creates (or reuses) an SG named rodeo-<name> in the subnet's VPC, opens exactly the ports the host needs — SSH (22), Harvester UI (8443), Rancher NodePort (30002) — and scopes all three to this machine's current public IP, detected automatically. Re-running from a different IP (new wifi, VPN toggled) updates the rule in place rather than piling up stale ones. rodeo destroy --cloud --yes deletes it once nothing else tagged for the workshop is still running (best-effort: if the instance hasn't finished detaching yet, re-run destroy — it's never left as a hard failure). Set security_group_ids explicitly to opt back into a hand-managed SG — useful for a shared multi-attendee IP range, a bastion topology, or an SG your org's security policy already owns.

Instance size (single-host v1): pick one of three tiers for the lab profile, or set an explicit type. rodeo up --target aws prompts interactively when neither is set; with --yes it uses recommended.

Tier Meaning
budget Meets the profile minimum (often EBS-backed Nitro)
recommended Preferred size (usually i7i.* local NVMe for Harvester/Edge)
performance Metal / larger escape hatch

Before create, rodeo checks the type is offered in the region and probes capacity via RunInstances DryRun. If the region has no capacity, it fails with a message to try another region or tier — it does not silently downsize.

deployment_target: aws
provider:
  type: aws
  region: eu-central-1
  # Either:
  instance_tier: recommended          # budget | recommended | performance
  # Or pin explicitly:
  # instance_type: i7i.8xlarge
  # ami omitted → newest suse-sles-16-0-v<date>-hvm-ssd-x86_64 (SLES 16 PAYG)
  # ami: ami-…                        # optional pin
  # ami_name_filter: "suse-sles-16-0-v????????-hvm-ssd-x86_64"
  subnet_id: subnet-…
  # security_group_ids: [sg-…]        # omit → rodeo creates/manages one,
  #                                    # scoped to this machine's public IP
  ssh_user: ec2-user                  # SLES 16 / Leap default
  # nested_virtualization: true       # default on for non-metal
  # volume_size_gib: 100              # root EBS; lab disks use NVMe
  # ref: main                         # rodeo-cli git ref to run on the host
  # install_url: https://…/install.sh # fork or air-gapped mirror
rodeo up --yes --profile harvester --target aws --instance-tier recommended

Which rodeo-cli the host runs (--ref / provider.ref). The instance bootstraps itself with install.sh from GitHub — your local working tree never reaches it. By default the bootstrap runs only when rodeo is absent, so a host stays on the code it was first installed with, the same way clean --refresh refuses to move a pinned host's version unasked.

Pass a ref to change that:

rodeo up --target aws --ref main         # pick up commits pushed since bootstrap
rodeo up --target aws --ref v0.15.0      # pin a release
rodeo up --target aws --ref feat/my-fix  # test a branch on a real host

A ref makes the bootstrap run every time and hard-resets the host's checkout to it (install.sh --ref), which is the only way a just-pushed commit reaches an existing host. The installer itself is fetched from the same ref, so install.sh and the code it installs cannot disagree. An explicit provider.install_url is used verbatim — for a fork or an air-gapped mirror — and the ref is still passed to it. --ref beats provider.ref; an invalid ref is rejected before any instance is launched, so a typo costs nothing. --ref applies only when the laptop is the AWS control plane; anywhere else it warns and is ignored. Fleet has the same mechanism — fleet deploy --ref / lab.ref.

AWS API credentials (boto3 — never in the plan): ~/.aws/credentials / AWS_PROFILE, or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional AWS_SESSION_TOKEN). AWS CLI is optional.

AMI choice matters more than it looks. The default is SLES 16 pay-as-you-go (published by SUSE via Amazon), and it is not interchangeable with the alternatives:

  • openSUSE Leap 16 is a Marketplace product whose listing does not permit 7th-generation Intel instance types. Nested virtualisation needs exactly those, so Leap cannot run a nested lab on anything but *.metal.
  • SLES BYOS images allow i7i, but ship without a subscription, so zypper has no repos and install-deps fails. rodeo does not register hosts.

PAYG avoids both traps at a licence surcharge of roughly $0.125/hr on i7i.8xlarge. No Marketplace subscription or opt-in is needed.

SSH / root: new instances get cloud-init UserData that installs the managed pubkey for root and passwordless sudo for ssh_user (ec2-user on SLES 16). Remote rodeo up runs under sudo -n so it never prompts. rodeo ssh primary or rodeo ssh primary/rancher use the same managed key.

resources

Per-VM sizing. All fields are per-node (not total).

Field Unit Minimum Notes
memory_mib MiB 12288 (12 GiB) for Harvester Harvester requires at least 12 GiB; 16 GiB recommended
vcpu threads 4 On hyper-threaded hosts, each thread counts
disk_gb GiB 320 for Harvester Elemental's persistent partition is a fixed 150 GiB floor regardless of disk size; below ~250 it starves and containerd fails; the rest goes to Longhorn's own partition, so bigger leaves Longhorn more room

The rancher block is only used when the topology includes a Rancher VM (see definition.yaml).

credentials

All credential values must use ?? placeholders. Plain text passwords in plan files are rejected by validate_config().

Key Used for
harvester_os_password OS login on Harvester nodes (rancher user SSH + console)
harvester_admin_password admin user in the Harvester web UI
rancher_admin_password admin user in the Rancher web UI
harvester_token RKE2 cluster join token — internal; do not share with attendees

rodeo up and rodeo init generate random values and write them to ~/.rodeo/secrets.yaml (chmod 600). Never commit that file.

network

Field Default Notes
vip 192.168.122.10 Harvester cluster VIP. Must be in the same /24 as nodes but outside the DHCP range.

Most network topology (CIDR, gateway, DNS domain, per-node IPs) is in definition.yaml.

storage

Field Default Notes
device "" Leave empty for single-disk hosts. Set to /dev/nvme1n1 (or similar) to dedicate a second disk. Confirm with lsblk.
image_dir /var/lib/libvirt/images Where libvirt stores VM disks and ISOs. Must have 600–900 GiB free for a full lab.

libvirt

Field Default Notes
uri qemu:///system libvirt connection URI.
disk_cache target-dependent Guest qcow2 cache= mode. Default none on baremetal, writeback when deployment_target: instruqt (better nested cloud I/O). Allowed: none, writethrough, writeback, unsafe, directsync.
disk_io target-dependent Guest qcow2 io= mode. Default native on baremetal, threads on Instruqt. Allowed: native, threads, io_uring.

Override at deploy time without editing the plan:

rodeo deploy -P libvirt.disk_cache=writeback -P libvirt.disk_io=threads

versions

Field Default Notes
harvester "1.8.1" ISO version string. rodeo downloads the matching ISO from the Harvester release page.
rancher "2.14.1" Rancher Prime Helm chart version.
k3s "v1.35.3+k3s1" K3s version installed on the Rancher VM (suse-virt / rancher). SUSE Edge defaults to v1.35.5+k3s1.
cert_manager "v1.16.2" cert-manager Helm chart (Rancher dependency).

parameters

Optional map of Jinja2 template variables. Use to avoid repeating values across the file. Override at deploy time with -P key=value or --paramfile overrides.yaml.


CLI overrides

You can override any plan value at deploy time without editing the file:

# Single value
rodeo deploy -P resources.harvester.memory_mib=20480

# Multiple values
rodeo deploy -P resources.harvester.vcpu=10 -P versions.harvester=1.5.0

# From a file (deep-merged over the plan)
rodeo deploy --paramfile big-lab.yaml

Precedence (lowest to highest): profile defaults → rodeo-plan.yaml--paramfile-P


story — workshop narrative, languages, and variants

rodeo story render produces the participant hand-out for this lab from rmstory-tagged markdown in the lab's story/ directory:

<lab>/story/*.md       tagged markdown sources (authored in English)
<lab>/story/stories/   story-variant indexes, one <id>.yaml per variant
<lab>/story/strings/   translation store (rmstory filesystem backend)

The optional story: block sets the defaults (CLI flags override):

story:
  language: es          # target language (default en = source, no translation)
  id: villain-arc       # story variant to assemble (default: all spans)
  engine: gemini        # rmstory engine to machine-fill missing translations
  engine_env:           # engine credentials — ?? secrets are resolved
    GEMINI_API_KEY: "??gemini_api_key"

Deployment facts are substituted into the rendered text as Jinja expressions — write them inside invariant spans so translation never touches them: <span no>{{ rancher_url }}</span>. Available facts: name, type, language, vip, harvester_url, rancher_ip, rancher_url, rancher_nodeport, dns_domain, gateway, vms, vm_names, credentials.

Rendering in the source language with no variant needs nothing installed; translation and variant assembly use the rmstory system package (sudo rodeo install-deps --story — distro packages, never PyPI).

The post-deploy success screen uses the same machinery: each profile ships a tagged success.md, a lab can override it with story/success.md, and story.language localizes it. Translation problems always degrade to the English source — the payoff screen never fails.


lab_in_a_box — exporting to lab-in-a-box

rodeo export --format lab-in-a-box renders the lab as the lab.json that lab-in-a-box's setup_lab.sh / destroy_lab.sh consume (tested against release 1.0.0). The plan and definition stay the source of truth; the optional lab_in_a_box: block holds the knobs that only exist on the lab-in-a-box side:

lab_in_a_box:
  iso_image: openSUSE-Leap-15.6.qcow2   # base qcow2 in lab-in-a-box's ISO_LOC (required to deploy)
  config_method: cloud-init             # cloud-init (default) | iso-cloud-init | "" (ignition/combustion)
  cluster_name: mgmt                    # kcluster name (also its DNS record: <name>.<domain>)
  cluster_type: k3s                     # k3s (default) | rke2
  clu_rel: stable                       # install channel — exact version pins don't carry over
  addons: [rancher]                     # override the derived install_<addon> list
  sections:                             # verbatim extra/override lab.json sections
    rancher: {rancher_rel: stable}

Not carried over (warned at export time): PXE-booted Harvester nodes (lab-in-a-box has no PXE — use --skip-unsupported to export the rest), exposed-service host port-forwards, storage/image-dir selection, and exact k3s/rke2 version pins.


What belongs here vs. definition.yaml

Put it in rodeo-plan.yaml Put it in definition.yaml
VM resource sizing (RAM, CPU, disk) Node names, IPs, MACs
Credentials and secret references Network CIDR, gateway, DNS domain
Deployment target (baremetal/instruqt/aws) Start order and etcd join gap
Software versions Exposed services and port mapping
Storage device path Node templates (interface roles)
Jinja2 parameters Host prep requirements (sysctls, SELinux)

The rule of thumb: the plan is how big and where. The definition is what.