| name | cloud-init-renderer |
| description | Pure renderer from VmSpec + VmCloudInit to NoCloud seed ISO (user-data +
meta-data + network-config). Covers composeUsers adopt-merge, SMBIOS vs
cloud_init additive channels, xorriso ISO emission, and charly_install.strategy
state machine. Source: charly/cloud_init_render.go, charly/cloud_init_iso.go, charly/charly_install.go.
MUST be invoked before editing cloud-init emission paths.
|
cloud-init-renderer
Host-side renderer producing NoCloud seed ISOs for cloud_image VMs (and bootc VMs that include the cloud-init layer). Pure transformation — given VmSpec + VmCloudInit, produces three files (user-data, meta-data, network-config) and packages them into a FAT-labeled cidata ISO via xorriso.
Lives host-side, in the charly binary. The guest-side /charly-distros:cloud-init layer is complementary: it puts the cloud-init package into the bootc guest OS so that guest reads the seed ISO. The two sides cooperate across the host/guest boundary.
Source files
| File | Contents |
|---|
charly/cloud_init_render.go | RenderCloudInit, ResolveKeyInjectionChannels, composeUsers, composePackages, composeRunCmd |
charly/cloud_init_iso.go | WriteSeedISO via xorriso; genisoimage + mkisofs fallbacks |
charly/charly_install.go | EnsureCharlyInGuest state machine (auto/scp/url/skip strategies) |
charly/spec/cue_types_gen.go (generated) | VmCloudInit, VmCloudInitUser, VmCloudInitFile, VmCloudInitNetwork, VmCloudInitMirrors, VmCharlyInstall |
RenderCloudInit top-level
func RenderCloudInit(spec *VmSpec, rt CloudInitRuntimeParams) (userData, metaData, networkConfig string, err error)
Returns three strings — the three files NoCloud expects on the seed ISO. The caller (BuildCloudImage or the vm deploy preflight) then calls WriteSeedISO to pack them.
Egress validation gate. Before returning, each rendered document is validated
against a CUE schema — user-data against the vendored Canonical cloud-config
schema (#CloudConfig), meta-data against #CloudInitMeta, network-config against
#NetworkConfigV2 (via ValidateEgress). A malformed render fails here, so a
cloud-init that its own schema would reject never reaches the seed ISO. The gate
is owned by /charly-internals:egress.
composeUsers (adopt-merge pattern)
Single most important function in this renderer. Produces the users: list in user-data.
When spec.Source.BaseUser is non-empty (cloud_image adopt pattern):
users:
- default
- name: <base_user>
ssh_authorized_keys:
- <pubkey>
No useradd, no sudoers write, no shell change — cloud-init interprets "name: X" on an existing account as "append ssh_authorized_keys". The parity is exact with the container-side base_user: + user_policy: adopt pattern (/charly-image:image "user_policy").
When BaseUser is empty and VmSSH.User is non-empty (create pattern):
users:
- default
- name: <ssh_user>
sudo: ALL=(ALL) NOPASSWD:ALL
groups: [wheel, sudo]
shell: /bin/bash
lock_passwd: true
ssh_authorized_keys:
- <pubkey>
Full account provisioning. Used by bootc VMs where no base_user: applies.
When a user already appears in spec.CloudInit.Users: the renderer appends the ssh pubkey to that existing entry instead of emitting a new one. Lets authors declare a user with specific sudo/groups/shell fields and still get the pubkey injected.
ResolveKeyInjectionChannels
Applies the per-source-kind auto-defaults documented in /charly-internals:vm-spec:
func ResolveKeyInjectionChannels(spec *VmSpec) (smbios bool, cloudInit bool) {
if spec.SSH != nil && spec.SSH.KeyInjection != nil {
return spec.SSH.KeyInjection.SMBIOS == "enabled" (with "auto" = source-kind default),
spec.SSH.KeyInjection.CloudInit == "enabled" (with "auto" = source-kind default)
}
switch spec.Source.Kind {
case "cloud_image":
return true, true
case "bootc":
return true, false
}
}
Both channels are additive — when both are on, systemd-ssh-generator (SMBIOS path) and cloud-init (user-data path) both inject the key. Dedup happens in the guest's authorized_keys. There's no correctness issue with duplicate keys; the dual-injection pattern is the safe default.
composePackages + composeRunCmd renderer defaults
The renderer prepends defaults to user-declared lists:
composePackages: prepends {openssh, curl, tar} (deduplicated against user's Packages). Guarantees the guest has SSH server + download tools + tar for later layer application.
composeRunCmd: prepends {systemctl enable --now sshd}. Distro-specific user runcmd: entries can assume sshd is running.
User-supplied fields extend defaults; they don't replace them. Prevents the common footgun where an author puts packages: [nginx] and accidentally breaks SSH because they overrode the default list.
WriteSeedISO
func WriteSeedISO(userData, metaData, networkConfig string, outputPath string) error
Writes a FAT-labeled cidata ISO. Tool preference order:
xorriso (preferred — modern, scriptable).
genisoimage (legacy but widely available).
mkisofs (oldest fallback).
Clean error when none are present, with distro-appropriate install recipe (dnf install xorriso, pacman -S libisoburn, apt-get install xorriso).
The ISO is mounted by QEMU as a CD-ROM; cloud-init's NoCloud datasource reads /dev/sr0 at first boot.
EnsureCharlyInGuest (charly_install.strategy state machine)
Runs post-boot inside the vm deploy preflight (the vmSubstrateLifecycle hook's PrepareVenue, charly/vm_deploy_lifecycle.go) after cloud-init completes, BEFORE the plugin walks the plans. Dispatches on spec.CloudInit.CharlyInstall.Strategy:
| Strategy | Action |
|---|
auto / scp | scp $(os.Executable()) guest:/usr/local/bin/charly; chmod +x |
url | ssh-execute curl -L <url> -o /usr/local/bin/charly && sha256sum -c (verified against VmCharlyInstall.Checksum) |
skip | ssh 'which charly' — fails if missing, returns early if present |
Idempotent. If charly is already present at the target version, the function returns without re-scp'ing. See /charly-internals:vm-deploy-target for how this plugs into the overall deploy flow.
Network config
VmCloudInitNetwork.Ethernets passes through to cloud-init's network-config v2 as-is. When unset, the renderer emits an empty network-config (cloud-init defaults to DHCP on every virtio-net interface). Good default; override only for static-IP deployments.
SMBIOS vs cloud_init can coexist
Explicitly supported — not either/or. VmKeyInjection.SMBIOS: enabled + VmKeyInjection.CloudInit: enabled simultaneously is the default for cloud_image VMs. Rationale: belt-and-suspenders (SMBIOS via systemd-ssh-generator v250+, cloud-init via user-data — both paths exist on modern Linux). No duplication cost.
Cross-References
/charly-internals:vm-spec — VmCloudInit, VmSSH.KeyInjection, VmCharlyInstall types
/charly-internals:libvirt-renderer — SMBIOS-channel emission (domain XML side)
/charly-internals:vm-deploy-target — EnsureCharlyInGuest caller; SSH/cloud-init readiness waits
/charly-vm:vm — command-family; cloud-init flow
/charly-vm:vms-catalog — YAML-authoring reference
/charly-vm:arch — charly_install.strategy: auto worked example; adopt-user pattern
/charly-distros:cloud-init — guest-side pairing: the cloud-init package installed inside bootc images reads the seed ISO this renderer produces