summaryrefslogtreecommitdiffstats
path: root/recipes-containers/vcontainer
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-15 21:50:06 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commit52ac90f1b29af4d0e6dc57ca9c947571f87872fd (patch)
treea3d98ad0bf5ad5a1a4c9ee72c75e87661805e6d4 /recipes-containers/vcontainer
parent929d1609efefd3189b650facaaeb3d2a13ffbe1d (diff)
downloadmeta-virtualization-52ac90f1b29af4d0e6dc57ca9c947571f87872fd.tar.gz
vcontainer: add virtio-9p fast path for batch imports
Add virtio-9p filesystem support for faster storage output during batch container imports, replacing slow base64-over-console method. - Add --timeout option for configurable import timeouts - Mount virtio-9p share in batch-import mode - Parse _9p=1 kernel parameter for 9p availability - Write storage.tar directly to shared filesystem - Reduces import time from ~600s to ~11s for large containers Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers/vcontainer')
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-init-common.sh4
-rwxr-xr-xrecipes-containers/vcontainer/files/vdkr-init.sh90
-rwxr-xr-xrecipes-containers/vcontainer/files/vpdmn-init.sh31
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh209
4 files changed, 284 insertions, 50 deletions
diff --git a/recipes-containers/vcontainer/files/vcontainer-init-common.sh b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
index 738d0343..619e334a 100755
--- a/recipes-containers/vcontainer/files/vcontainer-init-common.sh
+++ b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
@@ -106,6 +106,7 @@ parse_cmdline() {
106 RUNTIME_NETWORK="0" 106 RUNTIME_NETWORK="0"
107 RUNTIME_INTERACTIVE="0" 107 RUNTIME_INTERACTIVE="0"
108 RUNTIME_DAEMON="0" 108 RUNTIME_DAEMON="0"
109 RUNTIME_9P="0" # virtio-9p available for fast I/O
109 RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes 110 RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes
110 111
111 for param in $(cat /proc/cmdline); do 112 for param in $(cat /proc/cmdline); do
@@ -134,6 +135,9 @@ parse_cmdline() {
134 ${VCONTAINER_RUNTIME_PREFIX}_idle_timeout=*) 135 ${VCONTAINER_RUNTIME_PREFIX}_idle_timeout=*)
135 RUNTIME_IDLE_TIMEOUT="${param#${VCONTAINER_RUNTIME_PREFIX}_idle_timeout=}" 136 RUNTIME_IDLE_TIMEOUT="${param#${VCONTAINER_RUNTIME_PREFIX}_idle_timeout=}"
136 ;; 137 ;;
138 ${VCONTAINER_RUNTIME_PREFIX}_9p=*)
139 RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}"
140 ;;
137 esac 141 esac
138 done 142 done
139 143
diff --git a/recipes-containers/vcontainer/files/vdkr-init.sh b/recipes-containers/vcontainer/files/vdkr-init.sh
index 318ce521..67465138 100755
--- a/recipes-containers/vcontainer/files/vdkr-init.sh
+++ b/recipes-containers/vcontainer/files/vdkr-init.sh
@@ -123,11 +123,18 @@ start_dockerd() {
123 123
124 # Parse default registry from kernel cmdline (docker_registry=host:port/namespace) 124 # Parse default registry from kernel cmdline (docker_registry=host:port/namespace)
125 # Kernel cmdline OVERRIDES baked config from /etc/vdkr/registry.conf 125 # Kernel cmdline OVERRIDES baked config from /etc/vdkr/registry.conf
126 # Use docker_registry=none to explicitly disable baked registry
126 # This enables: "docker pull container-base" → "docker pull 10.0.2.2:5000/yocto/container-base" 127 # This enables: "docker pull container-base" → "docker pull 10.0.2.2:5000/yocto/container-base"
127 GREP_RESULT=$(grep -o 'docker_registry=[^ ]*' /proc/cmdline 2>/dev/null || true) 128 GREP_RESULT=$(grep -o 'docker_registry=[^ ]*' /proc/cmdline 2>/dev/null || true)
128 if [ -n "$GREP_RESULT" ]; then 129 if [ -n "$GREP_RESULT" ]; then
129 DOCKER_DEFAULT_REGISTRY=$(echo "$GREP_RESULT" | sed 's/docker_registry=//') 130 CMDLINE_REGISTRY=$(echo "$GREP_RESULT" | sed 's/docker_registry=//')
130 log "Registry from cmdline: $DOCKER_DEFAULT_REGISTRY" 131 if [ "$CMDLINE_REGISTRY" = "none" ] || [ -z "$CMDLINE_REGISTRY" ]; then
132 DOCKER_DEFAULT_REGISTRY=""
133 log "Registry disabled via cmdline"
134 else
135 DOCKER_DEFAULT_REGISTRY="$CMDLINE_REGISTRY"
136 log "Registry from cmdline: $DOCKER_DEFAULT_REGISTRY"
137 fi
131 elif [ -n "$DOCKER_DEFAULT_REGISTRY" ]; then 138 elif [ -n "$DOCKER_DEFAULT_REGISTRY" ]; then
132 log "Registry from baked config: $DOCKER_DEFAULT_REGISTRY" 139 log "Registry from baked config: $DOCKER_DEFAULT_REGISTRY"
133 fi 140 fi
@@ -285,8 +292,26 @@ is_pull_command() {
285 echo "$cmd" | grep -qE '^docker pull ' 292 echo "$cmd" | grep -qE '^docker pull '
286} 293}
287 294
295# Helper function to check if an image exists locally
296# Returns 0 if exists, 1 if not
297image_exists_locally() {
298 local img="$1"
299 # Try exact match first, then with :latest suffix
300 if docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -qE "^${img}$"; then
301 return 0
302 fi
303 # If no tag specified, try with :latest
304 if ! echo "$img" | grep -q ':'; then
305 if docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -qE "^${img}:latest$"; then
306 return 0
307 fi
308 fi
309 return 1
310}
311
288# Helper function to transform an unqualified image name 312# Helper function to transform an unqualified image name
289# Must be defined before transform_docker_command which uses it 313# Must be defined before transform_docker_command which uses it
314# Priority: 1) local image as-is, 2) with registry prefix, 3) unchanged
290transform_image_name() { 315transform_image_name() {
291 local img="$1" 316 local img="$1"
292 if [ -z "$img" ]; then 317 if [ -z "$img" ]; then
@@ -306,7 +331,18 @@ transform_image_name() {
306 fi 331 fi
307 # Check if image is unqualified (no /) 332 # Check if image is unqualified (no /)
308 if ! echo "$img" | grep -q '/'; then 333 if ! echo "$img" | grep -q '/'; then
309 echo "$DOCKER_DEFAULT_REGISTRY/$img" 334 # First check if image exists locally as-is
335 if image_exists_locally "$img"; then
336 echo "$img"
337 return
338 fi
339 # If not local and we have a default registry, use it
340 if [ -n "$DOCKER_DEFAULT_REGISTRY" ]; then
341 echo "$DOCKER_DEFAULT_REGISTRY/$img"
342 return
343 fi
344 # No registry configured, use as-is (Docker will try Docker Hub)
345 echo "$img"
310 # Check if already has registry with port - don't transform 346 # Check if already has registry with port - don't transform
311 elif echo "$img" | grep -qE '^[^/]+:[0-9]+/'; then 347 elif echo "$img" | grep -qE '^[^/]+:[0-9]+/'; then
312 echo "$img" 348 echo "$img"
@@ -392,6 +428,12 @@ transform_docker_command() {
392 local skip_next=false 428 local skip_next=false
393 429
394 for arg in $args; do 430 for arg in $args; do
431 # Once we have the image, everything else is the container command
432 if [ -n "$image" ]; then
433 rest="$rest $arg"
434 continue
435 fi
436
395 if [ "$skip_next" = "true" ]; then 437 if [ "$skip_next" = "true" ]; then
396 new_args="$new_args $arg" 438 new_args="$new_args $arg"
397 skip_next=false 439 skip_next=false
@@ -402,11 +444,11 @@ transform_docker_command() {
402 -d|--detach|-i|--interactive|-t|--tty|--rm|--privileged) 444 -d|--detach|-i|--interactive|-t|--tty|--rm|--privileged)
403 new_args="$new_args $arg" 445 new_args="$new_args $arg"
404 ;; 446 ;;
405 -p|--publish|-v|--volume|-e|--env|--name|--network|-w|--workdir|--entrypoint) 447 -p|--publish|-v|--volume|-e|--env|--name|--network|-w|--workdir|--entrypoint|-m|--memory|--cpus|--cpu-shares)
406 new_args="$new_args $arg" 448 new_args="$new_args $arg"
407 skip_next=true 449 skip_next=true
408 ;; 450 ;;
409 -p=*|--publish=*|-v=*|--volume=*|-e=*|--env=*|--name=*|--network=*|-w=*|--workdir=*|--entrypoint=*) 451 -p=*|--publish=*|-v=*|--volume=*|-e=*|--env=*|--name=*|--network=*|-w=*|--workdir=*|--entrypoint=*|-m=*|--memory=*)
410 new_args="$new_args $arg" 452 new_args="$new_args $arg"
411 ;; 453 ;;
412 -*) 454 -*)
@@ -415,12 +457,7 @@ transform_docker_command() {
415 ;; 457 ;;
416 *) 458 *)
417 # First non-option is the image 459 # First non-option is the image
418 if [ -z "$image" ]; then 460 image="$arg"
419 image="$arg"
420 else
421 # Rest is the command
422 rest="$rest $arg"
423 fi
424 ;; 461 ;;
425 esac 462 esac
426 done 463 done
@@ -452,11 +489,21 @@ handle_storage_output() {
452 echo "Storage size: $STORAGE_SIZE bytes" 489 echo "Storage size: $STORAGE_SIZE bytes"
453 490
454 if [ "$STORAGE_SIZE" -gt 1000 ]; then 491 if [ "$STORAGE_SIZE" -gt 1000 ]; then
455 dmesg -n 1 492 # Use virtio-9p if available (much faster than console base64)
456 echo "===STORAGE_START===" 493 if [ "$RUNTIME_9P" = "1" ] && mountpoint -q /mnt/share 2>/dev/null; then
457 base64 /tmp/storage.tar 494 echo "Using virtio-9p for storage output (fast path)"
458 echo "===STORAGE_END===" 495 cp /tmp/storage.tar /mnt/share/storage.tar
459 echo "===EXIT_CODE=$EXEC_EXIT_CODE===" 496 sync
497 echo "===9P_STORAGE_DONE==="
498 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
499 else
500 # Fallback: base64 to console (slow)
501 dmesg -n 1
502 echo "===STORAGE_START==="
503 base64 /tmp/storage.tar
504 echo "===STORAGE_END==="
505 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
506 fi
460 else 507 else
461 echo "===ERROR===" 508 echo "===ERROR==="
462 echo "Storage too small" 509 echo "Storage too small"
@@ -484,6 +531,17 @@ setup_cgroups
484# Parse kernel command line 531# Parse kernel command line
485parse_cmdline 532parse_cmdline
486 533
534# Mount virtio-9p share if available (for fast storage output in batch-import mode)
535if [ "$RUNTIME_9P" = "1" ]; then
536 mkdir -p /mnt/share
537 if mount -t 9p -o trans=virtio,version=9p2000.L,cache=none ${VCONTAINER_SHARE_NAME} /mnt/share 2>/dev/null; then
538 log "Mounted virtio-9p share at /mnt/share (fast I/O enabled)"
539 else
540 log "WARNING: Could not mount virtio-9p share, falling back to console output"
541 RUNTIME_9P="0"
542 fi
543fi
544
487# Detect and configure disks 545# Detect and configure disks
488detect_disks 546detect_disks
489 547
diff --git a/recipes-containers/vcontainer/files/vpdmn-init.sh b/recipes-containers/vcontainer/files/vpdmn-init.sh
index 52aa9129..70acd8af 100755
--- a/recipes-containers/vcontainer/files/vpdmn-init.sh
+++ b/recipes-containers/vcontainer/files/vpdmn-init.sh
@@ -120,11 +120,21 @@ handle_storage_output() {
120 echo "Storage size: $STORAGE_SIZE bytes" 120 echo "Storage size: $STORAGE_SIZE bytes"
121 121
122 if [ "$STORAGE_SIZE" -gt 1000 ]; then 122 if [ "$STORAGE_SIZE" -gt 1000 ]; then
123 dmesg -n 1 123 # Use virtio-9p if available (much faster than console base64)
124 echo "===STORAGE_START===" 124 if [ "$RUNTIME_9P" = "1" ] && mountpoint -q /mnt/share 2>/dev/null; then
125 base64 /tmp/storage.tar 125 echo "Using virtio-9p for storage output (fast path)"
126 echo "===STORAGE_END===" 126 cp /tmp/storage.tar /mnt/share/storage.tar
127 echo "===EXIT_CODE=$EXEC_EXIT_CODE===" 127 sync
128 echo "===9P_STORAGE_DONE==="
129 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
130 else
131 # Fallback: base64 to console (slow)
132 dmesg -n 1
133 echo "===STORAGE_START==="
134 base64 /tmp/storage.tar
135 echo "===STORAGE_END==="
136 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
137 fi
128 else 138 else
129 echo "===ERROR===" 139 echo "===ERROR==="
130 echo "Storage too small" 140 echo "Storage too small"
@@ -154,6 +164,17 @@ setup_cgroups
154# Parse kernel command line 164# Parse kernel command line
155parse_cmdline 165parse_cmdline
156 166
167# Mount virtio-9p share if available (for fast storage output in batch-import mode)
168if [ "$RUNTIME_9P" = "1" ]; then
169 mkdir -p /mnt/share
170 if mount -t 9p -o trans=virtio,version=9p2000.L,cache=none ${VCONTAINER_SHARE_NAME} /mnt/share 2>/dev/null; then
171 log "Mounted virtio-9p share at /mnt/share (fast I/O enabled)"
172 else
173 log "WARNING: Could not mount virtio-9p share, falling back to console output"
174 RUNTIME_9P="0"
175 fi
176fi
177
157# Detect and configure disks 178# Detect and configure disks
158detect_disks 179detect_disks
159 180
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh
index af9b855c..5d824ba5 100755
--- a/recipes-containers/vcontainer/files/vrunner.sh
+++ b/recipes-containers/vcontainer/files/vrunner.sh
@@ -103,6 +103,116 @@ log() {
103 esac 103 esac
104} 104}
105 105
106# ============================================================================
107# Multi-Architecture OCI Support for Batch Import
108# ============================================================================
109
110# Normalize architecture name to OCI convention
111normalize_arch_to_oci() {
112 local arch="$1"
113 case "$arch" in
114 aarch64) echo "arm64" ;;
115 x86_64) echo "amd64" ;;
116 *) echo "$arch" ;;
117 esac
118}
119
120# Check if OCI directory contains a multi-architecture Image Index
121is_oci_image_index() {
122 local oci_dir="$1"
123 [ -f "$oci_dir/index.json" ] || return 1
124 grep -q '"platform"' "$oci_dir/index.json" 2>/dev/null
125}
126
127# Get list of available platforms in a multi-arch OCI Image Index
128get_oci_platforms() {
129 local oci_dir="$1"
130 [ -f "$oci_dir/index.json" ] || return 1
131 grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$oci_dir/index.json" 2>/dev/null | \
132 sed 's/.*"\([^"]*\)"$/\1/' | tr '\n' ' ' | sed 's/ $//'
133}
134
135# Select manifest digest for a specific platform from OCI Image Index
136# Returns the sha256 digest (without prefix)
137select_platform_manifest() {
138 local oci_dir="$1"
139 local target_arch="$2"
140 local oci_arch=$(normalize_arch_to_oci "$target_arch")
141
142 [ -f "$oci_dir/index.json" ] || return 1
143
144 local in_manifest=0 current_digest="" current_arch="" matched_digest=""
145
146 while IFS= read -r line; do
147 if echo "$line" | grep -q '"manifests"'; then
148 in_manifest=1
149 continue
150 fi
151 if [ "$in_manifest" = "1" ]; then
152 if echo "$line" | grep -q '"digest"'; then
153 current_digest=$(echo "$line" | sed 's/.*"sha256:\([a-f0-9]*\)".*/\1/')
154 fi
155 # Handle both: "architecture": "arm64" or {"architecture": "arm64", ...}
156 if echo "$line" | grep -q '"architecture"'; then
157 current_arch=$(echo "$line" | sed 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
158 if [ "$current_arch" = "$oci_arch" ]; then
159 matched_digest="$current_digest"
160 break
161 fi
162 fi
163 if echo "$line" | grep -q '^[[:space:]]*}'; then
164 current_digest=""
165 current_arch=""
166 fi
167 fi
168 done < "$oci_dir/index.json"
169
170 [ -n "$matched_digest" ] && echo "$matched_digest"
171}
172
173# Extract a single platform from multi-arch OCI to a new OCI directory
174extract_platform_oci() {
175 local src_dir="$1"
176 local dest_dir="$2"
177 local manifest_digest="$3"
178
179 mkdir -p "$dest_dir/blobs/sha256"
180 cp "$src_dir/blobs/sha256/$manifest_digest" "$dest_dir/blobs/sha256/"
181
182 local manifest_file="$src_dir/blobs/sha256/$manifest_digest"
183
184 # Copy config blob
185 local config_digest=$(grep -o '"config"[[:space:]]*:[[:space:]]*{[^}]*"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' "$manifest_file" | \
186 sed 's/.*sha256:\([a-f0-9]*\)".*/\1/')
187 [ -n "$config_digest" ] && [ -f "$src_dir/blobs/sha256/$config_digest" ] && \
188 cp "$src_dir/blobs/sha256/$config_digest" "$dest_dir/blobs/sha256/"
189
190 # Copy layer blobs
191 grep -o '"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' "$manifest_file" | \
192 sed 's/.*sha256:\([a-f0-9]*\)".*/\1/' | while read -r layer_digest; do
193 [ -f "$src_dir/blobs/sha256/$layer_digest" ] && \
194 cp "$src_dir/blobs/sha256/$layer_digest" "$dest_dir/blobs/sha256/"
195 done
196
197 local manifest_size=$(stat -c%s "$manifest_file" 2>/dev/null || stat -f%z "$manifest_file" 2>/dev/null)
198
199 cat > "$dest_dir/index.json" << EOF
200{
201 "schemaVersion": 2,
202 "manifests": [
203 {
204 "mediaType": "application/vnd.oci.image.manifest.v1+json",
205 "digest": "sha256:$manifest_digest",
206 "size": $manifest_size
207 }
208 ]
209}
210EOF
211
212 [ -f "$src_dir/oci-layout" ] && cp "$src_dir/oci-layout" "$dest_dir/" || \
213 echo '{"imageLayoutVersion": "1.0.0"}' > "$dest_dir/oci-layout"
214}
215
106show_usage() { 216show_usage() {
107 cat << 'EOF' 217 cat << 'EOF'
108vrunner.sh - Execute docker commands in QEMU-emulated environment 218vrunner.sh - Execute docker commands in QEMU-emulated environment
@@ -700,8 +810,30 @@ if [ "$BATCH_IMPORT" = "true" ]; then
700 src="${BATCH_PATHS[$i]}" 810 src="${BATCH_PATHS[$i]}"
701 dest="$BATCH_INPUT_DIR/$i" 811 dest="$BATCH_INPUT_DIR/$i"
702 log "DEBUG" "Copying $src -> $dest" 812 log "DEBUG" "Copying $src -> $dest"
703 # Use cp -rL to dereference symlinks (OCI containers often use hardlinks) 813
704 cp -rL "$src" "$dest" 814 # Check for multi-architecture OCI Image Index
815 if is_oci_image_index "$src"; then
816 available_platforms=$(get_oci_platforms "$src")
817 log "INFO" "Multi-arch OCI detected: $src (platforms: $available_platforms)"
818
819 # Select manifest for target architecture
820 manifest_digest=$(select_platform_manifest "$src" "$TARGET_ARCH")
821 if [ -z "$manifest_digest" ]; then
822 log "ERROR" "Architecture $TARGET_ARCH not found in multi-arch image: $src"
823 log "ERROR" "Available platforms: $available_platforms"
824 exit 1
825 fi
826
827 log "INFO" "Selected platform $(normalize_arch_to_oci "$TARGET_ARCH") from multi-arch image"
828
829 # Extract single-platform OCI instead of copying full multi-arch
830 mkdir -p "$dest"
831 extract_platform_oci "$src" "$dest" "$manifest_digest"
832 else
833 # Single-arch OCI - copy as-is
834 # Use cp -rL to dereference symlinks (OCI containers often use hardlinks)
835 cp -rL "$src" "$dest"
836 fi
705 done 837 done
706 838
707 # Override INPUT_PATH to point to combined directory 839 # Override INPUT_PATH to point to combined directory
@@ -1069,6 +1201,16 @@ else
1069 QEMU_OPTS="$QEMU_OPTS -nic none" 1201 QEMU_OPTS="$QEMU_OPTS -nic none"
1070fi 1202fi
1071 1203
1204# Batch-import mode: add virtio-9p for fast output (instead of slow console base64)
1205if [ "$BATCH_IMPORT" = "true" ]; then
1206 BATCH_SHARE_DIR="$TEMP_DIR/share"
1207 mkdir -p "$BATCH_SHARE_DIR"
1208 SHARE_TAG="${TOOL_NAME}_share"
1209 QEMU_OPTS="$QEMU_OPTS -virtfs local,path=$BATCH_SHARE_DIR,mount_tag=$SHARE_TAG,security_model=none,id=$SHARE_TAG"
1210 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1"
1211 log "INFO" "Using virtio-9p for fast storage output"
1212fi
1213
1072# Daemon mode: add virtio-serial for command channel 1214# Daemon mode: add virtio-serial for command channel
1073if [ "$DAEMON_MODE" = "start" ]; then 1215if [ "$DAEMON_MODE" = "start" ]; then
1074 # Check for required tools 1216 # Check for required tools
@@ -1272,7 +1414,8 @@ for i in $(seq 1 $TIMEOUT); do
1272 fi 1414 fi
1273 ;; 1415 ;;
1274 storage) 1416 storage)
1275 if grep -q "===STORAGE_END===" "$QEMU_OUTPUT" 2>/dev/null; then 1417 # Check for both console (STORAGE_END) and virtio-9p (9P_STORAGE_DONE) markers
1418 if grep -qE "===STORAGE_END===|===9P_STORAGE_DONE===" "$QEMU_OUTPUT" 2>/dev/null; then
1276 COMPLETE=true 1419 COMPLETE=true
1277 break 1420 break
1278 fi 1421 fi
@@ -1355,33 +1498,41 @@ if [ "$COMPLETE" = "true" ]; then
1355 1498
1356 storage) 1499 storage)
1357 log "INFO" "Extracting storage..." 1500 log "INFO" "Extracting storage..."
1358 # Use awk for precise extraction: capture lines between markers (not including markers) 1501
1359 # This avoids grep -v "===" which could accidentally remove valid base64 lines 1502 # Check for virtio-9p shared directory first (fast path)
1360 # Pipeline: 1503 if [ -n "$BATCH_SHARE_DIR" ] && [ -f "$BATCH_SHARE_DIR/storage.tar" ]; then
1361 # 1. awk: extract lines between STORAGE_START and STORAGE_END markers 1504 log "INFO" "Using virtio-9p storage output (fast path)"
1362 # 2. tr -d '\r': remove carriage returns 1505 cp "$BATCH_SHARE_DIR/storage.tar" "$OUTPUT_FILE"
1363 # 3. sed: remove ANSI escape codes 1506 else
1364 # 4. grep -v: remove kernel log messages (lines starting with [ followed by timestamp) 1507 # Fallback: extract from console base64 (slow path)
1365 # 5. tr -cd: keep only valid base64 characters 1508 log "INFO" "Using console base64 output (slow path)"
1366 awk '/===STORAGE_START===/{capture=1; next} /===STORAGE_END===/{capture=0} capture' "$QEMU_OUTPUT" | \ 1509 # Use awk for precise extraction: capture lines between markers (not including markers)
1367 tr -d '\r' | \ 1510 # Pipeline:
1368 sed 's/\x1b\[[0-9;]*m//g' | \ 1511 # 1. awk: extract lines between STORAGE_START and STORAGE_END markers
1369 grep -v '^\[[[:space:]]*[0-9]' | \ 1512 # 2. tr -d '\r': remove carriage returns
1370 tr -cd 'A-Za-z0-9+/=\n' > "${TEMP_DIR}/storage_b64.txt" 1513 # 3. sed: remove ANSI escape codes
1371 1514 # 4. grep -v: remove kernel log messages (lines starting with [ followed by timestamp)
1372 B64_SIZE=$(wc -c < "${TEMP_DIR}/storage_b64.txt") 1515 # 5. tr -cd: keep only valid base64 characters
1373 log "DEBUG" "Base64 data extracted: $B64_SIZE bytes" 1516 awk '/===STORAGE_START===/{capture=1; next} /===STORAGE_END===/{capture=0} capture' "$QEMU_OUTPUT" | \
1374 1517 tr -d '\r' | \
1375 # Decode with error reporting (not suppressed) 1518 sed 's/\x1b\[[0-9;]*m//g' | \
1376 if ! base64 -d < "${TEMP_DIR}/storage_b64.txt" > "$OUTPUT_FILE" 2>"${TEMP_DIR}/b64_errors.txt"; then 1519 grep -v '^\[[[:space:]]*[0-9]' | \
1377 log "ERROR" "Base64 decode failed" 1520 tr -cd 'A-Za-z0-9+/=\n' > "${TEMP_DIR}/storage_b64.txt"
1378 if [ -s "${TEMP_DIR}/b64_errors.txt" ]; then 1521
1379 log "ERROR" "Decode errors: $(cat "${TEMP_DIR}/b64_errors.txt")" 1522 B64_SIZE=$(wc -c < "${TEMP_DIR}/storage_b64.txt")
1523 log "DEBUG" "Base64 data extracted: $B64_SIZE bytes"
1524
1525 # Decode with error reporting (not suppressed)
1526 if ! base64 -d < "${TEMP_DIR}/storage_b64.txt" > "$OUTPUT_FILE" 2>"${TEMP_DIR}/b64_errors.txt"; then
1527 log "ERROR" "Base64 decode failed"
1528 if [ -s "${TEMP_DIR}/b64_errors.txt" ]; then
1529 log "ERROR" "Decode errors: $(cat "${TEMP_DIR}/b64_errors.txt")"
1530 fi
1531 # Show a sample of the base64 data for debugging
1532 log "DEBUG" "First 200 chars of base64: $(head -c 200 "${TEMP_DIR}/storage_b64.txt")"
1533 log "DEBUG" "Last 200 chars of base64: $(tail -c 200 "${TEMP_DIR}/storage_b64.txt")"
1534 exit 1
1380 fi 1535 fi
1381 # Show a sample of the base64 data for debugging
1382 log "DEBUG" "First 200 chars of base64: $(head -c 200 "${TEMP_DIR}/storage_b64.txt")"
1383 log "DEBUG" "Last 200 chars of base64: $(tail -c 200 "${TEMP_DIR}/storage_b64.txt")"
1384 exit 1
1385 fi 1536 fi
1386 1537
1387 DECODED_SIZE=$(wc -c < "$OUTPUT_FILE") 1538 DECODED_SIZE=$(wc -c < "$OUTPUT_FILE")