diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-15 21:50:06 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | 52ac90f1b29af4d0e6dc57ca9c947571f87872fd (patch) | |
| tree | a3d98ad0bf5ad5a1a4c9ee72c75e87661805e6d4 /recipes-containers/vcontainer | |
| parent | 929d1609efefd3189b650facaaeb3d2a13ffbe1d (diff) | |
| download | meta-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-x | recipes-containers/vcontainer/files/vcontainer-init-common.sh | 4 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vdkr-init.sh | 90 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vpdmn-init.sh | 31 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 209 |
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 | ||
| 297 | image_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 | ||
| 290 | transform_image_name() { | 315 | transform_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 |
| 485 | parse_cmdline | 532 | parse_cmdline |
| 486 | 533 | ||
| 534 | # Mount virtio-9p share if available (for fast storage output in batch-import mode) | ||
| 535 | if [ "$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 | ||
| 543 | fi | ||
| 544 | |||
| 487 | # Detect and configure disks | 545 | # Detect and configure disks |
| 488 | detect_disks | 546 | detect_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 |
| 155 | parse_cmdline | 165 | parse_cmdline |
| 156 | 166 | ||
| 167 | # Mount virtio-9p share if available (for fast storage output in batch-import mode) | ||
| 168 | if [ "$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 | ||
| 176 | fi | ||
| 177 | |||
| 157 | # Detect and configure disks | 178 | # Detect and configure disks |
| 158 | detect_disks | 179 | detect_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 | ||
| 111 | normalize_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 | ||
| 121 | is_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 | ||
| 128 | get_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) | ||
| 137 | select_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 | ||
| 174 | extract_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 | } | ||
| 210 | EOF | ||
| 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 | |||
| 106 | show_usage() { | 216 | show_usage() { |
| 107 | cat << 'EOF' | 217 | cat << 'EOF' |
| 108 | vrunner.sh - Execute docker commands in QEMU-emulated environment | 218 | vrunner.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" |
| 1070 | fi | 1202 | fi |
| 1071 | 1203 | ||
| 1204 | # Batch-import mode: add virtio-9p for fast output (instead of slow console base64) | ||
| 1205 | if [ "$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" | ||
| 1212 | fi | ||
| 1213 | |||
| 1072 | # Daemon mode: add virtio-serial for command channel | 1214 | # Daemon mode: add virtio-serial for command channel |
| 1073 | if [ "$DAEMON_MODE" = "start" ]; then | 1215 | if [ "$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") |
