summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--classes/xen-guest-cross-install.bbclass437
-rw-r--r--recipes-extended/images/xen-guest-image-minimal.bb26
-rw-r--r--recipes-extended/xen-guest-bundles/example-xen-guest-bundle_1.0.bb54
3 files changed, 492 insertions, 25 deletions
diff --git a/classes/xen-guest-cross-install.bbclass b/classes/xen-guest-cross-install.bbclass
new file mode 100644
index 00000000..f36182f8
--- /dev/null
+++ b/classes/xen-guest-cross-install.bbclass
@@ -0,0 +1,437 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# xen-guest-cross-install.bbclass
6# ===========================================================================
7# Xen guest bundling class for Dom0 images
8# ===========================================================================
9#
10# This class enables bundling Xen guest images into a Dom0 host image during
11# build time. It follows the same pattern as container-cross-install.bbclass:
12# variable-driven, parse-time dependency generation, and a structured
13# ROOTFS_POSTPROCESS_COMMAND.
14#
15# Usage (in image recipe):
16# inherit xen-guest-cross-install
17#
18# Configuration (in local.conf or image recipe):
19# BUNDLED_XEN_GUESTS = "xen-guest-image-minimal"
20# BUNDLED_XEN_GUESTS = "xen-guest-image-minimal:autostart"
21# BUNDLED_XEN_GUESTS = "xen-guest-image-minimal my-other-guest:autostart"
22# BUNDLED_XEN_GUESTS = "prebuilt-guest:external"
23#
24# Guest format: recipe-name[:autostart][:external]
25# - recipe-name: Yocto image recipe name that produces the guest rootfs
26# - autostart: Optional. Creates symlink in /etc/xen/auto/ for xendomains
27# - external: Optional. Skip dependency generation (3rd-party guest)
28#
29# Per-guest configuration via varflags:
30# XEN_GUEST_MEMORY[guest-name] = "1024"
31# XEN_GUEST_VCPUS[guest-name] = "2"
32# XEN_GUEST_VIF[guest-name] = "bridge=xenbr0"
33# XEN_GUEST_EXTRA[guest-name] = "root=/dev/xvda ro console=hvc0 ip=dhcp"
34# XEN_GUEST_DISK_DEVICE[guest-name] = "xvda"
35# XEN_GUEST_NAME[guest-name] = "my-custom-name"
36#
37# Custom config file (replaces auto-generation entirely):
38# XEN_GUEST_CONFIG_FILE[guest-name] = "${UNPACKDIR}/custom.cfg"
39#
40# Explicit rootfs/kernel paths (for external/3rd-party guests):
41# XEN_GUEST_ROOTFS[my-vendor-guest] = "vendor-rootfs.ext4"
42# XEN_GUEST_KERNEL[my-vendor-guest] = "vendor-kernel"
43#
44# Artifacts installed on target:
45# /var/lib/xen/images/ - guest rootfs and kernel files
46# /etc/xen/<guest>.cfg - Xen guest configuration files
47# /etc/xen/auto/ - symlinks for autostart guests (xendomains)
48#
49# Guests can be launched after boot with:
50# xl create -c /etc/xen/<guest>.cfg
51#
52
53# ===========================================================================
54# Default variables
55# ===========================================================================
56
57BUNDLED_XEN_GUESTS ?= ""
58XEN_GUEST_IMAGE_FSTYPE ?= "ext4"
59XEN_GUEST_MEMORY_DEFAULT ?= "512"
60XEN_GUEST_VCPUS_DEFAULT ?= "1"
61XEN_GUEST_VIF_DEFAULT ?= "bridge=xenbr0"
62XEN_GUEST_EXTRA_DEFAULT ?= "root=/dev/xvda ro ip=dhcp"
63XEN_GUEST_DISK_DEVICE_DEFAULT ?= "xvda"
64
65# ===========================================================================
66# Parse-time dependency generation
67# ===========================================================================
68
69python __anonymous() {
70 bundled = (d.getVar('BUNDLED_XEN_GUESTS') or "").split()
71 if not bundled:
72 return
73
74 deps = ""
75 for entry in bundled:
76 parts = entry.split(':')
77 guest_name = parts[0]
78
79 # Check for :external tag
80 is_external = 'external' in parts
81
82 # Skip dependency for external guests
83 if is_external:
84 continue
85
86 # Generate dependency on guest recipe
87 deps += " %s:do_image_complete" % guest_name
88
89 if deps:
90 d.appendVarFlag('do_rootfs', 'depends', deps)
91}
92
93# ===========================================================================
94# Python helpers - build varflag maps for shell access
95# ===========================================================================
96
97# Build XEN_GUEST_CONFIG_FILE_MAP from varflags
98# Format: guest1=/path/to/file1;guest2=/path/to/file2
99def get_xen_guest_config_map(d):
100 bundled = (d.getVar('BUNDLED_XEN_GUESTS') or "").split()
101 if not bundled:
102 return ""
103
104 mappings = []
105 for entry in bundled:
106 parts = entry.split(':')
107 guest_name = parts[0]
108 custom_file = d.getVarFlag('XEN_GUEST_CONFIG_FILE', guest_name)
109 if custom_file:
110 mappings.append("%s=%s" % (guest_name, custom_file))
111
112 return ";".join(mappings)
113
114XEN_GUEST_CONFIG_FILE_MAP = "${@get_xen_guest_config_map(d)}"
115
116# Build XEN_GUEST_PARAMS_MAP from varflags
117# Format: guest1=memory|vcpus|vif|extra|disk_device|name|rootfs|kernel;guest2=...
118def get_xen_guest_params(d):
119 bundled = (d.getVar('BUNDLED_XEN_GUESTS') or "").split()
120 if not bundled:
121 return ""
122
123 mem_default = d.getVar('XEN_GUEST_MEMORY_DEFAULT')
124 vcpus_default = d.getVar('XEN_GUEST_VCPUS_DEFAULT')
125 vif_default = d.getVar('XEN_GUEST_VIF_DEFAULT')
126 extra_default = d.getVar('XEN_GUEST_EXTRA_DEFAULT')
127 disk_default = d.getVar('XEN_GUEST_DISK_DEVICE_DEFAULT')
128
129 mappings = []
130 for entry in bundled:
131 parts = entry.split(':')
132 guest_name = parts[0]
133
134 memory = d.getVarFlag('XEN_GUEST_MEMORY', guest_name) or mem_default
135 vcpus = d.getVarFlag('XEN_GUEST_VCPUS', guest_name) or vcpus_default
136 vif = d.getVarFlag('XEN_GUEST_VIF', guest_name) or vif_default
137 extra = d.getVarFlag('XEN_GUEST_EXTRA', guest_name) or extra_default
138 disk_device = d.getVarFlag('XEN_GUEST_DISK_DEVICE', guest_name) or disk_default
139 name = d.getVarFlag('XEN_GUEST_NAME', guest_name) or guest_name
140 rootfs = d.getVarFlag('XEN_GUEST_ROOTFS', guest_name) or ""
141 kernel = d.getVarFlag('XEN_GUEST_KERNEL', guest_name) or ""
142
143 params = "|".join([memory, vcpus, vif, extra, disk_device, name, rootfs, kernel])
144 mappings.append("%s=%s" % (guest_name, params))
145
146 return ";".join(mappings)
147
148XEN_GUEST_PARAMS_MAP = "${@get_xen_guest_params(d)}"
149
150# ===========================================================================
151# Shell function: merge_installed_xen_bundles
152# Processes packages created by xen-guest-bundle.bbclass
153# ===========================================================================
154
155merge_installed_xen_bundles() {
156 set +e
157
158 BUNDLES_DIR="${IMAGE_ROOTFS}${datadir}/xen-guest-bundles"
159
160 if [ ! -d "${BUNDLES_DIR}" ]; then
161 bbnote "No Xen guest bundles found in ${BUNDLES_DIR}"
162 return 0
163 fi
164
165 bbnote "Processing installed Xen guest bundles from ${BUNDLES_DIR}"
166
167 DEST_DIR="${IMAGE_ROOTFS}/var/lib/xen/images"
168 CONFIG_DIR="${IMAGE_ROOTFS}/etc/xen"
169 AUTO_DIR="${IMAGE_ROOTFS}/etc/xen/auto"
170
171 mkdir -p "$DEST_DIR"
172 mkdir -p "$CONFIG_DIR"
173
174 for bundle_dir in ${BUNDLES_DIR}/*/; do
175 [ -d "$bundle_dir" ] || continue
176
177 manifest="${bundle_dir}manifest"
178 if [ ! -f "$manifest" ]; then
179 bbwarn "No manifest found in $bundle_dir, skipping"
180 continue
181 fi
182
183 bundle_name=$(basename "$bundle_dir")
184 bbnote "Processing bundle: $bundle_name"
185
186 while IFS=: read -r guest_name rootfs_file kernel_file autostart_flag || [ -n "$guest_name" ]; do
187 [ -z "$guest_name" ] && continue
188
189 bbnote " Guest: $guest_name (rootfs=$rootfs_file kernel=$kernel_file autostart=$autostart_flag)"
190
191 # Copy rootfs
192 if [ -f "${bundle_dir}images/${rootfs_file}" ]; then
193 cp "${bundle_dir}images/${rootfs_file}" "$DEST_DIR/"
194 bbnote " Installed rootfs: $rootfs_file"
195 else
196 bbwarn " Rootfs not found: ${bundle_dir}images/${rootfs_file}"
197 fi
198
199 # Copy kernel
200 if [ -f "${bundle_dir}images/${kernel_file}" ]; then
201 cp "${bundle_dir}images/${kernel_file}" "$DEST_DIR/"
202 bbnote " Installed kernel: $kernel_file"
203 else
204 bbwarn " Kernel not found: ${bundle_dir}images/${kernel_file}"
205 fi
206
207 # Copy config
208 if [ -f "${bundle_dir}configs/${guest_name}.cfg" ]; then
209 cp "${bundle_dir}configs/${guest_name}.cfg" "${CONFIG_DIR}/"
210 bbnote " Installed config: ${guest_name}.cfg"
211 else
212 bbwarn " Config not found: ${bundle_dir}configs/${guest_name}.cfg"
213 fi
214
215 # Handle autostart
216 if [ "$autostart_flag" = "autostart" ]; then
217 mkdir -p "$AUTO_DIR"
218 ln -sf "/etc/xen/${guest_name}.cfg" "${AUTO_DIR}/${guest_name}.cfg"
219 bbnote " Autostart enabled: ${guest_name}.cfg"
220 fi
221
222 bbnote " Guest '$guest_name' merged successfully"
223 done < "$manifest"
224 done
225
226 # Clean up bundle files from final image
227 rm -rf "${BUNDLES_DIR}"
228 bbnote "Cleaned up Xen guest bundle files"
229
230 return 0
231}
232
233# ===========================================================================
234# Shell function: bundle_xen_guests
235# ROOTFS_POSTPROCESS_COMMAND
236# ===========================================================================
237
238bundle_xen_guests() {
239 set +e
240
241 # ========================================================================
242 # Helper functions
243 # Defined inside bundle_xen_guests() for bitbake execution context
244 # ========================================================================
245
246 # Extract a per-guest parameter from the params map
247 # Args: guest_name field_index (0=memory, 1=vcpus, 2=vif, 3=extra,
248 # 4=disk_device, 5=name, 6=rootfs_override, 7=kernel_override)
249 get_guest_param() {
250 local guest="$1"
251 local field="$2"
252 local params_map="${XEN_GUEST_PARAMS_MAP}"
253
254 # Find this guest's params in the map
255 local guest_params=$(echo "$params_map" | tr ';' '\n' | grep "^${guest}=" | cut -d= -f2-)
256 if [ -z "$guest_params" ]; then
257 return 1
258 fi
259
260 # Extract field by index (pipe-separated)
261 echo "$guest_params" | cut -d'|' -f$(expr $field + 1)
262 }
263
264 # Resolve guest rootfs path
265 # Checks varflag override first, then standard Yocto naming
266 resolve_guest_rootfs() {
267 local guest="$1"
268 local override=$(get_guest_param "$guest" 6)
269
270 if [ -n "$override" ]; then
271 # Explicit rootfs filename provided
272 local path="${DEPLOY_DIR_IMAGE}/$override"
273 if [ -e "$path" ]; then
274 readlink -f "$path"
275 return 0
276 fi
277 bbwarn "XEN_GUEST_ROOTFS override '$override' not found at $path"
278 return 1
279 fi
280
281 # Standard Yocto naming: <recipe>-<MACHINE>.<fstype>
282 local path="${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.${XEN_GUEST_IMAGE_FSTYPE}"
283 if [ -e "$path" ]; then
284 readlink -f "$path"
285 return 0
286 fi
287
288 # Fallback: <recipe>-<MACHINE>.rootfs.<fstype>
289 path="${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.rootfs.${XEN_GUEST_IMAGE_FSTYPE}"
290 if [ -e "$path" ]; then
291 readlink -f "$path"
292 return 0
293 fi
294
295 bbwarn "Guest rootfs not found for '$guest'. Searched:"
296 bbwarn " ${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.${XEN_GUEST_IMAGE_FSTYPE}"
297 bbwarn " ${DEPLOY_DIR_IMAGE}/${guest}-${MACHINE}.rootfs.${XEN_GUEST_IMAGE_FSTYPE}"
298 return 1
299 }
300
301 # Resolve guest kernel path
302 # Checks varflag override first, then uses KERNEL_IMAGETYPE
303 resolve_guest_kernel() {
304 local guest="$1"
305 local override=$(get_guest_param "$guest" 7)
306
307 if [ -n "$override" ]; then
308 # Explicit kernel filename provided
309 local path="${DEPLOY_DIR_IMAGE}/$override"
310 if [ -e "$path" ]; then
311 readlink -f "$path"
312 return 0
313 fi
314 bbwarn "XEN_GUEST_KERNEL override '$override' not found at $path"
315 return 1
316 fi
317
318 # Default: shared kernel (same MACHINE)
319 local path="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE}"
320 if [ -e "$path" ]; then
321 readlink -f "$path"
322 return 0
323 fi
324
325 bbwarn "Guest kernel not found at ${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE}"
326 return 1
327 }
328
329 # Generate a Xen guest configuration file
330 # Args: guest_name rootfs_path kernel_path
331 generate_xen_guest_config() {
332 local guest="$1"
333 local rootfs_basename="$2"
334 local kernel_basename="$3"
335 local outfile="$4"
336
337 local memory=$(get_guest_param "$guest" 0)
338 local vcpus=$(get_guest_param "$guest" 1)
339 local vif=$(get_guest_param "$guest" 2)
340 local extra=$(get_guest_param "$guest" 3)
341 local disk_device=$(get_guest_param "$guest" 4)
342 local name=$(get_guest_param "$guest" 5)
343
344 cat > "$outfile" << EOF
345name = "$name"
346memory = $memory
347vcpus = $vcpus
348disk = ['file:/var/lib/xen/images/$rootfs_basename,$disk_device,rw']
349vif = ['$vif']
350kernel = "/var/lib/xen/images/$kernel_basename"
351extra = "$extra"
352EOF
353 }
354
355 # ========================================================================
356 # Main loop
357 # ========================================================================
358
359 if [ -z "${BUNDLED_XEN_GUESTS}" ]; then
360 bbnote "No Xen bundled guests specified"
361 return 0
362 fi
363
364 bbnote "Processing Xen bundled guests: ${BUNDLED_XEN_GUESTS}"
365
366 DEST_DIR="${IMAGE_ROOTFS}/var/lib/xen/images"
367 CONFIG_DIR="${IMAGE_ROOTFS}/etc/xen"
368 AUTO_DIR="${IMAGE_ROOTFS}/etc/xen/auto"
369
370 mkdir -p "$DEST_DIR"
371 mkdir -p "$CONFIG_DIR"
372
373 for entry in ${BUNDLED_XEN_GUESTS}; do
374 # Parse: recipe-name[:autostart][:external]
375 guest_name="$(echo $entry | cut -d: -f1)"
376
377 # Check for tags
378 autostart=""
379 if echo "$entry" | grep -q ':autostart'; then
380 autostart="1"
381 fi
382
383 bbnote "Processing guest: $guest_name (autostart=$autostart)"
384
385 # Resolve rootfs
386 rootfs_path=$(resolve_guest_rootfs "$guest_name")
387 if [ $? -ne 0 ] || [ -z "$rootfs_path" ]; then
388 bbfatal "Cannot resolve rootfs for guest '$guest_name'"
389 fi
390 rootfs_basename=$(basename "$rootfs_path")
391
392 # Resolve kernel
393 kernel_path=$(resolve_guest_kernel "$guest_name")
394 if [ $? -ne 0 ] || [ -z "$kernel_path" ]; then
395 bbfatal "Cannot resolve kernel for guest '$guest_name'"
396 fi
397 kernel_basename=$(basename "$kernel_path")
398
399 # Copy rootfs and kernel to target
400 bbnote "Copying rootfs: $rootfs_path -> $DEST_DIR/"
401 cp "$rootfs_path" "$DEST_DIR/"
402
403 bbnote "Copying kernel: $kernel_path -> $DEST_DIR/"
404 cp "$kernel_path" "$DEST_DIR/"
405
406 # Generate or install config file
407 config_map="${XEN_GUEST_CONFIG_FILE_MAP}"
408 custom_config=$(echo "$config_map" | tr ';' '\n' | grep "^${guest_name}=" | cut -d= -f2-)
409
410 if [ -n "$custom_config" ] && [ -f "$custom_config" ]; then
411 bbnote "Installing custom config: $custom_config"
412 # Fix paths in custom config to point to /var/lib/xen/images/
413 sed -E \
414 -e "s#^(disk = \[)[^,]+#\1'file:/var/lib/xen/images/$rootfs_basename#" \
415 -e "s#^(kernel = )\"[^\"]+\"#\1\"/var/lib/xen/images/$kernel_basename\"#" \
416 "$custom_config" > "${CONFIG_DIR}/${guest_name}.cfg"
417 else
418 bbnote "Generating config for $guest_name"
419 generate_xen_guest_config "$guest_name" "$rootfs_basename" "$kernel_basename" \
420 "${CONFIG_DIR}/${guest_name}.cfg"
421 fi
422
423 # Handle autostart
424 if [ -n "$autostart" ]; then
425 mkdir -p "$AUTO_DIR"
426 ln -sf "/etc/xen/${guest_name}.cfg" "${AUTO_DIR}/${guest_name}.cfg"
427 bbnote "Autostart enabled: ${AUTO_DIR}/${guest_name}.cfg -> /etc/xen/${guest_name}.cfg"
428 fi
429
430 bbnote "Guest '$guest_name' bundled successfully"
431 done
432
433 bbnote "Done processing all Xen bundled guests"
434}
435
436# First merge any bundles installed via IMAGE_INSTALL, then process BUNDLED_XEN_GUESTS
437ROOTFS_POSTPROCESS_COMMAND += "merge_installed_xen_bundles; bundle_xen_guests;"
diff --git a/recipes-extended/images/xen-guest-image-minimal.bb b/recipes-extended/images/xen-guest-image-minimal.bb
index 76f320e4..1afc4219 100644
--- a/recipes-extended/images/xen-guest-image-minimal.bb
+++ b/recipes-extended/images/xen-guest-image-minimal.bb
@@ -1,7 +1,6 @@
1DESCRIPTION = "A Xen guest image." 1DESCRIPTION = "A Xen guest image."
2 2
3inherit core-image features_check deploy 3inherit core-image features_check
4inherit kernel-artifact-names
5 4
6IMAGE_INSTALL += " \ 5IMAGE_INSTALL += " \
7 packagegroup-core-boot \ 6 packagegroup-core-boot \
@@ -26,26 +25,3 @@ LICENSE = "MIT"
26APPEND += "console=hvc0" 25APPEND += "console=hvc0"
27 26
28IMAGE_FSTYPES = "tar.bz2 ext4 ext4.qcow2" 27IMAGE_FSTYPES = "tar.bz2 ext4 ext4.qcow2"
29
30XEN_GUEST_AUTO_BUNDLE ?= ""
31
32# When a xen-guest-image-minimal is built with the
33# XEN_GUEST_AUTO_BUNDLE varaible set to True, a configuration file for
34# automatic guest bundling will be generated and the guest bundled
35# automatically when a xen host image is built.
36do_deploy() {
37 if [ -n "${XEN_GUEST_AUTO_BUNDLE}" ]; then
38 outname="xen-guest-bundle-${IMAGE_BASENAME}${IMAGE_MACHINE_SUFFIX}-${IMAGE_VERSION_SUFFIX}.cfg"
39cat <<EOF >>${DEPLOYDIR}/$outname
40name = "xen-guest"
41memory = 512
42vcpus = 1
43disk = ['file:${IMAGE_LINK_NAME}.ext4,xvda,rw']
44vif = ['bridge=xenbr0']
45kernel = "${KERNEL_IMAGETYPE}"
46extra = "root=/dev/xvda ro ip=dhcp"
47EOF
48 fi
49}
50
51addtask deploy after do_compile
diff --git a/recipes-extended/xen-guest-bundles/example-xen-guest-bundle_1.0.bb b/recipes-extended/xen-guest-bundles/example-xen-guest-bundle_1.0.bb
new file mode 100644
index 00000000..96e17275
--- /dev/null
+++ b/recipes-extended/xen-guest-bundles/example-xen-guest-bundle_1.0.bb
@@ -0,0 +1,54 @@
1# example-xen-guest-bundle_1.0.bb
2# ===========================================================================
3# Example Xen guest bundle recipe demonstrating xen-guest-bundle.bbclass
4# ===========================================================================
5#
6# This recipe shows how to create a package that bundles Xen guest images.
7# When installed via IMAGE_INSTALL into a Dom0 image that inherits
8# xen-guest-cross-install, the guests are automatically deployed.
9#
10# Usage in image recipe (e.g., xen-image-minimal.bb):
11# IMAGE_INSTALL += "example-xen-guest-bundle"
12#
13# Or in local.conf (use pn- override for specific images):
14# IMAGE_INSTALL:append:pn-xen-image-minimal = " example-xen-guest-bundle"
15#
16# IMPORTANT: Do NOT use global IMAGE_INSTALL:append without pn- override!
17# This causes circular dependencies when guest images try to include
18# the bundle that depends on them.
19#
20# ===========================================================================
21
22SUMMARY = "Example Xen guest bundle"
23DESCRIPTION = "Demonstrates xen-guest-bundle.bbclass by bundling the \
24 xen-guest-image-minimal guest. Use this as a template \
25 for your own Xen guest bundles."
26LICENSE = "MIT"
27LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
28
29inherit xen-guest-bundle
30
31# Define guests to bundle
32# Format: recipe-name[:autostart][:external]
33#
34# recipe-name: Yocto image recipe that produces the guest rootfs
35# autostart: Creates symlink in /etc/xen/auto/ for xendomains
36# external: Skip dependency generation (pre-built/3rd-party guest)
37XEN_GUEST_BUNDLES = "\
38 xen-guest-image-minimal:autostart \
39"
40
41# Per-guest configuration via varflags (optional):
42XEN_GUEST_MEMORY[xen-guest-image-minimal] = "1024"
43# XEN_GUEST_VCPUS[xen-guest-image-minimal] = "2"
44# XEN_GUEST_VIF[xen-guest-image-minimal] = "bridge=xenbr0"
45# XEN_GUEST_EXTRA[xen-guest-image-minimal] = "root=/dev/xvda ro console=hvc0 ip=dhcp"
46
47# Custom config file (replaces auto-generation):
48# SRC_URI += "file://my-custom-guest.cfg"
49# XEN_GUEST_CONFIG_FILE[xen-guest-image-minimal] = "${UNPACKDIR}/my-custom-guest.cfg"
50
51# External guest example (rootfs/kernel already in DEPLOY_DIR_IMAGE):
52# XEN_GUEST_BUNDLES += "my-vendor-guest:external"
53# XEN_GUEST_ROOTFS[my-vendor-guest] = "vendor-rootfs.ext4"
54# XEN_GUEST_KERNEL[my-vendor-guest] = "vendor-kernel"