summaryrefslogtreecommitdiffstats
path: root/classes/xen-guest-bundle.bbclass
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-02-12 18:30:30 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-26 01:05:01 +0000
commitac150976f5c892d3c4c919dcc2e0b15bc3761e02 (patch)
treee14bf509c268cd9edf762a22ea3f0b0f837bd6e3 /classes/xen-guest-bundle.bbclass
parentf7022507859f519f87cbca5dcff437e9dc676ff0 (diff)
downloadmeta-virtualization-ac150976f5c892d3c4c919dcc2e0b15bc3761e02.tar.gz
xen-guest-bundle: add bbclass for packaging Xen guest bundles
New bbclass that creates installable packages bundling Xen guest images (rootfs + kernel + config). When installed via IMAGE_INSTALL into a Dom0 image that inherits xen-guest-cross-install, guests are automatically deployed by merge_installed_xen_bundles(). Features: - Parse-time dependency generation from XEN_GUEST_BUNDLES - Per-guest varflags for memory, vcpus, vif, extra, disk, name - Custom config file support via XEN_GUEST_CONFIG_FILE varflag - Explicit rootfs/kernel path overrides for external guests - Manifest-based packaging for cross-install integration Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes/xen-guest-bundle.bbclass')
-rw-r--r--classes/xen-guest-bundle.bbclass405
1 files changed, 405 insertions, 0 deletions
diff --git a/classes/xen-guest-bundle.bbclass b/classes/xen-guest-bundle.bbclass
new file mode 100644
index 00000000..0e061bfb
--- /dev/null
+++ b/classes/xen-guest-bundle.bbclass
@@ -0,0 +1,405 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# xen-guest-bundle.bbclass
6# ===========================================================================
7# Xen guest bundling class for creating installable guest packages
8# ===========================================================================
9#
10# This class creates packages that bundle Xen guest images (rootfs + kernel +
11# config). When these packages are installed via IMAGE_INSTALL into a Dom0
12# image that inherits xen-guest-cross-install, the guests are automatically
13# merged into the target image by merge_installed_xen_bundles().
14#
15# ===========================================================================
16# Component Relationships
17# ===========================================================================
18#
19# To bundle a guest like "xen-guest-image-minimal:autostart", two recipe
20# types work together:
21#
22# 1. Guest Image Recipe (creates the guest rootfs)
23# recipes-extended/images/xen-guest-image-minimal.bb
24# +-- inherit core-image
25# +-- Produces: ${DEPLOY_DIR_IMAGE}/xen-guest-image-minimal-${MACHINE}.ext4
26#
27# 2. Bundle Recipe (packages guest images for deployment)
28# recipes-extended/xen-guest-bundles/my-bundle_1.0.bb
29# +-- inherit xen-guest-bundle
30# +-- XEN_GUEST_BUNDLES = "xen-guest-image-minimal:autostart"
31# +-- Creates installable package with rootfs, kernel, and config
32#
33# Flow diagram:
34#
35# xen-guest-image-minimal.bb
36# (guest image recipe)
37# |
38# | do_image_complete
39# v
40# ${DEPLOY_DIR_IMAGE}/xen-guest-image-minimal-${MACHINE}.ext4
41# |
42# | XEN_GUEST_BUNDLES="xen-guest-image-minimal"
43# v
44# my-bundle_1.0.bb --------> my-bundle package
45# (inherits xen-guest-bundle) |
46# | IMAGE_INSTALL="my-bundle"
47# v
48# xen-image-minimal
49# (Dom0 host image)
50#
51# ===========================================================================
52# When to Use This Class vs BUNDLED_XEN_GUESTS
53# ===========================================================================
54#
55# There are two ways to bundle Xen guests into a host image:
56#
57# 1. BUNDLED_XEN_GUESTS variable (simpler, no extra recipe needed)
58# Set in local.conf or image recipe:
59# BUNDLED_XEN_GUESTS = "xen-guest-image-minimal:autostart"
60#
61# 2. xen-guest-bundle packages (this class)
62# Create a bundle recipe, install via IMAGE_INSTALL
63#
64# Decision guide:
65#
66# Use Case | BUNDLED_XEN_GUESTS | Bundle Recipe
67# --------------------------------------------|--------------------|--------------
68# Simple: guests in one host image | recommended | overkill
69# Reuse guests across multiple host images | repetitive | recommended
70# Package versioning and dependencies | not supported | supported
71# Distribute pre-built guest sets | not supported | supported
72#
73# For most single-image use cases, BUNDLED_XEN_GUESTS is simpler.
74#
75# ===========================================================================
76# Usage
77# ===========================================================================
78#
79# inherit xen-guest-bundle
80#
81# XEN_GUEST_BUNDLES = "\
82# xen-guest-image-minimal:autostart \
83# my-other-guest \
84# "
85#
86# Variable format: recipe-name[:autostart][:external]
87# - recipe-name: Yocto image recipe name that produces the guest rootfs
88# - autostart: Optional. Creates symlink in /etc/xen/auto/ for xendomains
89# - external: Optional. Skip dependency generation (3rd-party guest)
90#
91# Per-guest configuration via varflags (same interface as cross-install):
92# XEN_GUEST_MEMORY[guest-name] = "1024"
93# XEN_GUEST_VCPUS[guest-name] = "2"
94# XEN_GUEST_VIF[guest-name] = "bridge=xenbr0"
95# XEN_GUEST_EXTRA[guest-name] = "root=/dev/xvda ro console=hvc0 ip=dhcp"
96# XEN_GUEST_DISK_DEVICE[guest-name] = "xvda"
97# XEN_GUEST_NAME[guest-name] = "my-custom-name"
98#
99# Custom config file (replaces auto-generation entirely):
100# SRC_URI += "file://custom.cfg"
101# XEN_GUEST_CONFIG_FILE[guest-name] = "${UNPACKDIR}/custom.cfg"
102#
103# Explicit rootfs/kernel paths (for external/3rd-party guests):
104# XEN_GUEST_ROOTFS[my-vendor-guest] = "vendor-rootfs.ext4"
105# XEN_GUEST_KERNEL[my-vendor-guest] = "vendor-kernel"
106#
107# ===========================================================================
108# Integration with xen-guest-cross-install.bbclass
109# ===========================================================================
110#
111# This class creates packages that are processed by xen-guest-cross-install:
112# 1. Installs guest files to ${datadir}/xen-guest-bundles/${PN}/
113# 2. merge_installed_xen_bundles() copies them to final locations at image time
114# 3. Bundle files are removed from the final image after merge
115#
116# See also: xen-guest-cross-install.bbclass
117
118XEN_GUEST_BUNDLES ?= ""
119XEN_GUEST_IMAGE_FSTYPE ?= "ext4"
120XEN_GUEST_MEMORY_DEFAULT ?= "512"
121XEN_GUEST_VCPUS_DEFAULT ?= "1"
122XEN_GUEST_VIF_DEFAULT ?= "bridge=xenbr0"
123XEN_GUEST_EXTRA_DEFAULT ?= "root=/dev/xvda ro ip=dhcp"
124XEN_GUEST_DISK_DEVICE_DEFAULT ?= "xvda"
125
126# ===========================================================================
127# Parse-time dependency generation
128# ===========================================================================
129
130python __anonymous() {
131 bundles = (d.getVar('XEN_GUEST_BUNDLES') or "").split()
132 if not bundles:
133 return
134
135 processed = []
136 deps = ""
137
138 for entry in bundles:
139 parts = entry.split(':')
140 guest_name = parts[0]
141
142 is_external = 'external' in parts
143 is_autostart = 'autostart' in parts
144
145 # Generate dependency on guest recipe (unless external)
146 if not is_external:
147 deps += " %s:do_image_complete" % guest_name
148
149 # Store processed entry: guest_name:autostart_flag
150 autostart_flag = "autostart" if is_autostart else ""
151 processed.append("%s:%s" % (guest_name, autostart_flag))
152
153 if deps:
154 d.appendVarFlag('do_compile', 'depends', deps)
155
156 d.setVar('_PROCESSED_XEN_BUNDLES', ' '.join(processed))
157
158 # Build config file map from varflags
159 # Format: guest1=/path/to/file1;guest2=/path/to/file2
160 config_mappings = []
161 for entry in bundles:
162 guest_name = entry.split(':')[0]
163 custom_file = d.getVarFlag('XEN_GUEST_CONFIG_FILE', guest_name)
164 if custom_file:
165 config_mappings.append("%s=%s" % (guest_name, custom_file))
166 d.setVar('_XEN_GUEST_CONFIG_FILE_MAP', ';'.join(config_mappings))
167
168 # Build params map from varflags
169 # Format: guest1=memory|vcpus|vif|extra|disk_device|name|rootfs|kernel;guest2=...
170 mem_default = d.getVar('XEN_GUEST_MEMORY_DEFAULT')
171 vcpus_default = d.getVar('XEN_GUEST_VCPUS_DEFAULT')
172 vif_default = d.getVar('XEN_GUEST_VIF_DEFAULT')
173 extra_default = d.getVar('XEN_GUEST_EXTRA_DEFAULT')
174 disk_default = d.getVar('XEN_GUEST_DISK_DEVICE_DEFAULT')
175
176 param_mappings = []
177 for entry in bundles:
178 guest_name = entry.split(':')[0]
179
180 memory = d.getVarFlag('XEN_GUEST_MEMORY', guest_name) or mem_default
181 vcpus = d.getVarFlag('XEN_GUEST_VCPUS', guest_name) or vcpus_default
182 vif = d.getVarFlag('XEN_GUEST_VIF', guest_name) or vif_default
183 extra = d.getVarFlag('XEN_GUEST_EXTRA', guest_name) or extra_default
184 disk_device = d.getVarFlag('XEN_GUEST_DISK_DEVICE', guest_name) or disk_default
185 name = d.getVarFlag('XEN_GUEST_NAME', guest_name) or guest_name
186 rootfs = d.getVarFlag('XEN_GUEST_ROOTFS', guest_name) or ""
187 kernel = d.getVarFlag('XEN_GUEST_KERNEL', guest_name) or ""
188
189 params = "|".join([memory, vcpus, vif, extra, disk_device, name, rootfs, kernel])
190 param_mappings.append("%s=%s" % (guest_name, params))
191
192 d.setVar('_XEN_GUEST_PARAMS_MAP', ';'.join(param_mappings))
193}
194
195S = "${UNPACKDIR}/sources"
196B = "${WORKDIR}/build"
197
198do_patch[noexec] = "1"
199do_configure[noexec] = "1"
200
201# ===========================================================================
202# do_compile: resolve guests and generate configs
203# ===========================================================================
204
205do_compile() {
206 set -e
207
208 mkdir -p "${S}"
209 rm -rf "${B}/images" "${B}/configs"
210 mkdir -p "${B}/images"
211 mkdir -p "${B}/configs"
212
213 # Clear manifest
214 : > "${B}/manifest"
215
216 if [ -z "${_PROCESSED_XEN_BUNDLES}" ]; then
217 bbnote "No Xen guest bundles to process"
218 return 0
219 fi
220
221 bbnote "Processing Xen guest bundles: ${_PROCESSED_XEN_BUNDLES}"
222
223 for bundle in ${_PROCESSED_XEN_BUNDLES}; do
224 guest_name=$(echo "$bundle" | cut -d: -f1)
225 autostart_flag=$(echo "$bundle" | cut -d: -f2)
226
227 bbnote "Processing guest: $guest_name (autostart=$autostart_flag)"
228
229 # Resolve rootfs
230 rootfs_path=$(resolve_bundle_rootfs "$guest_name")
231 if [ -z "$rootfs_path" ]; then
232 bbfatal "Cannot resolve rootfs for guest '$guest_name'"
233 fi
234 rootfs_basename=$(basename "$rootfs_path")
235
236 # Resolve kernel
237 kernel_path=$(resolve_bundle_kernel "$guest_name")
238 if [ -z "$kernel_path" ]; then
239 bbfatal "Cannot resolve kernel for guest '$guest_name'"
240 fi
241 kernel_basename=$(basename "$kernel_path")
242
243 # Copy to build dir (readlink -f resolves versioned symlinks)
244 cp "$(readlink -f "$rootfs_path")" "${B}/images/${rootfs_basename}"
245 cp "$(readlink -f "$kernel_path")" "${B}/images/${kernel_basename}"
246
247 # Generate or install config
248 config_map="${_XEN_GUEST_CONFIG_FILE_MAP}"
249 custom_config=$(echo "$config_map" | tr ';' '\n' | grep "^${guest_name}=" | cut -d= -f2-)
250
251 if [ -n "$custom_config" ] && [ -f "$custom_config" ]; then
252 bbnote "Installing custom config: $custom_config"
253 sed -E \
254 -e "s#^(disk = \[)[^,]+#\1'file:/var/lib/xen/images/$rootfs_basename#" \
255 -e "s#^(kernel = )\"[^\"]+\"#\1\"/var/lib/xen/images/$kernel_basename\"#" \
256 "$custom_config" > "${B}/configs/${guest_name}.cfg"
257 else
258 bbnote "Generating config for $guest_name"
259 generate_bundle_config "$guest_name" "$rootfs_basename" "$kernel_basename" \
260 "${B}/configs/${guest_name}.cfg"
261 fi
262
263 # Write manifest entry: guest_name:rootfs_file:kernel_file:autostart_flag
264 echo "${guest_name}:${rootfs_basename}:${kernel_basename}:${autostart_flag}" >> "${B}/manifest"
265
266 bbnote "Guest '$guest_name' compiled successfully"
267 done
268}
269
270# Resolve guest rootfs path from DEPLOY_DIR_IMAGE
271resolve_bundle_rootfs() {
272 local guest="$1"
273 local params_map="${_XEN_GUEST_PARAMS_MAP}"
274
275 # Check for explicit rootfs override (field 6)
276 local guest_params=$(echo "$params_map" | tr ';' '\n' | grep "^${guest}=" | cut -d= -f2-)
277 local override=""
278 if [ -n "$guest_params" ]; then
279 override=$(echo "$guest_params" | cut -d'|' -f7)
280 fi
281
282 if [ -n "$override" ]; then
283 local path="${DEPLOY_DIR_IMAGE}/$override"
284 if [ -e "$path" ]; then
285 echo "$path"
286 return 0
287 fi
288 bbwarn "XEN_GUEST_ROOTFS override '$override' not found at $path"
289 return 1
290 fi
291
292 # Standard Yocto naming: <recipe>-<MACHINE>.<fstype>
293 local path="${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.${XEN_GUEST_IMAGE_FSTYPE}"
294 if [ -e "$path" ]; then
295 echo "$path"
296 return 0
297 fi
298
299 # Fallback: <recipe>-<MACHINE>.rootfs.<fstype>
300 path="${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.rootfs.${XEN_GUEST_IMAGE_FSTYPE}"
301 if [ -e "$path" ]; then
302 echo "$path"
303 return 0
304 fi
305
306 bbwarn "Guest rootfs not found for '$guest'. Searched:"
307 bbwarn " ${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.${XEN_GUEST_IMAGE_FSTYPE}"
308 bbwarn " ${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.rootfs.${XEN_GUEST_IMAGE_FSTYPE}"
309 return 1
310}
311
312# Resolve guest kernel path from DEPLOY_DIR_IMAGE
313resolve_bundle_kernel() {
314 local guest="$1"
315 local params_map="${_XEN_GUEST_PARAMS_MAP}"
316
317 # Check for explicit kernel override (field 7)
318 local guest_params=$(echo "$params_map" | tr ';' '\n' | grep "^${guest}=" | cut -d= -f2-)
319 local override=""
320 if [ -n "$guest_params" ]; then
321 override=$(echo "$guest_params" | cut -d'|' -f8)
322 fi
323
324 if [ -n "$override" ]; then
325 local path="${DEPLOY_DIR_IMAGE}/$override"
326 if [ -e "$path" ]; then
327 echo "$path"
328 return 0
329 fi
330 bbwarn "XEN_GUEST_KERNEL override '$override' not found at $path"
331 return 1
332 fi
333
334 # Default: shared kernel (same MACHINE)
335 local path="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE}"
336 if [ -e "$path" ]; then
337 echo "$path"
338 return 0
339 fi
340
341 bbwarn "Guest kernel not found at ${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE}"
342 return 1
343}
344
345# Generate a Xen guest configuration file with final target paths
346generate_bundle_config() {
347 local guest="$1"
348 local rootfs_basename="$2"
349 local kernel_basename="$3"
350 local outfile="$4"
351 local params_map="${_XEN_GUEST_PARAMS_MAP}"
352
353 # Extract params
354 local guest_params=$(echo "$params_map" | tr ';' '\n' | grep "^${guest}=" | cut -d= -f2-)
355
356 local memory=$(echo "$guest_params" | cut -d'|' -f1)
357 local vcpus=$(echo "$guest_params" | cut -d'|' -f2)
358 local vif=$(echo "$guest_params" | cut -d'|' -f3)
359 local extra=$(echo "$guest_params" | cut -d'|' -f4)
360 local disk_device=$(echo "$guest_params" | cut -d'|' -f5)
361 local name=$(echo "$guest_params" | cut -d'|' -f6)
362
363 cat > "$outfile" << EOF
364name = "$name"
365memory = $memory
366vcpus = $vcpus
367disk = ['file:/var/lib/xen/images/$rootfs_basename,$disk_device,rw']
368vif = ['$vif']
369kernel = "/var/lib/xen/images/$kernel_basename"
370extra = "$extra"
371EOF
372}
373
374# ===========================================================================
375# do_install: package for merge_installed_xen_bundles
376# ===========================================================================
377
378do_install() {
379 if [ ! -f "${B}/manifest" ] || [ ! -s "${B}/manifest" ]; then
380 bbnote "No guests to install"
381 return 0
382 fi
383
384 install -d ${D}${datadir}/xen-guest-bundles/${PN}/images
385 install -d ${D}${datadir}/xen-guest-bundles/${PN}/configs
386
387 # Install guest images
388 if [ -d "${B}/images" ] && [ -n "$(ls -A ${B}/images 2>/dev/null)" ]; then
389 cp ${B}/images/* ${D}${datadir}/xen-guest-bundles/${PN}/images/
390 fi
391
392 # Install guest configs
393 if [ -d "${B}/configs" ] && [ -n "$(ls -A ${B}/configs 2>/dev/null)" ]; then
394 cp ${B}/configs/* ${D}${datadir}/xen-guest-bundles/${PN}/configs/
395 fi
396
397 # Install manifest
398 install -m 0644 ${B}/manifest ${D}${datadir}/xen-guest-bundles/${PN}/manifest
399}
400
401FILES:${PN} = "${datadir}/xen-guest-bundles"
402
403# Guest rootfs images are binary filesystem images that contain build paths
404# internally (normal for ext4/etc images) and can be large
405INSANE_SKIP:${PN} += "installed-vs-shipped buildpaths"