diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-06 20:41:53 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | c32db56912b06a89490fda5d554468dbc12a39a2 (patch) | |
| tree | 8084bd9c1b56b13354cddf99b39356967f0142ea /recipes-containers/vcontainer | |
| parent | 4baa3503321fb2ad9edfebc5e89bcd37115e626e (diff) | |
| download | meta-virtualization-c32db56912b06a89490fda5d554468dbc12a39a2.tar.gz | |
vcontainer: add shared infrastructure and runner
Add core vcontainer infrastructure shared by vdkr and vpdmn:
Scripts:
- vrunner.sh: QEMU runner supporting both Docker and Podman runtimes
- vcontainer-common.sh: Shared CLI functions and command handling
- vcontainer-init-common.sh: Shared init script functions for QEMU guest
- vdkr-preinit.sh: Initramfs preinit for switch_root to squashfs overlay
Recipes:
- vcontainer-native: Installs vrunner.sh and shared scripts
- vcontainer-initramfs-create.inc: Shared initramfs build logic
Features:
- Docker/Podman-compatible commands: images, pull, load, save, run, exec
- Memory resident mode for fast command execution
- KVM acceleration when host matches target
- Interactive mode with volume mounts
- Squashfs rootfs with tmpfs overlay
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers/vcontainer')
| -rwxr-xr-x | recipes-containers/vcontainer/files/vcontainer-common.sh | 2004 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vcontainer-init-common.sh | 537 | ||||
| -rw-r--r-- | recipes-containers/vcontainer/files/vdkr-preinit.sh | 133 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 1353 | ||||
| -rw-r--r-- | recipes-containers/vcontainer/vcontainer-initramfs-create.inc | 237 | ||||
| -rw-r--r-- | recipes-containers/vcontainer/vcontainer-native.bb | 43 |
6 files changed, 4307 insertions, 0 deletions
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh new file mode 100755 index 00000000..cd76ec6c --- /dev/null +++ b/recipes-containers/vcontainer/files/vcontainer-common.sh | |||
| @@ -0,0 +1,2004 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | # vcontainer-common.sh: Shared code for vdkr and vpdmn CLI wrappers | ||
| 7 | # | ||
| 8 | # This file is sourced by vdkr.sh and vpdmn.sh after they set: | ||
| 9 | # VCONTAINER_RUNTIME_NAME - Tool name (vdkr or vpdmn) | ||
| 10 | # VCONTAINER_RUNTIME_CMD - Container command (docker or podman) | ||
| 11 | # VCONTAINER_RUNTIME_PREFIX - Env var prefix (VDKR or VPDMN) | ||
| 12 | # VCONTAINER_IMPORT_TARGET - skopeo target (docker-daemon: or containers-storage:) | ||
| 13 | # VCONTAINER_STATE_FILE - State image name (docker-state.img or podman-state.img) | ||
| 14 | # VCONTAINER_OTHER_PREFIX - Other tool's prefix for orphan checking (VPDMN or VDKR) | ||
| 15 | # VCONTAINER_VERSION - Tool version | ||
| 16 | |||
| 17 | set -e | ||
| 18 | |||
| 19 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 20 | |||
| 21 | # Validate required variables are set | ||
| 22 | : "${VCONTAINER_RUNTIME_NAME:?VCONTAINER_RUNTIME_NAME must be set}" | ||
| 23 | : "${VCONTAINER_RUNTIME_CMD:?VCONTAINER_RUNTIME_CMD must be set}" | ||
| 24 | : "${VCONTAINER_RUNTIME_PREFIX:?VCONTAINER_RUNTIME_PREFIX must be set}" | ||
| 25 | : "${VCONTAINER_IMPORT_TARGET:?VCONTAINER_IMPORT_TARGET must be set}" | ||
| 26 | : "${VCONTAINER_STATE_FILE:?VCONTAINER_STATE_FILE must be set}" | ||
| 27 | : "${VCONTAINER_OTHER_PREFIX:?VCONTAINER_OTHER_PREFIX must be set}" | ||
| 28 | : "${VCONTAINER_VERSION:?VCONTAINER_VERSION must be set}" | ||
| 29 | |||
| 30 | # ============================================================================ | ||
| 31 | # Configuration Management | ||
| 32 | # ============================================================================ | ||
| 33 | # Config directory can be set via: | ||
| 34 | # 1. --config-dir command line option | ||
| 35 | # 2. ${VCONTAINER_RUNTIME_PREFIX}_CONFIG_DIR environment variable | ||
| 36 | # 3. Default: ~/.config/${VCONTAINER_RUNTIME_NAME} | ||
| 37 | # | ||
| 38 | # Config file format: key=value (one per line) | ||
| 39 | # Supported keys: arch, timeout, state-dir, verbose | ||
| 40 | # ============================================================================ | ||
| 41 | |||
| 42 | # Pre-parse --config-dir from command line (needs to happen before detect_default_arch) | ||
| 43 | _preparse_config_dir() { | ||
| 44 | local i=1 | ||
| 45 | while [ $i -le $# ]; do | ||
| 46 | local arg="${!i}" | ||
| 47 | case "$arg" in | ||
| 48 | --config-dir) | ||
| 49 | i=$((i + 1)) | ||
| 50 | echo "${!i}" | ||
| 51 | return | ||
| 52 | ;; | ||
| 53 | --config-dir=*) | ||
| 54 | echo "${arg#--config-dir=}" | ||
| 55 | return | ||
| 56 | ;; | ||
| 57 | esac | ||
| 58 | i=$((i + 1)) | ||
| 59 | done | ||
| 60 | echo "" | ||
| 61 | } | ||
| 62 | |||
| 63 | _PREPARSE_CONFIG_DIR=$(_preparse_config_dir "$@") | ||
| 64 | |||
| 65 | # Get environment variable value dynamically | ||
| 66 | _get_env_var() { | ||
| 67 | local var_name="${VCONTAINER_RUNTIME_PREFIX}_$1" | ||
| 68 | echo "${!var_name}" | ||
| 69 | } | ||
| 70 | |||
| 71 | CONFIG_DIR="${_PREPARSE_CONFIG_DIR:-$(_get_env_var CONFIG_DIR)}" | ||
| 72 | [ -z "$CONFIG_DIR" ] && CONFIG_DIR="$HOME/.config/$VCONTAINER_RUNTIME_NAME" | ||
| 73 | CONFIG_FILE="$CONFIG_DIR/config" | ||
| 74 | |||
| 75 | # Read a config value | ||
| 76 | # Usage: config_get <key> [default] | ||
| 77 | config_get() { | ||
| 78 | local key="$1" | ||
| 79 | local default="$2" | ||
| 80 | |||
| 81 | if [ -f "$CONFIG_FILE" ]; then | ||
| 82 | local value=$(grep "^${key}=" "$CONFIG_FILE" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]') | ||
| 83 | if [ -n "$value" ]; then | ||
| 84 | echo "$value" | ||
| 85 | return | ||
| 86 | fi | ||
| 87 | fi | ||
| 88 | echo "$default" | ||
| 89 | } | ||
| 90 | |||
| 91 | # Write a config value | ||
| 92 | # Usage: config_set <key> <value> | ||
| 93 | config_set() { | ||
| 94 | local key="$1" | ||
| 95 | local value="$2" | ||
| 96 | |||
| 97 | mkdir -p "$CONFIG_DIR" | ||
| 98 | |||
| 99 | if [ -f "$CONFIG_FILE" ]; then | ||
| 100 | # Remove existing key | ||
| 101 | grep -v "^${key}=" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" 2>/dev/null || true | ||
| 102 | mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" | ||
| 103 | fi | ||
| 104 | |||
| 105 | # Add new value | ||
| 106 | echo "${key}=${value}" >> "$CONFIG_FILE" | ||
| 107 | } | ||
| 108 | |||
| 109 | # Remove a config value | ||
| 110 | # Usage: config_unset <key> | ||
| 111 | config_unset() { | ||
| 112 | local key="$1" | ||
| 113 | |||
| 114 | if [ -f "$CONFIG_FILE" ]; then | ||
| 115 | grep -v "^${key}=" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" 2>/dev/null || true | ||
| 116 | mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" | ||
| 117 | fi | ||
| 118 | } | ||
| 119 | |||
| 120 | # List all config values | ||
| 121 | config_list() { | ||
| 122 | if [ -f "$CONFIG_FILE" ]; then | ||
| 123 | cat "$CONFIG_FILE" | ||
| 124 | fi | ||
| 125 | } | ||
| 126 | |||
| 127 | # Get config default value | ||
| 128 | config_default() { | ||
| 129 | local key="$1" | ||
| 130 | case "$key" in | ||
| 131 | arch) uname -m ;; | ||
| 132 | timeout) echo "300" ;; | ||
| 133 | state-dir) echo "$HOME/.$VCONTAINER_RUNTIME_NAME" ;; | ||
| 134 | verbose) echo "false" ;; | ||
| 135 | *) echo "" ;; | ||
| 136 | esac | ||
| 137 | } | ||
| 138 | |||
| 139 | # ============================================================================ | ||
| 140 | # Architecture Detection | ||
| 141 | # ============================================================================ | ||
| 142 | # Priority order: | ||
| 143 | # 1. --arch / -a command line flag (parsed below) | ||
| 144 | # 2. Executable name: ${name}-aarch64 -> aarch64, ${name}-x86_64 -> x86_64 | ||
| 145 | # 3. ${PREFIX}_ARCH environment variable | ||
| 146 | # 4. Config file: $CONFIG_DIR/config (arch key) | ||
| 147 | # 5. Legacy config file: $CONFIG_DIR/arch (for backwards compatibility) | ||
| 148 | # 6. Host architecture (uname -m) | ||
| 149 | # ============================================================================ | ||
| 150 | |||
| 151 | detect_arch_from_name() { | ||
| 152 | local prog_name=$(basename "$0") | ||
| 153 | case "$prog_name" in | ||
| 154 | ${VCONTAINER_RUNTIME_NAME}-aarch64) echo "aarch64" ;; | ||
| 155 | ${VCONTAINER_RUNTIME_NAME}-x86_64) echo "x86_64" ;; | ||
| 156 | *) echo "" ;; | ||
| 157 | esac | ||
| 158 | } | ||
| 159 | |||
| 160 | detect_default_arch() { | ||
| 161 | # Check executable name first | ||
| 162 | local name_arch=$(detect_arch_from_name) | ||
| 163 | if [ -n "$name_arch" ]; then | ||
| 164 | echo "$name_arch" | ||
| 165 | return | ||
| 166 | fi | ||
| 167 | |||
| 168 | # Check environment variable | ||
| 169 | local env_arch=$(_get_env_var ARCH) | ||
| 170 | if [ -n "$env_arch" ]; then | ||
| 171 | echo "$env_arch" | ||
| 172 | return | ||
| 173 | fi | ||
| 174 | |||
| 175 | # Check new config file (arch key) | ||
| 176 | local config_arch=$(config_get "arch" "") | ||
| 177 | if [ -n "$config_arch" ]; then | ||
| 178 | echo "$config_arch" | ||
| 179 | return | ||
| 180 | fi | ||
| 181 | |||
| 182 | # Check legacy config file for backwards compatibility | ||
| 183 | local legacy_file="$CONFIG_DIR/arch" | ||
| 184 | if [ -f "$legacy_file" ]; then | ||
| 185 | local legacy_arch=$(cat "$legacy_file" | tr -d '[:space:]') | ||
| 186 | if [ -n "$legacy_arch" ]; then | ||
| 187 | echo "$legacy_arch" | ||
| 188 | return | ||
| 189 | fi | ||
| 190 | fi | ||
| 191 | |||
| 192 | # Fall back to host architecture | ||
| 193 | uname -m | ||
| 194 | } | ||
| 195 | |||
| 196 | DEFAULT_ARCH=$(detect_default_arch) | ||
| 197 | BLOB_DIR="$(_get_env_var BLOB_DIR)" | ||
| 198 | VERBOSE="$(_get_env_var VERBOSE)" | ||
| 199 | [ -z "$VERBOSE" ] && VERBOSE="false" | ||
| 200 | STATELESS="$(_get_env_var STATELESS)" | ||
| 201 | [ -z "$STATELESS" ] && STATELESS="false" | ||
| 202 | |||
| 203 | # Default state directory (per-architecture) | ||
| 204 | DEFAULT_STATE_DIR="$(_get_env_var STATE_DIR)" | ||
| 205 | [ -z "$DEFAULT_STATE_DIR" ] && DEFAULT_STATE_DIR="$HOME/.$VCONTAINER_RUNTIME_NAME" | ||
| 206 | |||
| 207 | # Other tool's state directory (for orphan checking) | ||
| 208 | OTHER_STATE_DIR="$HOME/.$(echo $VCONTAINER_OTHER_PREFIX | tr 'A-Z' 'a-z')" | ||
| 209 | |||
| 210 | # Runner script | ||
| 211 | RUNNER="$(_get_env_var RUNNER)" | ||
| 212 | [ -z "$RUNNER" ] && RUNNER="$SCRIPT_DIR/vrunner.sh" | ||
| 213 | |||
| 214 | # Colors (use $'...' for proper escape interpretation) | ||
| 215 | RED=$'\033[0;31m' | ||
| 216 | GREEN=$'\033[0;32m' | ||
| 217 | YELLOW=$'\033[0;33m' | ||
| 218 | BLUE=$'\033[0;34m' | ||
| 219 | CYAN=$'\033[0;36m' | ||
| 220 | BOLD=$'\033[1m' | ||
| 221 | NC=$'\033[0m' | ||
| 222 | |||
| 223 | # Check OCI image architecture and warn/error if mismatched | ||
| 224 | # Usage: check_oci_arch <oci_dir> <target_arch> | ||
| 225 | # Returns: 0 if match or non-OCI, 1 if mismatch | ||
| 226 | check_oci_arch() { | ||
| 227 | local oci_dir="$1" | ||
| 228 | local target_arch="$2" | ||
| 229 | |||
| 230 | # Only check OCI directories | ||
| 231 | if [ ! -f "$oci_dir/index.json" ]; then | ||
| 232 | return 0 | ||
| 233 | fi | ||
| 234 | |||
| 235 | # Try to extract architecture from the OCI image | ||
| 236 | # OCI structure: index.json -> manifest -> config blob -> architecture | ||
| 237 | local image_arch="" | ||
| 238 | |||
| 239 | # First, get the manifest digest from index.json | ||
| 240 | local manifest_digest=$(cat "$oci_dir/index.json" 2>/dev/null | \ | ||
| 241 | grep -o '"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' | head -1 | \ | ||
| 242 | sed 's/.*sha256:\([a-f0-9]*\)".*/\1/') | ||
| 243 | |||
| 244 | if [ -n "$manifest_digest" ]; then | ||
| 245 | local manifest_file="$oci_dir/blobs/sha256/$manifest_digest" | ||
| 246 | if [ -f "$manifest_file" ]; then | ||
| 247 | # Get the config digest from manifest | ||
| 248 | local config_digest=$(cat "$manifest_file" 2>/dev/null | \ | ||
| 249 | grep -o '"config"[[:space:]]*:[[:space:]]*{[^}]*"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' | \ | ||
| 250 | sed 's/.*sha256:\([a-f0-9]*\)".*/\1/') | ||
| 251 | |||
| 252 | if [ -n "$config_digest" ]; then | ||
| 253 | local config_file="$oci_dir/blobs/sha256/$config_digest" | ||
| 254 | if [ -f "$config_file" ]; then | ||
| 255 | # Extract architecture from config | ||
| 256 | image_arch=$(cat "$config_file" 2>/dev/null | \ | ||
| 257 | grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | \ | ||
| 258 | sed 's/.*"\([^"]*\)"$/\1/') | ||
| 259 | fi | ||
| 260 | fi | ||
| 261 | fi | ||
| 262 | fi | ||
| 263 | |||
| 264 | if [ -z "$image_arch" ]; then | ||
| 265 | # Couldn't determine architecture, allow import with warning | ||
| 266 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Warning: Could not determine image architecture" >&2 | ||
| 267 | return 0 | ||
| 268 | fi | ||
| 269 | |||
| 270 | # Normalize architecture names | ||
| 271 | local normalized_image_arch="$image_arch" | ||
| 272 | local normalized_target_arch="$target_arch" | ||
| 273 | |||
| 274 | case "$image_arch" in | ||
| 275 | arm64) normalized_image_arch="aarch64" ;; | ||
| 276 | amd64) normalized_image_arch="x86_64" ;; | ||
| 277 | esac | ||
| 278 | |||
| 279 | case "$target_arch" in | ||
| 280 | arm64) normalized_target_arch="aarch64" ;; | ||
| 281 | amd64) normalized_target_arch="x86_64" ;; | ||
| 282 | esac | ||
| 283 | |||
| 284 | if [ "$normalized_image_arch" != "$normalized_target_arch" ]; then | ||
| 285 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture mismatch!" >&2 | ||
| 286 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Image architecture: ${BOLD}$image_arch${NC}" >&2 | ||
| 287 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Target architecture: ${BOLD}$target_arch${NC}" >&2 | ||
| 288 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Use --arch $image_arch to import to a matching environment" >&2 | ||
| 289 | return 1 | ||
| 290 | fi | ||
| 291 | |||
| 292 | [ "$VERBOSE" = "true" ] && echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Image architecture: $image_arch (matches target)" >&2 | ||
| 293 | return 0 | ||
| 294 | } | ||
| 295 | |||
| 296 | show_usage() { | ||
| 297 | local PROG_NAME=$(basename "$0") | ||
| 298 | local RUNTIME_UPPER=$(echo "$VCONTAINER_RUNTIME_CMD" | sed 's/./\U&/') | ||
| 299 | cat << EOF | ||
| 300 | ${BOLD}${PROG_NAME}${NC} v$VCONTAINER_VERSION - ${RUNTIME_UPPER} CLI for cross-architecture emulation | ||
| 301 | |||
| 302 | ${BOLD}USAGE:${NC} | ||
| 303 | ${PROG_NAME} [OPTIONS] <command> [args...] | ||
| 304 | |||
| 305 | ${BOLD}${RUNTIME_UPPER}-COMPATIBLE COMMANDS:${NC} | ||
| 306 | ${BOLD}Images:${NC} | ||
| 307 | ${CYAN}images${NC} List images in emulated ${RUNTIME_UPPER} | ||
| 308 | ${CYAN}pull${NC} <image> Pull image from registry | ||
| 309 | ${CYAN}load${NC} -i <file> Load ${RUNTIME_UPPER} image archive (${VCONTAINER_RUNTIME_CMD} save output) | ||
| 310 | ${CYAN}import${NC} <tarball> [name:tag] Import rootfs tarball as image | ||
| 311 | ${CYAN}save${NC} -o <file> <image> Save image to tar archive | ||
| 312 | ${CYAN}tag${NC} <source> <target> Tag an image | ||
| 313 | ${CYAN}rmi${NC} <image> Remove an image | ||
| 314 | ${CYAN}history${NC} <image> Show image layer history | ||
| 315 | ${CYAN}inspect${NC} <image|container> Display detailed info | ||
| 316 | |||
| 317 | ${BOLD}Containers:${NC} | ||
| 318 | ${CYAN}run${NC} [opts] <image> [cmd] Run a command in a new container | ||
| 319 | ${CYAN}ps${NC} [options] List containers | ||
| 320 | ${CYAN}rm${NC} <container> Remove a container | ||
| 321 | ${CYAN}logs${NC} <container> View container logs | ||
| 322 | ${CYAN}start${NC} <container> Start a stopped container | ||
| 323 | ${CYAN}stop${NC} <container> Stop a running container | ||
| 324 | ${CYAN}restart${NC} <container> Restart a container | ||
| 325 | ${CYAN}kill${NC} <container> Kill a running container | ||
| 326 | ${CYAN}pause${NC} <container> Pause a running container | ||
| 327 | ${CYAN}unpause${NC} <container> Unpause a container | ||
| 328 | ${CYAN}commit${NC} <container> <image> Create image from container | ||
| 329 | ${CYAN}exec${NC} [opts] <container> <cmd> Execute command in container | ||
| 330 | ${CYAN}cp${NC} <src> <dest> Copy files to/from container | ||
| 331 | |||
| 332 | ${BOLD}Registry:${NC} | ||
| 333 | ${CYAN}login${NC} [options] Log in to a registry | ||
| 334 | ${CYAN}logout${NC} [registry] Log out from a registry | ||
| 335 | ${CYAN}push${NC} <image> Push image to registry | ||
| 336 | ${CYAN}search${NC} <term> Search registries for images | ||
| 337 | |||
| 338 | ${BOLD}System:${NC} | ||
| 339 | ${CYAN}info${NC} Display system info | ||
| 340 | ${CYAN}version${NC} Show ${RUNTIME_UPPER} version | ||
| 341 | ${CYAN}system df${NC} Show disk usage of images/containers/volumes | ||
| 342 | ${CYAN}system prune${NC} Remove unused data | ||
| 343 | ${CYAN}system prune -a${NC} Remove all unused images | ||
| 344 | |||
| 345 | ${BOLD}EXTENDED COMMANDS (${VCONTAINER_RUNTIME_NAME}-specific):${NC} | ||
| 346 | ${CYAN}vimport${NC} <path> [name:tag] Import from OCI dir, tarball, or directory (auto-detect) | ||
| 347 | ${CYAN}vrun${NC} [opts] <image> [cmd] Run command, clearing entrypoint (see RUN vs VRUN below) | ||
| 348 | ${CYAN}vstorage${NC} List all storage directories (alias: vstorage list) | ||
| 349 | ${CYAN}vstorage list${NC} List all storage directories with details | ||
| 350 | ${CYAN}vstorage path [arch]${NC} Show path to storage directory | ||
| 351 | ${CYAN}vstorage df${NC} Show detailed disk usage breakdown | ||
| 352 | ${CYAN}vstorage clean [arch|--all]${NC} Clean storage directories (stops memres first) | ||
| 353 | ${CYAN}clean${NC} ${YELLOW}[DEPRECATED]${NC} Use 'vstorage clean' instead | ||
| 354 | |||
| 355 | ${BOLD}MEMORY RESIDENT MODE (vmemres):${NC} | ||
| 356 | ${CYAN}vmemres start${NC} [-p port:port] Start memory resident VM in background | ||
| 357 | ${CYAN}vmemres stop${NC} Stop memory resident VM | ||
| 358 | ${CYAN}vmemres restart${NC} [--clean] Restart VM (optionally clean state first) | ||
| 359 | ${CYAN}vmemres status${NC} Show memory resident VM status | ||
| 360 | ${CYAN}vmemres list${NC} List all running memres instances | ||
| 361 | (Note: 'memres' also works as an alias for 'vmemres') | ||
| 362 | |||
| 363 | Port forwarding with vmemres: | ||
| 364 | -p <host_port>:<container_port>[/protocol] | ||
| 365 | Forward host port to container port (protocol: tcp or udp, default: tcp) | ||
| 366 | Multiple -p options can be specified | ||
| 367 | |||
| 368 | ${BOLD}RUN vs VRUN:${NC} | ||
| 369 | ${CYAN}run${NC} - Full ${RUNTIME_UPPER} passthrough. Entrypoint is honored. | ||
| 370 | Command args are passed TO the entrypoint. | ||
| 371 | Example: run alpine /bin/sh -> entrypoint receives '/bin/sh' as arg | ||
| 372 | ${CYAN}vrun${NC} - Convenience wrapper. Clears entrypoint when command given. | ||
| 373 | Command args become the container's command directly. | ||
| 374 | Example: vrun alpine /bin/sh -> runs /bin/sh as PID 1 | ||
| 375 | |||
| 376 | Use 'run' when you need --entrypoint, -e, --rm, or other ${VCONTAINER_RUNTIME_CMD} options. | ||
| 377 | Use 'vrun' for simple "run this command in image" cases. | ||
| 378 | |||
| 379 | ${BOLD}CONFIGURATION (vconfig):${NC} | ||
| 380 | ${CYAN}vconfig${NC} Show all configuration values | ||
| 381 | ${CYAN}vconfig${NC} <key> Get configuration value | ||
| 382 | ${CYAN}vconfig${NC} <key> <value> Set configuration value | ||
| 383 | ${CYAN}vconfig${NC} <key> --reset Reset to default value | ||
| 384 | |||
| 385 | Supported keys: arch, timeout, state-dir, verbose | ||
| 386 | Config file: \$CONFIG_DIR/config (default: ~/.config/${VCONTAINER_RUNTIME_NAME}/config) | ||
| 387 | |||
| 388 | ${BOLD}GLOBAL OPTIONS:${NC} | ||
| 389 | --arch, -a <arch> Target architecture: x86_64 or aarch64 [default: ${DEFAULT_ARCH}] | ||
| 390 | --config-dir <path> Configuration directory [default: ~/.config/${VCONTAINER_RUNTIME_NAME}] | ||
| 391 | --instance, -I <name> Use named instance (shortcut for --state-dir ~/.$VCONTAINER_RUNTIME_NAME/<name>) | ||
| 392 | --blob-dir <path> Path to kernel/initramfs blobs (override default) | ||
| 393 | --stateless Start with fresh ${RUNTIME_UPPER} state (no persistence) | ||
| 394 | --state-dir <path> Override state directory [default: ~/.$VCONTAINER_RUNTIME_NAME/<arch>] | ||
| 395 | --storage <file> Export ${VCONTAINER_RUNTIME_CMD} storage after command (tar file) | ||
| 396 | --input-storage <tar> Load ${RUNTIME_UPPER} state from tar before command | ||
| 397 | --no-kvm Disable KVM acceleration (use TCG emulation) | ||
| 398 | --verbose, -v Enable verbose output | ||
| 399 | --help, -h Show this help | ||
| 400 | |||
| 401 | ${BOLD}${RUNTIME_UPPER} RUN/VRUN OPTIONS:${NC} | ||
| 402 | All ${VCONTAINER_RUNTIME_CMD} run options are passed through (e.g., -it, -e, -p, --rm, etc.) | ||
| 403 | Interactive mode (-it) automatically handles daemon stop/restart | ||
| 404 | -v <host>:<container>[:mode] Mount host path in container (requires vmemres) | ||
| 405 | mode: ro (read-only) or rw (read-write, default) | ||
| 406 | |||
| 407 | ${BOLD}EXAMPLES:${NC} | ||
| 408 | # List images (uses persistent state by default) | ||
| 409 | ${PROG_NAME} images | ||
| 410 | |||
| 411 | # Import rootfs tarball (matches '${VCONTAINER_RUNTIME_CMD} import' exactly) | ||
| 412 | ${PROG_NAME} import rootfs.tar myapp:latest | ||
| 413 | |||
| 414 | # Import OCI directory (extended command, auto-detects format) | ||
| 415 | ${PROG_NAME} vimport ./container-oci/ myapp:latest | ||
| 416 | ${PROG_NAME} images # Image persists! | ||
| 417 | |||
| 418 | # Save image to tar archive | ||
| 419 | ${PROG_NAME} save -o myapp.tar myapp:latest | ||
| 420 | |||
| 421 | # Load a ${RUNTIME_UPPER} image archive (from '${VCONTAINER_RUNTIME_CMD} save') | ||
| 422 | ${PROG_NAME} load -i myapp.tar | ||
| 423 | |||
| 424 | # Start fresh (ignore existing state) | ||
| 425 | ${PROG_NAME} --stateless images | ||
| 426 | |||
| 427 | # Export storage for deployment to target | ||
| 428 | ${PROG_NAME} --storage /tmp/${VCONTAINER_RUNTIME_CMD}-storage.tar vimport ./container-oci/ myapp:latest | ||
| 429 | |||
| 430 | # Run a command in a container (${VCONTAINER_RUNTIME_CMD}-compatible syntax) | ||
| 431 | ${PROG_NAME} run alpine /bin/echo hello | ||
| 432 | ${PROG_NAME} run --rm alpine uname -m # Check container architecture | ||
| 433 | |||
| 434 | # Interactive shell (${VCONTAINER_RUNTIME_CMD}-compatible syntax) | ||
| 435 | ${PROG_NAME} run -it alpine /bin/sh | ||
| 436 | |||
| 437 | # With environment variables and other ${VCONTAINER_RUNTIME_CMD} options | ||
| 438 | ${PROG_NAME} run --rm -e FOO=bar myapp:latest | ||
| 439 | ${PROG_NAME} run -it -p 8080:80 nginx:latest | ||
| 440 | |||
| 441 | # Pull an image from a registry | ||
| 442 | ${PROG_NAME} pull alpine:latest | ||
| 443 | |||
| 444 | # vrun: convenience wrapper (clears entrypoint when command given) | ||
| 445 | ${PROG_NAME} vrun myapp:latest /bin/ls -la # Runs /bin/ls directly, not via entrypoint | ||
| 446 | |||
| 447 | # Volume mounts (requires memres to be running) | ||
| 448 | ${PROG_NAME} memres start | ||
| 449 | ${PROG_NAME} vrun -v /tmp/data:/data alpine cat /data/file.txt | ||
| 450 | ${PROG_NAME} vrun -v /home/user/src:/src:ro alpine ls /src | ||
| 451 | ${PROG_NAME} run -v ./local:/app --rm myapp:latest /app/run.sh | ||
| 452 | |||
| 453 | # Port forwarding (web server) | ||
| 454 | ${PROG_NAME} memres start -p 8080:80 # Forward host:8080 to guest:80 | ||
| 455 | ${PROG_NAME} run -d --rm --network=host nginx:alpine # Container uses host network | ||
| 456 | curl http://localhost:8080 # Access nginx from host | ||
| 457 | |||
| 458 | # Port forwarding (SSH into a container) | ||
| 459 | ${PROG_NAME} memres start -p 2222:22 # Forward host:2222 to guest:22 | ||
| 460 | ${PROG_NAME} run -d --network=host my-ssh-image # Container with SSH server | ||
| 461 | ssh -p 2222 localhost # SSH from host into container | ||
| 462 | |||
| 463 | # Multiple instances with different ports | ||
| 464 | ${PROG_NAME} memres list # Show running instances | ||
| 465 | ${PROG_NAME} -I web memres start -p 8080:80 # Start named instance | ||
| 466 | ${PROG_NAME} -I web images # Use named instance | ||
| 467 | ${PROG_NAME} -I backend run -d --network=host my-api:latest | ||
| 468 | |||
| 469 | ${BOLD}NOTES:${NC} | ||
| 470 | - Architecture detection (in priority order): | ||
| 471 | 1. --arch / -a flag | ||
| 472 | 2. Executable name (${VCONTAINER_RUNTIME_NAME}-aarch64 or ${VCONTAINER_RUNTIME_NAME}-x86_64) | ||
| 473 | 3. ${VCONTAINER_RUNTIME_PREFIX}_ARCH environment variable | ||
| 474 | 4. Config file: ~/.config/${VCONTAINER_RUNTIME_NAME}/arch | ||
| 475 | 5. Host architecture (uname -m) | ||
| 476 | - Current architecture: ${DEFAULT_ARCH} | ||
| 477 | - State persists in ~/.$VCONTAINER_RUNTIME_NAME/<arch>/ | ||
| 478 | - Use --stateless for fresh ${RUNTIME_UPPER} state each run | ||
| 479 | - Use --storage to export ${RUNTIME_UPPER} storage to tar file | ||
| 480 | - run vs vrun: | ||
| 481 | run = exact ${VCONTAINER_RUNTIME_CMD} run syntax (entrypoint honored) | ||
| 482 | vrun = clears entrypoint when command given (runs command directly) | ||
| 483 | |||
| 484 | ${BOLD}ENVIRONMENT:${NC} | ||
| 485 | ${VCONTAINER_RUNTIME_PREFIX}_BLOB_DIR Path to kernel/initramfs blobs | ||
| 486 | ${VCONTAINER_RUNTIME_PREFIX}_STATE_DIR Base directory for state [default: ~/.$VCONTAINER_RUNTIME_NAME] | ||
| 487 | ${VCONTAINER_RUNTIME_PREFIX}_STATELESS Run stateless by default (true/false) | ||
| 488 | ${VCONTAINER_RUNTIME_PREFIX}_VERBOSE Enable verbose output (true/false) | ||
| 489 | |||
| 490 | EOF | ||
| 491 | } | ||
| 492 | |||
| 493 | # Build runner args | ||
| 494 | build_runner_args() { | ||
| 495 | local args=() | ||
| 496 | |||
| 497 | # Specify runtime (docker for vdkr, podman for vpdmn) | ||
| 498 | args+=("--runtime" "$VCONTAINER_RUNTIME_CMD") | ||
| 499 | args+=("--arch" "$TARGET_ARCH") | ||
| 500 | |||
| 501 | [ -n "$BLOB_DIR" ] && args+=("--blob-dir" "$BLOB_DIR") | ||
| 502 | [ "$VERBOSE" = "true" ] && args+=("--verbose") | ||
| 503 | [ "$NETWORK" = "true" ] && args+=("--network") | ||
| 504 | [ "$INTERACTIVE" = "true" ] && args+=("--interactive") | ||
| 505 | [ -n "$STORAGE_OUTPUT" ] && args+=("--output-type" "storage" "--output" "$STORAGE_OUTPUT") | ||
| 506 | [ -n "$STATE_DIR" ] && args+=("--state-dir" "$STATE_DIR") | ||
| 507 | [ -n "$INPUT_STORAGE" ] && args+=("--input-storage" "$INPUT_STORAGE") | ||
| 508 | [ "$DISABLE_KVM" = "true" ] && args+=("--no-kvm") | ||
| 509 | |||
| 510 | # Add port forwards (each -p adds a --port-forward) | ||
| 511 | for pf in "${PORT_FORWARDS[@]}"; do | ||
| 512 | args+=("--port-forward" "$pf") | ||
| 513 | done | ||
| 514 | |||
| 515 | echo "${args[@]}" | ||
| 516 | } | ||
| 517 | |||
| 518 | # Parse global options first | ||
| 519 | TARGET_ARCH="$DEFAULT_ARCH" | ||
| 520 | STORAGE_OUTPUT="" | ||
| 521 | STATE_DIR="" | ||
| 522 | INPUT_STORAGE="" | ||
| 523 | NETWORK="true" | ||
| 524 | INTERACTIVE="false" | ||
| 525 | PORT_FORWARDS=() | ||
| 526 | DISABLE_KVM="false" | ||
| 527 | COMMAND="" | ||
| 528 | COMMAND_ARGS=() | ||
| 529 | |||
| 530 | while [ $# -gt 0 ]; do | ||
| 531 | case $1 in | ||
| 532 | --arch|-a) | ||
| 533 | # Only parse as global option before command is set | ||
| 534 | if [ -z "$COMMAND" ]; then | ||
| 535 | TARGET_ARCH="$2" | ||
| 536 | shift 2 | ||
| 537 | else | ||
| 538 | COMMAND_ARGS+=("$1") | ||
| 539 | shift | ||
| 540 | fi | ||
| 541 | ;; | ||
| 542 | --blob-dir) | ||
| 543 | BLOB_DIR="$2" | ||
| 544 | shift 2 | ||
| 545 | ;; | ||
| 546 | --storage) | ||
| 547 | STORAGE_OUTPUT="$2" | ||
| 548 | shift 2 | ||
| 549 | ;; | ||
| 550 | --state-dir) | ||
| 551 | STATE_DIR="$2" | ||
| 552 | shift 2 | ||
| 553 | ;; | ||
| 554 | --instance|-I) | ||
| 555 | # Shortcut: -I web expands to --state-dir ~/.$VCONTAINER_RUNTIME_NAME/web | ||
| 556 | STATE_DIR="$DEFAULT_STATE_DIR/$2" | ||
| 557 | shift 2 | ||
| 558 | ;; | ||
| 559 | --config-dir) | ||
| 560 | # Already pre-parsed, just consume it | ||
| 561 | shift 2 | ||
| 562 | ;; | ||
| 563 | --config-dir=*) | ||
| 564 | # Already pre-parsed, just consume it | ||
| 565 | shift | ||
| 566 | ;; | ||
| 567 | --input-storage) | ||
| 568 | INPUT_STORAGE="$2" | ||
| 569 | shift 2 | ||
| 570 | ;; | ||
| 571 | --stateless) | ||
| 572 | STATELESS="true" | ||
| 573 | shift | ||
| 574 | ;; | ||
| 575 | --no-network) | ||
| 576 | NETWORK="false" | ||
| 577 | shift | ||
| 578 | ;; | ||
| 579 | --no-kvm) | ||
| 580 | DISABLE_KVM="true" | ||
| 581 | shift | ||
| 582 | ;; | ||
| 583 | -it|--interactive) | ||
| 584 | INTERACTIVE="true" | ||
| 585 | shift | ||
| 586 | ;; | ||
| 587 | -i) | ||
| 588 | # -i alone means interactive, but only before we have a command | ||
| 589 | # After a command, -i might be an argument (e.g., load -i file) | ||
| 590 | if [ -z "$COMMAND" ]; then | ||
| 591 | INTERACTIVE="true" | ||
| 592 | else | ||
| 593 | COMMAND_ARGS+=("$1") | ||
| 594 | fi | ||
| 595 | shift | ||
| 596 | ;; | ||
| 597 | -t) | ||
| 598 | # -t alone means interactive (allocate TTY) before command | ||
| 599 | # After command, -t might be an argument | ||
| 600 | if [ -z "$COMMAND" ]; then | ||
| 601 | INTERACTIVE="true" | ||
| 602 | else | ||
| 603 | COMMAND_ARGS+=("$1") | ||
| 604 | fi | ||
| 605 | shift | ||
| 606 | ;; | ||
| 607 | --verbose) | ||
| 608 | VERBOSE="true" | ||
| 609 | shift | ||
| 610 | ;; | ||
| 611 | -v) | ||
| 612 | # -v can mean verbose (before command) or volume (after command like run/vrun) | ||
| 613 | if [ -z "$COMMAND" ]; then | ||
| 614 | VERBOSE="true" | ||
| 615 | else | ||
| 616 | # After command, -v is likely a volume flag - pass to subcommand | ||
| 617 | COMMAND_ARGS+=("$1") | ||
| 618 | fi | ||
| 619 | shift | ||
| 620 | ;; | ||
| 621 | --help|-h) | ||
| 622 | show_usage | ||
| 623 | exit 0 | ||
| 624 | ;; | ||
| 625 | --version) | ||
| 626 | echo "$VCONTAINER_RUNTIME_NAME version $VCONTAINER_VERSION" | ||
| 627 | exit 0 | ||
| 628 | ;; | ||
| 629 | -*) | ||
| 630 | # Unknown option - might be for subcommand | ||
| 631 | COMMAND_ARGS+=("$1") | ||
| 632 | shift | ||
| 633 | ;; | ||
| 634 | *) | ||
| 635 | if [ -z "$COMMAND" ]; then | ||
| 636 | COMMAND="$1" | ||
| 637 | else | ||
| 638 | COMMAND_ARGS+=("$1") | ||
| 639 | fi | ||
| 640 | shift | ||
| 641 | ;; | ||
| 642 | esac | ||
| 643 | done | ||
| 644 | |||
| 645 | if [ -z "$COMMAND" ]; then | ||
| 646 | show_usage | ||
| 647 | exit 0 | ||
| 648 | fi | ||
| 649 | |||
| 650 | # Set up state directory (default to persistent unless --stateless) | ||
| 651 | if [ "$STATELESS" != "true" ] && [ -z "$STATE_DIR" ] && [ -z "$INPUT_STORAGE" ]; then | ||
| 652 | STATE_DIR="$DEFAULT_STATE_DIR/$TARGET_ARCH" | ||
| 653 | fi | ||
| 654 | |||
| 655 | # Check runner exists | ||
| 656 | if [ ! -x "$RUNNER" ]; then | ||
| 657 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Runner script not found: $RUNNER" >&2 | ||
| 658 | exit 1 | ||
| 659 | fi | ||
| 660 | |||
| 661 | # Helper function to check if daemon is running | ||
| 662 | daemon_is_running() { | ||
| 663 | # Use STATE_DIR if set, otherwise use default | ||
| 664 | local state_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" | ||
| 665 | local pid_file="$state_dir/daemon.pid" | ||
| 666 | if [ -f "$pid_file" ]; then | ||
| 667 | local pid=$(cat "$pid_file") | ||
| 668 | if [ -d "/proc/$pid" ]; then | ||
| 669 | return 0 | ||
| 670 | fi | ||
| 671 | fi | ||
| 672 | return 1 | ||
| 673 | } | ||
| 674 | |||
| 675 | # Helper function to run command via daemon or regular mode | ||
| 676 | run_runtime_command() { | ||
| 677 | local runtime_cmd="$1" | ||
| 678 | local runner_args=$(build_runner_args) | ||
| 679 | |||
| 680 | if daemon_is_running; then | ||
| 681 | # Use daemon mode - faster | ||
| 682 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon mode" >&2 | ||
| 683 | "$RUNNER" $runner_args --daemon-send "$runtime_cmd" | ||
| 684 | else | ||
| 685 | # Regular mode - start QEMU for this command | ||
| 686 | "$RUNNER" $runner_args -- "$runtime_cmd" | ||
| 687 | fi | ||
| 688 | } | ||
| 689 | |||
| 690 | # Helper function to run command with input | ||
| 691 | # Uses daemon mode with virtio-9p if daemon is running, otherwise regular mode | ||
| 692 | run_runtime_command_with_input() { | ||
| 693 | local input_path="$1" | ||
| 694 | local input_type="$2" | ||
| 695 | local runtime_cmd="$3" | ||
| 696 | local runner_args=$(build_runner_args) | ||
| 697 | |||
| 698 | if daemon_is_running; then | ||
| 699 | # Use daemon mode with virtio-9p shared directory | ||
| 700 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon mode for file I/O" >&2 | ||
| 701 | "$RUNNER" $runner_args --input "$input_path" --input-type "$input_type" --daemon-send-input -- "$runtime_cmd" | ||
| 702 | else | ||
| 703 | # Regular mode - start QEMU for this command | ||
| 704 | "$RUNNER" $runner_args --input "$input_path" --input-type "$input_type" -- "$runtime_cmd" | ||
| 705 | fi | ||
| 706 | } | ||
| 707 | |||
| 708 | # ============================================================================ | ||
| 709 | # Volume Mount Support | ||
| 710 | # ============================================================================ | ||
| 711 | # Volumes are copied to the share directory before running the container. | ||
| 712 | # Format: -v /host/path:/container/path[:ro|:rw] | ||
| 713 | # | ||
| 714 | # Implementation: | ||
| 715 | # - Copy host path to $SHARE_DIR/volumes/<hash>/ | ||
| 716 | # - Transform -v to use /mnt/share/volumes/<hash>:/container/path | ||
| 717 | # - After container exits, sync back for :rw mounts (default) | ||
| 718 | # | ||
| 719 | # Limitations: | ||
| 720 | # - Requires daemon mode (memres) for volume mounts | ||
| 721 | # - Changes in container are synced back after container exits (not real-time) | ||
| 722 | # - Large volumes may be slow to copy | ||
| 723 | # ============================================================================ | ||
| 724 | |||
| 725 | # Array to track volume mounts for cleanup/sync | ||
| 726 | declare -a VOLUME_MOUNTS=() | ||
| 727 | declare -a VOLUME_MODES=() | ||
| 728 | |||
| 729 | # Generate a short hash for volume directory naming | ||
| 730 | volume_hash() { | ||
| 731 | echo "$1" | md5sum | cut -c1-8 | ||
| 732 | } | ||
| 733 | |||
| 734 | # Global to receive result from prepare_volume (avoids subshell issue) | ||
| 735 | PREPARE_VOLUME_RESULT="" | ||
| 736 | |||
| 737 | # Prepare a volume mount: copy host path to share directory | ||
| 738 | # Sets PREPARE_VOLUME_RESULT to the guest path (avoids subshell issue with arrays) | ||
| 739 | prepare_volume() { | ||
| 740 | local host_path="$1" | ||
| 741 | local container_path="$2" | ||
| 742 | local mode="$3" # ro or rw (default: rw) | ||
| 743 | |||
| 744 | [ -z "$mode" ] && mode="rw" | ||
| 745 | PREPARE_VOLUME_RESULT="" | ||
| 746 | |||
| 747 | # Validate host path exists | ||
| 748 | if [ ! -e "$host_path" ]; then | ||
| 749 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume source not found: $host_path" >&2 | ||
| 750 | return 1 | ||
| 751 | fi | ||
| 752 | |||
| 753 | # Get share directory | ||
| 754 | local share_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share" | ||
| 755 | local volumes_dir="$share_dir/volumes" | ||
| 756 | |||
| 757 | # Create volumes directory | ||
| 758 | mkdir -p "$volumes_dir" | ||
| 759 | |||
| 760 | # Generate unique directory name based on host path | ||
| 761 | local hash=$(volume_hash "$host_path") | ||
| 762 | local vol_dir="$volumes_dir/$hash" | ||
| 763 | |||
| 764 | # Clean and copy | ||
| 765 | rm -rf "$vol_dir" | ||
| 766 | mkdir -p "$vol_dir" | ||
| 767 | |||
| 768 | if [ -d "$host_path" ]; then | ||
| 769 | # Directory: copy contents | ||
| 770 | cp -rL "$host_path"/* "$vol_dir/" 2>/dev/null || true | ||
| 771 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Volume: copied directory $host_path -> /mnt/share/volumes/$hash" >&2 | ||
| 772 | else | ||
| 773 | # File: copy file | ||
| 774 | cp -L "$host_path" "$vol_dir/" | ||
| 775 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Volume: copied file $host_path -> /mnt/share/volumes/$hash" >&2 | ||
| 776 | fi | ||
| 777 | |||
| 778 | # Sync to ensure data is visible to guest | ||
| 779 | sync | ||
| 780 | |||
| 781 | # Track for later sync-back (in parent shell, not subshell) | ||
| 782 | VOLUME_MOUNTS+=("$host_path:$vol_dir:$container_path") | ||
| 783 | VOLUME_MODES+=("$mode") | ||
| 784 | |||
| 785 | # Set result in global variable (caller reads this, not $(prepare_volume)) | ||
| 786 | PREPARE_VOLUME_RESULT="/mnt/share/volumes/$hash" | ||
| 787 | } | ||
| 788 | |||
| 789 | # Sync volumes back from guest to host (for :rw mounts) | ||
| 790 | sync_volumes_back() { | ||
| 791 | local share_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share" | ||
| 792 | local volumes_dir="$share_dir/volumes" | ||
| 793 | |||
| 794 | # Wait for 9p filesystem to sync writes from guest to host | ||
| 795 | sleep 1 | ||
| 796 | sync | ||
| 797 | |||
| 798 | for i in "${!VOLUME_MOUNTS[@]}"; do | ||
| 799 | local mount="${VOLUME_MOUNTS[$i]}" | ||
| 800 | local mode="${VOLUME_MODES[$i]}" | ||
| 801 | |||
| 802 | if [ "$mode" = "rw" ]; then | ||
| 803 | # Parse mount string: host_path:vol_dir:container_path | ||
| 804 | local host_path=$(echo "$mount" | cut -d: -f1) | ||
| 805 | local vol_dir=$(echo "$mount" | cut -d: -f2) | ||
| 806 | |||
| 807 | if [ -d "$vol_dir" ] && [ -d "$host_path" ]; then | ||
| 808 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Syncing volume back: $vol_dir -> $host_path" >&2 | ||
| 809 | # Use rsync if available, otherwise cp | ||
| 810 | if command -v rsync >/dev/null 2>&1; then | ||
| 811 | rsync -a --delete "$vol_dir/" "$host_path/" | ||
| 812 | else | ||
| 813 | rm -rf "$host_path"/* | ||
| 814 | cp -rL "$vol_dir"/* "$host_path/" 2>/dev/null || true | ||
| 815 | fi | ||
| 816 | elif [ -f "$host_path" ]; then | ||
| 817 | # Single file mount | ||
| 818 | local filename=$(basename "$host_path") | ||
| 819 | if [ -f "$vol_dir/$filename" ]; then | ||
| 820 | cp -L "$vol_dir/$filename" "$host_path" | ||
| 821 | fi | ||
| 822 | fi | ||
| 823 | fi | ||
| 824 | done | ||
| 825 | } | ||
| 826 | |||
| 827 | # Clean up volume directories | ||
| 828 | cleanup_volumes() { | ||
| 829 | local share_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share" | ||
| 830 | local volumes_dir="$share_dir/volumes" | ||
| 831 | |||
| 832 | if [ -d "$volumes_dir" ]; then | ||
| 833 | rm -rf "$volumes_dir" | ||
| 834 | fi | ||
| 835 | |||
| 836 | # Clear tracking arrays | ||
| 837 | VOLUME_MOUNTS=() | ||
| 838 | VOLUME_MODES=() | ||
| 839 | } | ||
| 840 | |||
| 841 | # Global variable to hold transformed volume arguments | ||
| 842 | TRANSFORMED_VOLUME_ARGS=() | ||
| 843 | |||
| 844 | # Parse volume mounts from arguments and transform them | ||
| 845 | # Input: array elements passed as arguments | ||
| 846 | # Output: sets TRANSFORMED_VOLUME_ARGS with transformed arguments | ||
| 847 | # Side effect: populates VOLUME_MOUNTS array | ||
| 848 | parse_and_prepare_volumes() { | ||
| 849 | TRANSFORMED_VOLUME_ARGS=() | ||
| 850 | local args=("$@") | ||
| 851 | local i=0 | ||
| 852 | |||
| 853 | while [ $i -lt ${#args[@]} ]; do | ||
| 854 | local arg="${args[$i]}" | ||
| 855 | |||
| 856 | case "$arg" in | ||
| 857 | -v|--volume) | ||
| 858 | # Next arg is the volume spec | ||
| 859 | i=$((i + 1)) | ||
| 860 | if [ $i -ge ${#args[@]} ]; then | ||
| 861 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -v requires an argument" >&2 | ||
| 862 | return 1 | ||
| 863 | fi | ||
| 864 | local vol_spec="${args[$i]}" | ||
| 865 | |||
| 866 | # Parse volume spec: host:container[:mode] | ||
| 867 | local host_path=$(echo "$vol_spec" | cut -d: -f1) | ||
| 868 | local container_path=$(echo "$vol_spec" | cut -d: -f2) | ||
| 869 | local mode=$(echo "$vol_spec" | cut -d: -f3) | ||
| 870 | |||
| 871 | # Make host path absolute | ||
| 872 | if [[ "$host_path" != /* ]]; then | ||
| 873 | host_path="$(pwd)/$host_path" | ||
| 874 | fi | ||
| 875 | |||
| 876 | # Prepare volume (sets PREPARE_VOLUME_RESULT global) | ||
| 877 | prepare_volume "$host_path" "$container_path" "$mode" || return 1 | ||
| 878 | local guest_path="$PREPARE_VOLUME_RESULT" | ||
| 879 | |||
| 880 | # Add transformed volume option | ||
| 881 | if [ -d "$host_path" ]; then | ||
| 882 | TRANSFORMED_VOLUME_ARGS+=("-v" "${guest_path}:${container_path}${mode:+:$mode}") | ||
| 883 | else | ||
| 884 | # For single file, include filename | ||
| 885 | local filename=$(basename "$host_path") | ||
| 886 | TRANSFORMED_VOLUME_ARGS+=("-v" "${guest_path}/${filename}:${container_path}${mode:+:$mode}") | ||
| 887 | fi | ||
| 888 | ;; | ||
| 889 | *) | ||
| 890 | TRANSFORMED_VOLUME_ARGS+=("$arg") | ||
| 891 | ;; | ||
| 892 | esac | ||
| 893 | i=$((i + 1)) | ||
| 894 | done | ||
| 895 | } | ||
| 896 | |||
| 897 | # Handle commands | ||
| 898 | case "$COMMAND" in | ||
| 899 | images) | ||
| 900 | # runtime images | ||
| 901 | run_runtime_command "$VCONTAINER_RUNTIME_CMD images ${COMMAND_ARGS[*]}" | ||
| 902 | ;; | ||
| 903 | |||
| 904 | pull) | ||
| 905 | # runtime pull <image> | ||
| 906 | # Daemon mode already has networking enabled, so this works via daemon | ||
| 907 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 908 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} pull requires <image>" >&2 | ||
| 909 | exit 1 | ||
| 910 | fi | ||
| 911 | |||
| 912 | IMAGE_NAME="${COMMAND_ARGS[0]}" | ||
| 913 | |||
| 914 | if daemon_is_running; then | ||
| 915 | # Use daemon mode (already has networking) | ||
| 916 | run_runtime_command "$VCONTAINER_RUNTIME_CMD pull $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 917 | else | ||
| 918 | # Regular mode - need to enable networking | ||
| 919 | NETWORK="true" | ||
| 920 | RUNNER_ARGS=$(build_runner_args) | ||
| 921 | "$RUNNER" $RUNNER_ARGS -- "$VCONTAINER_RUNTIME_CMD pull $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 922 | fi | ||
| 923 | ;; | ||
| 924 | |||
| 925 | load) | ||
| 926 | # runtime load -i <file> | ||
| 927 | # Parse -i argument | ||
| 928 | INPUT_FILE="" | ||
| 929 | LOAD_ARGS=() | ||
| 930 | i=0 | ||
| 931 | while [ $i -lt ${#COMMAND_ARGS[@]} ]; do | ||
| 932 | arg="${COMMAND_ARGS[$i]}" | ||
| 933 | case "$arg" in | ||
| 934 | -i|--input) | ||
| 935 | i=$((i + 1)) | ||
| 936 | INPUT_FILE="${COMMAND_ARGS[$i]}" | ||
| 937 | ;; | ||
| 938 | *) | ||
| 939 | LOAD_ARGS+=("$arg") | ||
| 940 | ;; | ||
| 941 | esac | ||
| 942 | i=$((i + 1)) | ||
| 943 | done | ||
| 944 | |||
| 945 | if [ -z "$INPUT_FILE" ]; then | ||
| 946 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} load requires -i <file>" >&2 | ||
| 947 | exit 1 | ||
| 948 | fi | ||
| 949 | |||
| 950 | if [ ! -f "$INPUT_FILE" ]; then | ||
| 951 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} File not found: $INPUT_FILE" >&2 | ||
| 952 | exit 1 | ||
| 953 | fi | ||
| 954 | |||
| 955 | run_runtime_command_with_input "$INPUT_FILE" "tar" \ | ||
| 956 | "$VCONTAINER_RUNTIME_CMD load -i {INPUT}/$(basename "$INPUT_FILE") ${LOAD_ARGS[*]}" | ||
| 957 | ;; | ||
| 958 | |||
| 959 | import) | ||
| 960 | # runtime import <tarball> [name:tag] - matches Docker/Podman's import exactly | ||
| 961 | # Only accepts tarballs (rootfs archives), not OCI directories | ||
| 962 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 963 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} import requires <tarball> [name:tag]" >&2 | ||
| 964 | echo "For OCI directories, use 'vimport' instead." >&2 | ||
| 965 | exit 1 | ||
| 966 | fi | ||
| 967 | |||
| 968 | INPUT_PATH="${COMMAND_ARGS[0]}" | ||
| 969 | IMAGE_NAME="${COMMAND_ARGS[1]:-imported:latest}" | ||
| 970 | |||
| 971 | if [ ! -e "$INPUT_PATH" ]; then | ||
| 972 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Not found: $INPUT_PATH" >&2 | ||
| 973 | exit 1 | ||
| 974 | fi | ||
| 975 | |||
| 976 | # Only accept files (tarballs), not directories | ||
| 977 | if [ -d "$INPUT_PATH" ]; then | ||
| 978 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} import only accepts tarballs, not directories" >&2 | ||
| 979 | echo "For OCI directories, use: $VCONTAINER_RUNTIME_NAME vimport $INPUT_PATH $IMAGE_NAME" >&2 | ||
| 980 | exit 1 | ||
| 981 | fi | ||
| 982 | |||
| 983 | run_runtime_command_with_input "$INPUT_PATH" "tar" \ | ||
| 984 | "$VCONTAINER_RUNTIME_CMD import {INPUT}/$(basename "$INPUT_PATH") $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 985 | ;; | ||
| 986 | |||
| 987 | vimport) | ||
| 988 | # Extended import: handles OCI directories, tarballs, and plain directories | ||
| 989 | # Auto-detects format | ||
| 990 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 991 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} vimport requires <path> [name:tag]" >&2 | ||
| 992 | exit 1 | ||
| 993 | fi | ||
| 994 | |||
| 995 | INPUT_PATH="${COMMAND_ARGS[0]}" | ||
| 996 | IMAGE_NAME="${COMMAND_ARGS[1]:-imported:latest}" | ||
| 997 | |||
| 998 | if [ ! -e "$INPUT_PATH" ]; then | ||
| 999 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Not found: $INPUT_PATH" >&2 | ||
| 1000 | exit 1 | ||
| 1001 | fi | ||
| 1002 | |||
| 1003 | # Detect input type | ||
| 1004 | if [ -d "$INPUT_PATH" ]; then | ||
| 1005 | if [ -f "$INPUT_PATH/index.json" ] || [ -f "$INPUT_PATH/oci-layout" ]; then | ||
| 1006 | INPUT_TYPE="oci" | ||
| 1007 | |||
| 1008 | # Check architecture before importing | ||
| 1009 | if ! check_oci_arch "$INPUT_PATH" "$TARGET_ARCH"; then | ||
| 1010 | exit 1 | ||
| 1011 | fi | ||
| 1012 | |||
| 1013 | # Use skopeo to properly import OCI image with full metadata (entrypoint, cmd, etc.) | ||
| 1014 | # This preserves the container config unlike raw import | ||
| 1015 | RUNTIME_CMD="skopeo copy oci:{INPUT} ${VCONTAINER_IMPORT_TARGET}$IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 1016 | else | ||
| 1017 | # Directory but not OCI - check if it looks like a deploy/images dir | ||
| 1018 | # and provide a helpful hint | ||
| 1019 | if ls "$INPUT_PATH"/*-oci >/dev/null 2>&1; then | ||
| 1020 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Directory is not an OCI container: $INPUT_PATH" >&2 | ||
| 1021 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Found OCI directories inside. Did you mean one of these?" >&2 | ||
| 1022 | for oci_dir in "$INPUT_PATH"/*-oci; do | ||
| 1023 | if [ -d "$oci_dir" ]; then | ||
| 1024 | echo " $(basename "$oci_dir")" >&2 | ||
| 1025 | fi | ||
| 1026 | done | ||
| 1027 | echo "" >&2 | ||
| 1028 | echo "Example: $VCONTAINER_RUNTIME_NAME vimport $INPUT_PATH/$(ls "$INPUT_PATH" | grep -m1 '\-oci$') myimage:latest" >&2 | ||
| 1029 | exit 1 | ||
| 1030 | fi | ||
| 1031 | INPUT_TYPE="dir" | ||
| 1032 | RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD import {INPUT} $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 1033 | fi | ||
| 1034 | else | ||
| 1035 | INPUT_TYPE="tar" | ||
| 1036 | RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD import {INPUT}/$(basename "$INPUT_PATH") $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 1037 | fi | ||
| 1038 | |||
| 1039 | run_runtime_command_with_input "$INPUT_PATH" "$INPUT_TYPE" "$RUNTIME_CMD" | ||
| 1040 | ;; | ||
| 1041 | |||
| 1042 | save) | ||
| 1043 | # runtime save -o <file> <image> | ||
| 1044 | OUTPUT_FILE="" | ||
| 1045 | IMAGE_NAME="" | ||
| 1046 | i=0 | ||
| 1047 | while [ $i -lt ${#COMMAND_ARGS[@]} ]; do | ||
| 1048 | arg="${COMMAND_ARGS[$i]}" | ||
| 1049 | case "$arg" in | ||
| 1050 | -o|--output) | ||
| 1051 | i=$((i + 1)) | ||
| 1052 | OUTPUT_FILE="${COMMAND_ARGS[$i]}" | ||
| 1053 | ;; | ||
| 1054 | *) | ||
| 1055 | IMAGE_NAME="$arg" | ||
| 1056 | ;; | ||
| 1057 | esac | ||
| 1058 | i=$((i + 1)) | ||
| 1059 | done | ||
| 1060 | |||
| 1061 | if [ -z "$OUTPUT_FILE" ]; then | ||
| 1062 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} save requires -o <file>" >&2 | ||
| 1063 | exit 1 | ||
| 1064 | fi | ||
| 1065 | |||
| 1066 | if [ -z "$IMAGE_NAME" ]; then | ||
| 1067 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} save requires <image> name" >&2 | ||
| 1068 | exit 1 | ||
| 1069 | fi | ||
| 1070 | |||
| 1071 | if daemon_is_running; then | ||
| 1072 | # Use daemon mode with virtio-9p - save to shared dir, then copy to host | ||
| 1073 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon mode for save" >&2 | ||
| 1074 | SHARE_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share" | ||
| 1075 | |||
| 1076 | # Clear share dir and run save command | ||
| 1077 | rm -rf "$SHARE_DIR"/* 2>/dev/null || true | ||
| 1078 | run_runtime_command "$VCONTAINER_RUNTIME_CMD save -o /mnt/share/output.tar $IMAGE_NAME" | ||
| 1079 | |||
| 1080 | # Copy from share dir to output file | ||
| 1081 | if [ -f "$SHARE_DIR/output.tar" ]; then | ||
| 1082 | cp "$SHARE_DIR/output.tar" "$OUTPUT_FILE" | ||
| 1083 | rm -f "$SHARE_DIR/output.tar" | ||
| 1084 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Saved to $OUTPUT_FILE ($(du -h "$OUTPUT_FILE" | cut -f1))" | ||
| 1085 | else | ||
| 1086 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Save failed - output not found in shared directory" >&2 | ||
| 1087 | exit 1 | ||
| 1088 | fi | ||
| 1089 | else | ||
| 1090 | # Regular mode - use serial output | ||
| 1091 | RUNNER_ARGS=$(build_runner_args) | ||
| 1092 | RUNNER_ARGS=$(echo "$RUNNER_ARGS" | sed 's/--output-type storage//') | ||
| 1093 | "$RUNNER" $RUNNER_ARGS --output-type tar --output "$OUTPUT_FILE" \ | ||
| 1094 | -- "$VCONTAINER_RUNTIME_CMD save -o /tmp/output.tar $IMAGE_NAME" | ||
| 1095 | fi | ||
| 1096 | ;; | ||
| 1097 | |||
| 1098 | tag|rmi) | ||
| 1099 | # Commands that work with existing images | ||
| 1100 | run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" | ||
| 1101 | ;; | ||
| 1102 | |||
| 1103 | # Container lifecycle commands | ||
| 1104 | ps) | ||
| 1105 | # List containers | ||
| 1106 | run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}" | ||
| 1107 | ;; | ||
| 1108 | |||
| 1109 | rm) | ||
| 1110 | # Remove containers | ||
| 1111 | run_runtime_command "$VCONTAINER_RUNTIME_CMD rm ${COMMAND_ARGS[*]}" | ||
| 1112 | ;; | ||
| 1113 | |||
| 1114 | logs) | ||
| 1115 | # View container logs | ||
| 1116 | run_runtime_command "$VCONTAINER_RUNTIME_CMD logs ${COMMAND_ARGS[*]}" | ||
| 1117 | ;; | ||
| 1118 | |||
| 1119 | inspect) | ||
| 1120 | # Inspect container or image | ||
| 1121 | run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}" | ||
| 1122 | ;; | ||
| 1123 | |||
| 1124 | start|stop|restart|kill|pause|unpause) | ||
| 1125 | # Container state commands | ||
| 1126 | run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" | ||
| 1127 | ;; | ||
| 1128 | |||
| 1129 | # Image commands | ||
| 1130 | commit) | ||
| 1131 | # Commit container to image | ||
| 1132 | run_runtime_command "$VCONTAINER_RUNTIME_CMD commit ${COMMAND_ARGS[*]}" | ||
| 1133 | ;; | ||
| 1134 | |||
| 1135 | history) | ||
| 1136 | # Show image history | ||
| 1137 | run_runtime_command "$VCONTAINER_RUNTIME_CMD history ${COMMAND_ARGS[*]}" | ||
| 1138 | ;; | ||
| 1139 | |||
| 1140 | # Registry commands | ||
| 1141 | push) | ||
| 1142 | # Push image to registry | ||
| 1143 | run_runtime_command "$VCONTAINER_RUNTIME_CMD push ${COMMAND_ARGS[*]}" | ||
| 1144 | ;; | ||
| 1145 | |||
| 1146 | search) | ||
| 1147 | # Search registries | ||
| 1148 | run_runtime_command "$VCONTAINER_RUNTIME_CMD search ${COMMAND_ARGS[*]}" | ||
| 1149 | ;; | ||
| 1150 | |||
| 1151 | login) | ||
| 1152 | # Login to registry - may need credentials via stdin | ||
| 1153 | # For non-interactive: runtime login -u user -p pass registry | ||
| 1154 | run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" | ||
| 1155 | ;; | ||
| 1156 | |||
| 1157 | logout) | ||
| 1158 | # Logout from registry | ||
| 1159 | run_runtime_command "$VCONTAINER_RUNTIME_CMD logout ${COMMAND_ARGS[*]}" | ||
| 1160 | ;; | ||
| 1161 | |||
| 1162 | # Runtime exec - execute command in running container | ||
| 1163 | exec) | ||
| 1164 | if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then | ||
| 1165 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} exec requires <container> <command>" >&2 | ||
| 1166 | exit 1 | ||
| 1167 | fi | ||
| 1168 | |||
| 1169 | # Check for interactive flags | ||
| 1170 | EXEC_INTERACTIVE=false | ||
| 1171 | EXEC_ARGS=() | ||
| 1172 | for arg in "${COMMAND_ARGS[@]}"; do | ||
| 1173 | case "$arg" in | ||
| 1174 | -it|-ti|--interactive|--tty) | ||
| 1175 | EXEC_INTERACTIVE=true | ||
| 1176 | EXEC_ARGS+=("$arg") | ||
| 1177 | ;; | ||
| 1178 | -i|-t) | ||
| 1179 | EXEC_INTERACTIVE=true | ||
| 1180 | EXEC_ARGS+=("$arg") | ||
| 1181 | ;; | ||
| 1182 | *) | ||
| 1183 | EXEC_ARGS+=("$arg") | ||
| 1184 | ;; | ||
| 1185 | esac | ||
| 1186 | done | ||
| 1187 | |||
| 1188 | if [ "$EXEC_INTERACTIVE" = "true" ]; then | ||
| 1189 | # Interactive exec can use daemon_interactive if daemon is running | ||
| 1190 | if daemon_is_running; then | ||
| 1191 | # Use daemon interactive mode - keeps daemon running | ||
| 1192 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2 | ||
| 1193 | RUNNER_ARGS=$(build_runner_args) | ||
| 1194 | "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$VCONTAINER_RUNTIME_CMD exec ${EXEC_ARGS[*]}" | ||
| 1195 | else | ||
| 1196 | # No daemon running, use regular QEMU | ||
| 1197 | RUNNER_ARGS=$(build_runner_args) | ||
| 1198 | "$RUNNER" $RUNNER_ARGS -- "$VCONTAINER_RUNTIME_CMD exec ${EXEC_ARGS[*]}" | ||
| 1199 | fi | ||
| 1200 | else | ||
| 1201 | # Non-interactive exec via daemon | ||
| 1202 | run_runtime_command "$VCONTAINER_RUNTIME_CMD exec ${EXEC_ARGS[*]}" | ||
| 1203 | fi | ||
| 1204 | ;; | ||
| 1205 | |||
| 1206 | # Runtime cp - copy files to/from container | ||
| 1207 | cp) | ||
| 1208 | if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then | ||
| 1209 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires <src> <dest>" >&2 | ||
| 1210 | echo "Usage: $VCONTAINER_RUNTIME_NAME cp <container>:<path> <local_path>" >&2 | ||
| 1211 | echo " $VCONTAINER_RUNTIME_NAME cp <local_path> <container>:<path>" >&2 | ||
| 1212 | exit 1 | ||
| 1213 | fi | ||
| 1214 | |||
| 1215 | SRC="${COMMAND_ARGS[0]}" | ||
| 1216 | DEST="${COMMAND_ARGS[1]}" | ||
| 1217 | |||
| 1218 | # Determine direction: host->container or container->host | ||
| 1219 | if [[ "$SRC" == *":"* ]] && [[ "$DEST" != *":"* ]]; then | ||
| 1220 | # Container to host: runtime cp container:/path /local/path | ||
| 1221 | # Run runtime cp to /mnt/share, then copy from share to host | ||
| 1222 | CONTAINER_PATH="$SRC" | ||
| 1223 | HOST_PATH="$DEST" | ||
| 1224 | SHARE_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share" | ||
| 1225 | |||
| 1226 | if daemon_is_running; then | ||
| 1227 | rm -rf "$SHARE_DIR"/* 2>/dev/null || true | ||
| 1228 | run_runtime_command "$VCONTAINER_RUNTIME_CMD cp $CONTAINER_PATH /mnt/share/" | ||
| 1229 | # Find what was copied and move to destination | ||
| 1230 | if [ -n "$(ls -A "$SHARE_DIR" 2>/dev/null)" ]; then | ||
| 1231 | cp -r "$SHARE_DIR"/* "$HOST_PATH" 2>/dev/null || cp -r "$SHARE_DIR"/* "$(dirname "$HOST_PATH")/" | ||
| 1232 | rm -rf "$SHARE_DIR"/* | ||
| 1233 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Copied to $HOST_PATH" | ||
| 1234 | else | ||
| 1235 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Copy failed - no files in share directory" >&2 | ||
| 1236 | exit 1 | ||
| 1237 | fi | ||
| 1238 | else | ||
| 1239 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2 | ||
| 1240 | exit 1 | ||
| 1241 | fi | ||
| 1242 | |||
| 1243 | elif [[ "$SRC" != *":"* ]] && [[ "$DEST" == *":"* ]]; then | ||
| 1244 | # Host to container: runtime cp /local/path container:/path | ||
| 1245 | HOST_PATH="$SRC" | ||
| 1246 | CONTAINER_PATH="$DEST" | ||
| 1247 | |||
| 1248 | if [ ! -e "$HOST_PATH" ]; then | ||
| 1249 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Source not found: $HOST_PATH" >&2 | ||
| 1250 | exit 1 | ||
| 1251 | fi | ||
| 1252 | |||
| 1253 | if daemon_is_running; then | ||
| 1254 | SHARE_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share" | ||
| 1255 | rm -rf "$SHARE_DIR"/* 2>/dev/null || true | ||
| 1256 | cp -r "$HOST_PATH" "$SHARE_DIR/" | ||
| 1257 | sync | ||
| 1258 | BASENAME=$(basename "$HOST_PATH") | ||
| 1259 | run_runtime_command "$VCONTAINER_RUNTIME_CMD cp /mnt/share/$BASENAME $CONTAINER_PATH" | ||
| 1260 | rm -rf "$SHARE_DIR"/* | ||
| 1261 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Copied to $CONTAINER_PATH" | ||
| 1262 | else | ||
| 1263 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2 | ||
| 1264 | exit 1 | ||
| 1265 | fi | ||
| 1266 | else | ||
| 1267 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Invalid cp syntax. One path must be container:path" >&2 | ||
| 1268 | exit 1 | ||
| 1269 | fi | ||
| 1270 | ;; | ||
| 1271 | |||
| 1272 | vconfig) | ||
| 1273 | # Configuration management (runs on host, not in VM) | ||
| 1274 | VALID_KEYS="arch timeout state-dir verbose" | ||
| 1275 | |||
| 1276 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 1277 | # Show all config | ||
| 1278 | echo "$VCONTAINER_RUNTIME_NAME configuration ($CONFIG_FILE):" | ||
| 1279 | echo "" | ||
| 1280 | for key in $VALID_KEYS; do | ||
| 1281 | value=$(config_get "$key" "") | ||
| 1282 | default=$(config_default "$key") | ||
| 1283 | if [ -n "$value" ]; then | ||
| 1284 | echo " ${CYAN}$key${NC} = $value" | ||
| 1285 | else | ||
| 1286 | echo " ${CYAN}$key${NC} = $default ${YELLOW}(default)${NC}" | ||
| 1287 | fi | ||
| 1288 | done | ||
| 1289 | echo "" | ||
| 1290 | echo "Config directory: $CONFIG_DIR" | ||
| 1291 | else | ||
| 1292 | KEY="${COMMAND_ARGS[0]}" | ||
| 1293 | VALUE="${COMMAND_ARGS[1]:-}" | ||
| 1294 | |||
| 1295 | # Validate key | ||
| 1296 | if ! echo "$VALID_KEYS" | grep -qw "$KEY"; then | ||
| 1297 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown config key: $KEY" >&2 | ||
| 1298 | echo "Valid keys: $VALID_KEYS" >&2 | ||
| 1299 | exit 1 | ||
| 1300 | fi | ||
| 1301 | |||
| 1302 | if [ -z "$VALUE" ]; then | ||
| 1303 | # Get value | ||
| 1304 | current=$(config_get "$KEY" "") | ||
| 1305 | default=$(config_default "$KEY") | ||
| 1306 | if [ -n "$current" ]; then | ||
| 1307 | echo "$current" | ||
| 1308 | else | ||
| 1309 | echo "$default" | ||
| 1310 | fi | ||
| 1311 | elif [ "$VALUE" = "--reset" ]; then | ||
| 1312 | # Reset to default | ||
| 1313 | config_unset "$KEY" | ||
| 1314 | echo "Reset $KEY to default: $(config_default "$KEY")" | ||
| 1315 | else | ||
| 1316 | # Validate value for arch | ||
| 1317 | if [ "$KEY" = "arch" ]; then | ||
| 1318 | case "$VALUE" in | ||
| 1319 | aarch64|x86_64) ;; | ||
| 1320 | *) | ||
| 1321 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Invalid architecture: $VALUE" >&2 | ||
| 1322 | echo "Valid values: aarch64, x86_64" >&2 | ||
| 1323 | exit 1 | ||
| 1324 | ;; | ||
| 1325 | esac | ||
| 1326 | fi | ||
| 1327 | |||
| 1328 | # Validate value for verbose | ||
| 1329 | if [ "$KEY" = "verbose" ]; then | ||
| 1330 | case "$VALUE" in | ||
| 1331 | true|false) ;; | ||
| 1332 | *) | ||
| 1333 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Invalid verbose value: $VALUE" >&2 | ||
| 1334 | echo "Valid values: true, false" >&2 | ||
| 1335 | exit 1 | ||
| 1336 | ;; | ||
| 1337 | esac | ||
| 1338 | fi | ||
| 1339 | |||
| 1340 | # Set value | ||
| 1341 | config_set "$KEY" "$VALUE" | ||
| 1342 | echo "Set $KEY = $VALUE" | ||
| 1343 | fi | ||
| 1344 | fi | ||
| 1345 | ;; | ||
| 1346 | |||
| 1347 | clean) | ||
| 1348 | # DEPRECATED: Use 'vstorage clean' instead | ||
| 1349 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} DEPRECATED: 'clean' is deprecated, use 'vstorage clean' instead" >&2 | ||
| 1350 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} vstorage clean - clean current architecture" >&2 | ||
| 1351 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} vstorage clean <arch> - clean specific architecture" >&2 | ||
| 1352 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} vstorage clean --all - clean all architectures" >&2 | ||
| 1353 | echo "" >&2 | ||
| 1354 | |||
| 1355 | # Still perform the clean for now (will be removed in future version) | ||
| 1356 | CLEAN_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" | ||
| 1357 | if [ -d "$CLEAN_DIR" ]; then | ||
| 1358 | # Stop memres if running | ||
| 1359 | if [ -f "$CLEAN_DIR/daemon.pid" ]; then | ||
| 1360 | pid=$(cat "$CLEAN_DIR/daemon.pid" 2>/dev/null) | ||
| 1361 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then | ||
| 1362 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..." | ||
| 1363 | kill "$pid" 2>/dev/null || true | ||
| 1364 | fi | ||
| 1365 | fi | ||
| 1366 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing state directory: $CLEAN_DIR" | ||
| 1367 | rm -rf "$CLEAN_DIR" | ||
| 1368 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} State cleaned. Next run will start fresh." | ||
| 1369 | else | ||
| 1370 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} No state directory found for $TARGET_ARCH" | ||
| 1371 | fi | ||
| 1372 | ;; | ||
| 1373 | |||
| 1374 | info) | ||
| 1375 | run_runtime_command "$VCONTAINER_RUNTIME_CMD info" | ||
| 1376 | ;; | ||
| 1377 | |||
| 1378 | version) | ||
| 1379 | run_runtime_command "$VCONTAINER_RUNTIME_CMD version" | ||
| 1380 | ;; | ||
| 1381 | |||
| 1382 | system) | ||
| 1383 | # Passthrough to runtime system commands (df, prune, events, etc.) | ||
| 1384 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 1385 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} system requires a subcommand: df, prune, events, info" >&2 | ||
| 1386 | exit 1 | ||
| 1387 | fi | ||
| 1388 | run_runtime_command "$VCONTAINER_RUNTIME_CMD system ${COMMAND_ARGS[*]}" | ||
| 1389 | ;; | ||
| 1390 | |||
| 1391 | vstorage) | ||
| 1392 | # Host-side storage management (runs on host, not in VM) | ||
| 1393 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 1394 | STORAGE_CMD="list" | ||
| 1395 | else | ||
| 1396 | STORAGE_CMD="${COMMAND_ARGS[0]}" | ||
| 1397 | fi | ||
| 1398 | |||
| 1399 | case "$STORAGE_CMD" in | ||
| 1400 | list) | ||
| 1401 | echo "$VCONTAINER_RUNTIME_NAME storage directories:" | ||
| 1402 | echo "" | ||
| 1403 | found=0 | ||
| 1404 | for state_dir in "$DEFAULT_STATE_DIR"/*/; do | ||
| 1405 | [ -d "$state_dir" ] || continue | ||
| 1406 | found=1 | ||
| 1407 | instance=$(basename "$state_dir") | ||
| 1408 | size=$(du -sh "$state_dir" 2>/dev/null | cut -f1) | ||
| 1409 | |||
| 1410 | echo " ${CYAN}$instance${NC}" | ||
| 1411 | echo " Path: $state_dir" | ||
| 1412 | echo " Size: $size" | ||
| 1413 | |||
| 1414 | # Check if memres is running | ||
| 1415 | if [ -f "$state_dir/daemon.pid" ]; then | ||
| 1416 | pid=$(cat "$state_dir/daemon.pid") | ||
| 1417 | if [ -d "/proc/$pid" ]; then | ||
| 1418 | echo " Status: ${GREEN}memres running${NC} (PID $pid)" | ||
| 1419 | else | ||
| 1420 | echo " Status: stopped" | ||
| 1421 | fi | ||
| 1422 | else | ||
| 1423 | echo " Status: no memres" | ||
| 1424 | fi | ||
| 1425 | echo "" | ||
| 1426 | done | ||
| 1427 | if [ $found -eq 0 ]; then | ||
| 1428 | echo " (no storage directories found)" | ||
| 1429 | echo "" | ||
| 1430 | fi | ||
| 1431 | |||
| 1432 | # Total size | ||
| 1433 | if [ -d "$DEFAULT_STATE_DIR" ] && [ $found -gt 0 ]; then | ||
| 1434 | total=$(du -sh "$DEFAULT_STATE_DIR" 2>/dev/null | cut -f1) | ||
| 1435 | echo "Total: $total" | ||
| 1436 | fi | ||
| 1437 | ;; | ||
| 1438 | |||
| 1439 | path) | ||
| 1440 | # Show path for specific or current architecture | ||
| 1441 | arch="${COMMAND_ARGS[1]:-$TARGET_ARCH}" | ||
| 1442 | echo "${STATE_DIR:-$DEFAULT_STATE_DIR/$arch}" | ||
| 1443 | ;; | ||
| 1444 | |||
| 1445 | df) | ||
| 1446 | # Detailed breakdown | ||
| 1447 | for state_dir in "$DEFAULT_STATE_DIR"/*/; do | ||
| 1448 | [ -d "$state_dir" ] || continue | ||
| 1449 | instance=$(basename "$state_dir") | ||
| 1450 | echo "${BOLD}$instance${NC}:" | ||
| 1451 | |||
| 1452 | # Show individual components | ||
| 1453 | for item in "$VCONTAINER_STATE_FILE" share; do | ||
| 1454 | if [ -e "$state_dir/$item" ]; then | ||
| 1455 | item_size=$(du -sh "$state_dir/$item" 2>/dev/null | cut -f1) | ||
| 1456 | printf " %-20s %s\n" "$item" "$item_size" | ||
| 1457 | fi | ||
| 1458 | done | ||
| 1459 | echo "" | ||
| 1460 | done | ||
| 1461 | ;; | ||
| 1462 | |||
| 1463 | clean) | ||
| 1464 | # Clean storage for specific arch or all | ||
| 1465 | arch="${COMMAND_ARGS[1]:-}" | ||
| 1466 | if [ "$arch" = "--all" ]; then | ||
| 1467 | # Stop any running memres first | ||
| 1468 | for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do | ||
| 1469 | [ -f "$pid_file" ] || continue | ||
| 1470 | pid=$(cat "$pid_file" 2>/dev/null) | ||
| 1471 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then | ||
| 1472 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..." | ||
| 1473 | kill "$pid" 2>/dev/null || true | ||
| 1474 | fi | ||
| 1475 | done | ||
| 1476 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing all storage directories..." | ||
| 1477 | rm -rf "$DEFAULT_STATE_DIR" | ||
| 1478 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} All storage cleaned." | ||
| 1479 | elif [ -n "$arch" ]; then | ||
| 1480 | # Clean specific arch | ||
| 1481 | clean_dir="$DEFAULT_STATE_DIR/$arch" | ||
| 1482 | if [ -d "$clean_dir" ]; then | ||
| 1483 | # Stop memres if running | ||
| 1484 | if [ -f "$clean_dir/daemon.pid" ]; then | ||
| 1485 | pid=$(cat "$clean_dir/daemon.pid" 2>/dev/null) | ||
| 1486 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then | ||
| 1487 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..." | ||
| 1488 | kill "$pid" 2>/dev/null || true | ||
| 1489 | fi | ||
| 1490 | fi | ||
| 1491 | rm -rf "$clean_dir" | ||
| 1492 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Cleaned: $clean_dir" | ||
| 1493 | else | ||
| 1494 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Not found: $clean_dir" | ||
| 1495 | fi | ||
| 1496 | else | ||
| 1497 | # Clean current arch (same as existing clean command) | ||
| 1498 | clean_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" | ||
| 1499 | if [ -d "$clean_dir" ]; then | ||
| 1500 | # Stop memres if running | ||
| 1501 | if [ -f "$clean_dir/daemon.pid" ]; then | ||
| 1502 | pid=$(cat "$clean_dir/daemon.pid" 2>/dev/null) | ||
| 1503 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then | ||
| 1504 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..." | ||
| 1505 | kill "$pid" 2>/dev/null || true | ||
| 1506 | fi | ||
| 1507 | fi | ||
| 1508 | rm -rf "$clean_dir" | ||
| 1509 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Cleaned: $clean_dir" | ||
| 1510 | else | ||
| 1511 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} No storage directory found for $TARGET_ARCH" | ||
| 1512 | fi | ||
| 1513 | fi | ||
| 1514 | ;; | ||
| 1515 | |||
| 1516 | *) | ||
| 1517 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown vstorage subcommand: $STORAGE_CMD" >&2 | ||
| 1518 | echo "Usage: $VCONTAINER_RUNTIME_NAME vstorage [list|path|df|clean]" >&2 | ||
| 1519 | echo "" >&2 | ||
| 1520 | echo "Subcommands:" >&2 | ||
| 1521 | echo " list List all storage directories with details" >&2 | ||
| 1522 | echo " path [arch] Show path to storage directory" >&2 | ||
| 1523 | echo " df Show detailed disk usage breakdown" >&2 | ||
| 1524 | echo " clean [arch|--all] Clean storage directories" >&2 | ||
| 1525 | exit 1 | ||
| 1526 | ;; | ||
| 1527 | esac | ||
| 1528 | ;; | ||
| 1529 | |||
| 1530 | vrun) | ||
| 1531 | # Extended run: run a command in a container (runtime-like syntax) | ||
| 1532 | # Usage: <tool> vrun [options] <image> [command] [args...] | ||
| 1533 | # Options: | ||
| 1534 | # -it, -i, -t Interactive mode with TTY | ||
| 1535 | # --network, -n Enable networking | ||
| 1536 | # -p <host>:<guest> Forward port from host to container | ||
| 1537 | # -v <host>:<container> Mount host directory in container | ||
| 1538 | # | ||
| 1539 | # Parse vrun-specific options (allows runtime-like: vdkr vrun -it alpine /bin/sh) | ||
| 1540 | VRUN_VOLUMES=() | ||
| 1541 | HAS_VOLUMES=false | ||
| 1542 | |||
| 1543 | while [ ${#COMMAND_ARGS[@]} -gt 0 ]; do | ||
| 1544 | case "${COMMAND_ARGS[0]}" in | ||
| 1545 | -it|--interactive) | ||
| 1546 | INTERACTIVE="true" | ||
| 1547 | COMMAND_ARGS=("${COMMAND_ARGS[@]:1}") | ||
| 1548 | ;; | ||
| 1549 | -i|-t) | ||
| 1550 | INTERACTIVE="true" | ||
| 1551 | COMMAND_ARGS=("${COMMAND_ARGS[@]:1}") | ||
| 1552 | ;; | ||
| 1553 | --no-network) | ||
| 1554 | NETWORK="false" | ||
| 1555 | COMMAND_ARGS=("${COMMAND_ARGS[@]:1}") | ||
| 1556 | ;; | ||
| 1557 | -p|--publish) | ||
| 1558 | # Port forward: -p 8080:80 or -p 8080:80/tcp | ||
| 1559 | NETWORK="true" # Port forwarding requires networking | ||
| 1560 | if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then | ||
| 1561 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -p requires <host_port>:<container_port>" >&2 | ||
| 1562 | exit 1 | ||
| 1563 | fi | ||
| 1564 | PORT_FORWARDS+=("${COMMAND_ARGS[1]}") | ||
| 1565 | COMMAND_ARGS=("${COMMAND_ARGS[@]:2}") | ||
| 1566 | ;; | ||
| 1567 | -v|--volume) | ||
| 1568 | # Volume mount: -v /host/path:/container/path[:ro|:rw] | ||
| 1569 | if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then | ||
| 1570 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -v requires <host_path>:<container_path>" >&2 | ||
| 1571 | exit 1 | ||
| 1572 | fi | ||
| 1573 | VRUN_VOLUMES+=("-v" "${COMMAND_ARGS[1]}") | ||
| 1574 | HAS_VOLUMES=true | ||
| 1575 | COMMAND_ARGS=("${COMMAND_ARGS[@]:2}") | ||
| 1576 | ;; | ||
| 1577 | -*) | ||
| 1578 | # Unknown option - stop parsing, rest goes to container | ||
| 1579 | break | ||
| 1580 | ;; | ||
| 1581 | *) | ||
| 1582 | # Not an option - this is the image name | ||
| 1583 | break | ||
| 1584 | ;; | ||
| 1585 | esac | ||
| 1586 | done | ||
| 1587 | |||
| 1588 | # Volume mounts require daemon mode | ||
| 1589 | if [ "$HAS_VOLUMES" = "true" ] && ! daemon_is_running; then | ||
| 1590 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume mounts require daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2 | ||
| 1591 | exit 1 | ||
| 1592 | fi | ||
| 1593 | |||
| 1594 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 1595 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} vrun requires <image> [command] [args...]" >&2 | ||
| 1596 | echo "Usage: $VCONTAINER_RUNTIME_NAME vrun [options] <image> [command] [args...]" >&2 | ||
| 1597 | echo "" >&2 | ||
| 1598 | echo "Options:" >&2 | ||
| 1599 | echo " -it, -i, -t Interactive mode with TTY" >&2 | ||
| 1600 | echo " --no-network Disable networking" >&2 | ||
| 1601 | echo " -p <host>:<container> Forward port" >&2 | ||
| 1602 | echo " -v <host>:<container> Mount host directory in container" >&2 | ||
| 1603 | echo "" >&2 | ||
| 1604 | echo "Examples:" >&2 | ||
| 1605 | echo " $VCONTAINER_RUNTIME_NAME vrun alpine /bin/ls -la" >&2 | ||
| 1606 | echo " $VCONTAINER_RUNTIME_NAME vrun -it alpine /bin/sh" >&2 | ||
| 1607 | echo " $VCONTAINER_RUNTIME_NAME vrun -p 8080:80 nginx:latest" >&2 | ||
| 1608 | echo " $VCONTAINER_RUNTIME_NAME vrun -v /tmp/data:/data alpine cat /data/file.txt" >&2 | ||
| 1609 | exit 1 | ||
| 1610 | fi | ||
| 1611 | |||
| 1612 | IMAGE_NAME="${COMMAND_ARGS[0]}" | ||
| 1613 | CONTAINER_CMD="" | ||
| 1614 | |||
| 1615 | # Build command from remaining args | ||
| 1616 | for ((i=1; i<${#COMMAND_ARGS[@]}; i++)); do | ||
| 1617 | if [ -n "$CONTAINER_CMD" ]; then | ||
| 1618 | CONTAINER_CMD="$CONTAINER_CMD ${COMMAND_ARGS[$i]}" | ||
| 1619 | else | ||
| 1620 | CONTAINER_CMD="${COMMAND_ARGS[$i]}" | ||
| 1621 | fi | ||
| 1622 | done | ||
| 1623 | |||
| 1624 | # Prepare volume mounts if any | ||
| 1625 | VOLUME_OPTS="" | ||
| 1626 | if [ "$HAS_VOLUMES" = "true" ]; then | ||
| 1627 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Preparing volume mounts..." >&2 | ||
| 1628 | |||
| 1629 | # Parse and prepare volumes (transforms host paths to guest paths) | ||
| 1630 | parse_and_prepare_volumes "${VRUN_VOLUMES[@]}" || { | ||
| 1631 | cleanup_volumes | ||
| 1632 | exit 1 | ||
| 1633 | } | ||
| 1634 | |||
| 1635 | # Build volume options string from transformed args | ||
| 1636 | VOLUME_OPTS="${TRANSFORMED_VOLUME_ARGS[*]}" | ||
| 1637 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Volume options: $VOLUME_OPTS" >&2 | ||
| 1638 | fi | ||
| 1639 | |||
| 1640 | # Build runtime run command | ||
| 1641 | RUNTIME_RUN_OPTS="--rm" | ||
| 1642 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 1643 | RUNTIME_RUN_OPTS="$RUNTIME_RUN_OPTS -it" | ||
| 1644 | fi | ||
| 1645 | # Use host networking when enabled (container shares VM's network stack) | ||
| 1646 | # This is needed because Docker runs with --bridge=none | ||
| 1647 | if [ "$NETWORK" = "true" ]; then | ||
| 1648 | RUNTIME_RUN_OPTS="$RUNTIME_RUN_OPTS --network=host --dns=10.0.2.3 --dns=8.8.8.8" | ||
| 1649 | fi | ||
| 1650 | |||
| 1651 | # Add volume mounts | ||
| 1652 | if [ -n "$VOLUME_OPTS" ]; then | ||
| 1653 | RUNTIME_RUN_OPTS="$RUNTIME_RUN_OPTS $VOLUME_OPTS" | ||
| 1654 | fi | ||
| 1655 | |||
| 1656 | if [ -n "$CONTAINER_CMD" ]; then | ||
| 1657 | # Clear entrypoint when command provided - ensures command runs directly | ||
| 1658 | # without being passed to image's entrypoint (e.g., prevents 'sh /bin/echo') | ||
| 1659 | RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run $RUNTIME_RUN_OPTS --entrypoint '' $IMAGE_NAME $CONTAINER_CMD" | ||
| 1660 | else | ||
| 1661 | RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run $RUNTIME_RUN_OPTS $IMAGE_NAME" | ||
| 1662 | fi | ||
| 1663 | |||
| 1664 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Runtime command: $RUNTIME_CMD" >&2 | ||
| 1665 | |||
| 1666 | # Use daemon mode for non-interactive runs | ||
| 1667 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 1668 | # Interactive mode with volumes still needs to stop daemon (volumes use share dir) | ||
| 1669 | # Interactive mode without volumes can use daemon_interactive (faster) | ||
| 1670 | if [ "$HAS_VOLUMES" = "false" ] && daemon_is_running; then | ||
| 1671 | # Use daemon interactive mode - keeps daemon running | ||
| 1672 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2 | ||
| 1673 | RUNNER_ARGS=$(build_runner_args) | ||
| 1674 | "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$RUNTIME_CMD" | ||
| 1675 | exit $? | ||
| 1676 | else | ||
| 1677 | # Fall back to regular QEMU for interactive (stop daemon if running) | ||
| 1678 | DAEMON_WAS_RUNNING=false | ||
| 1679 | if daemon_is_running; then | ||
| 1680 | DAEMON_WAS_RUNNING=true | ||
| 1681 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping daemon for interactive mode..." >&2 | ||
| 1682 | "$RUNNER" --state-dir "${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" --daemon-stop >/dev/null 2>&1 || true | ||
| 1683 | sleep 1 | ||
| 1684 | fi | ||
| 1685 | RUNNER_ARGS=$(build_runner_args) | ||
| 1686 | "$RUNNER" $RUNNER_ARGS -- "$RUNTIME_CMD" | ||
| 1687 | VRUN_EXIT=$? | ||
| 1688 | |||
| 1689 | # Sync volumes back after container exits | ||
| 1690 | if [ "$HAS_VOLUMES" = "true" ]; then | ||
| 1691 | sync_volumes_back | ||
| 1692 | cleanup_volumes | ||
| 1693 | fi | ||
| 1694 | |||
| 1695 | # Restart daemon if it was running before | ||
| 1696 | if [ "$DAEMON_WAS_RUNNING" = "true" ]; then | ||
| 1697 | echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Restarting daemon..." >&2 | ||
| 1698 | "$RUNNER" $RUNNER_ARGS --daemon-start >/dev/null 2>&1 || true | ||
| 1699 | fi | ||
| 1700 | |||
| 1701 | exit $VRUN_EXIT | ||
| 1702 | fi | ||
| 1703 | else | ||
| 1704 | # Non-interactive can use daemon mode | ||
| 1705 | run_runtime_command "$RUNTIME_CMD" | ||
| 1706 | VRUN_EXIT=$? | ||
| 1707 | |||
| 1708 | # Sync volumes back after container exits | ||
| 1709 | if [ "$HAS_VOLUMES" = "true" ]; then | ||
| 1710 | sync_volumes_back | ||
| 1711 | cleanup_volumes | ||
| 1712 | fi | ||
| 1713 | |||
| 1714 | exit $VRUN_EXIT | ||
| 1715 | fi | ||
| 1716 | ;; | ||
| 1717 | |||
| 1718 | run) | ||
| 1719 | # Runtime run command - mirrors 'docker/podman run' syntax | ||
| 1720 | # Usage: <tool> run [options] <image> [command] | ||
| 1721 | # Automatically prepends 'runtime run' to the arguments | ||
| 1722 | # Supports volume mounts with -v (requires daemon mode) | ||
| 1723 | if [ ${#COMMAND_ARGS[@]} -eq 0 ]; then | ||
| 1724 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} run requires an image" >&2 | ||
| 1725 | echo "Usage: $VCONTAINER_RUNTIME_NAME run [options] <image> [command]" >&2 | ||
| 1726 | echo "" >&2 | ||
| 1727 | echo "Examples:" >&2 | ||
| 1728 | echo " $VCONTAINER_RUNTIME_NAME run alpine /bin/echo hello" >&2 | ||
| 1729 | echo " $VCONTAINER_RUNTIME_NAME run -it alpine /bin/sh" >&2 | ||
| 1730 | echo " $VCONTAINER_RUNTIME_NAME run --rm -e FOO=bar myapp:latest" >&2 | ||
| 1731 | echo " $VCONTAINER_RUNTIME_NAME run -v /tmp/data:/data alpine cat /data/file.txt" >&2 | ||
| 1732 | exit 1 | ||
| 1733 | fi | ||
| 1734 | |||
| 1735 | # Check if any volume mounts are present | ||
| 1736 | RUN_HAS_VOLUMES=false | ||
| 1737 | for arg in "${COMMAND_ARGS[@]}"; do | ||
| 1738 | if [ "$arg" = "-v" ] || [ "$arg" = "--volume" ]; then | ||
| 1739 | RUN_HAS_VOLUMES=true | ||
| 1740 | break | ||
| 1741 | fi | ||
| 1742 | done | ||
| 1743 | |||
| 1744 | # Volume mounts require daemon mode | ||
| 1745 | if [ "$RUN_HAS_VOLUMES" = "true" ] && ! daemon_is_running; then | ||
| 1746 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume mounts require daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2 | ||
| 1747 | exit 1 | ||
| 1748 | fi | ||
| 1749 | |||
| 1750 | # Transform volume mounts if present | ||
| 1751 | if [ "$RUN_HAS_VOLUMES" = "true" ]; then | ||
| 1752 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Preparing volume mounts for run command..." >&2 | ||
| 1753 | |||
| 1754 | # Parse and prepare volumes (transforms host paths to guest paths) | ||
| 1755 | parse_and_prepare_volumes "${COMMAND_ARGS[@]}" || { | ||
| 1756 | cleanup_volumes | ||
| 1757 | exit 1 | ||
| 1758 | } | ||
| 1759 | # Update COMMAND_ARGS with transformed values | ||
| 1760 | COMMAND_ARGS=("${TRANSFORMED_VOLUME_ARGS[@]}") | ||
| 1761 | fi | ||
| 1762 | |||
| 1763 | # Build runtime run command from args | ||
| 1764 | # Note: -it may have been consumed by global parser, so add it back if INTERACTIVE is set | ||
| 1765 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 1766 | RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run -it ${COMMAND_ARGS[*]}" | ||
| 1767 | else | ||
| 1768 | RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run ${COMMAND_ARGS[*]}" | ||
| 1769 | fi | ||
| 1770 | |||
| 1771 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 1772 | # Interactive mode with volumes still needs to stop daemon (volumes use share dir) | ||
| 1773 | # Interactive mode without volumes can use daemon_interactive (faster) | ||
| 1774 | if [ "$RUN_HAS_VOLUMES" = "false" ] && daemon_is_running; then | ||
| 1775 | # Use daemon interactive mode - keeps daemon running | ||
| 1776 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2 | ||
| 1777 | RUNNER_ARGS=$(build_runner_args) | ||
| 1778 | "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$RUNTIME_CMD" | ||
| 1779 | exit $? | ||
| 1780 | else | ||
| 1781 | # Fall back to regular QEMU for interactive (stop daemon if running) | ||
| 1782 | DAEMON_WAS_RUNNING=false | ||
| 1783 | if daemon_is_running; then | ||
| 1784 | DAEMON_WAS_RUNNING=true | ||
| 1785 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping daemon for interactive mode..." >&2 | ||
| 1786 | "$RUNNER" --state-dir "${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" --daemon-stop >/dev/null 2>&1 || true | ||
| 1787 | sleep 1 | ||
| 1788 | fi | ||
| 1789 | RUNNER_ARGS=$(build_runner_args) | ||
| 1790 | "$RUNNER" $RUNNER_ARGS -- "$RUNTIME_CMD" | ||
| 1791 | RUN_EXIT=$? | ||
| 1792 | |||
| 1793 | # Sync volumes back after container exits | ||
| 1794 | if [ "$RUN_HAS_VOLUMES" = "true" ]; then | ||
| 1795 | sync_volumes_back | ||
| 1796 | cleanup_volumes | ||
| 1797 | fi | ||
| 1798 | |||
| 1799 | # Restart daemon if it was running before | ||
| 1800 | if [ "$DAEMON_WAS_RUNNING" = "true" ]; then | ||
| 1801 | echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Restarting daemon..." >&2 | ||
| 1802 | "$RUNNER" $RUNNER_ARGS --daemon-start >/dev/null 2>&1 || true | ||
| 1803 | fi | ||
| 1804 | |||
| 1805 | exit $RUN_EXIT | ||
| 1806 | fi | ||
| 1807 | else | ||
| 1808 | # Non-interactive - use daemon mode when available | ||
| 1809 | run_runtime_command "$RUNTIME_CMD" | ||
| 1810 | RUN_EXIT=$? | ||
| 1811 | |||
| 1812 | # Sync volumes back after container exits | ||
| 1813 | if [ "$RUN_HAS_VOLUMES" = "true" ]; then | ||
| 1814 | sync_volumes_back | ||
| 1815 | cleanup_volumes | ||
| 1816 | fi | ||
| 1817 | |||
| 1818 | exit $RUN_EXIT | ||
| 1819 | fi | ||
| 1820 | ;; | ||
| 1821 | |||
| 1822 | # Memory resident subcommand: <tool> memres start|stop|restart|status | ||
| 1823 | # vmemres is the preferred name (v prefix for tool-specific commands) | ||
| 1824 | memres|vmemres) | ||
| 1825 | if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then | ||
| 1826 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} memres requires a subcommand: start, stop, restart, status, list" >&2 | ||
| 1827 | exit 1 | ||
| 1828 | fi | ||
| 1829 | |||
| 1830 | MEMRES_CMD="${COMMAND_ARGS[0]}" | ||
| 1831 | |||
| 1832 | # Parse memres-specific options (after the subcommand) | ||
| 1833 | MEMRES_ARGS=("${COMMAND_ARGS[@]:1}") | ||
| 1834 | i=0 | ||
| 1835 | while [ $i -lt ${#MEMRES_ARGS[@]} ]; do | ||
| 1836 | arg="${MEMRES_ARGS[$i]}" | ||
| 1837 | case "$arg" in | ||
| 1838 | -p|--publish) | ||
| 1839 | # Port forward: -p 8080:80 or -p 8080:80/tcp | ||
| 1840 | i=$((i + 1)) | ||
| 1841 | if [ $i -ge ${#MEMRES_ARGS[@]} ]; then | ||
| 1842 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -p requires <host_port>:<container_port>" >&2 | ||
| 1843 | exit 1 | ||
| 1844 | fi | ||
| 1845 | PORT_FORWARDS+=("${MEMRES_ARGS[$i]}") | ||
| 1846 | ;; | ||
| 1847 | esac | ||
| 1848 | i=$((i + 1)) | ||
| 1849 | done | ||
| 1850 | |||
| 1851 | RUNNER_ARGS=$(build_runner_args) | ||
| 1852 | |||
| 1853 | case "$MEMRES_CMD" in | ||
| 1854 | start) | ||
| 1855 | if daemon_is_running; then | ||
| 1856 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} A memres instance is already running for $TARGET_ARCH" | ||
| 1857 | echo "" | ||
| 1858 | "$RUNNER" $RUNNER_ARGS --daemon-status | ||
| 1859 | echo "" | ||
| 1860 | echo "Options:" | ||
| 1861 | echo " 1) Restart with new settings (stops current instance)" | ||
| 1862 | echo " 2) Start additional instance with different --state-dir" | ||
| 1863 | echo " 3) Cancel" | ||
| 1864 | echo "" | ||
| 1865 | read -p "Choice [1-3]: " choice | ||
| 1866 | case "$choice" in | ||
| 1867 | 1) | ||
| 1868 | echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Restarting memres..." | ||
| 1869 | "$RUNNER" $RUNNER_ARGS --daemon-stop | ||
| 1870 | sleep 1 | ||
| 1871 | "$RUNNER" $RUNNER_ARGS --daemon-start | ||
| 1872 | ;; | ||
| 1873 | 2) | ||
| 1874 | echo "" | ||
| 1875 | echo "To start an additional instance, use -I <name>:" | ||
| 1876 | echo " $VCONTAINER_RUNTIME_NAME -I web memres start -p 8080:80" | ||
| 1877 | echo " $VCONTAINER_RUNTIME_NAME -I api memres start -p 3000:3000" | ||
| 1878 | echo "" | ||
| 1879 | echo "Then interact with it:" | ||
| 1880 | echo " $VCONTAINER_RUNTIME_NAME -I web images" | ||
| 1881 | echo "" | ||
| 1882 | exit 0 | ||
| 1883 | ;; | ||
| 1884 | *) | ||
| 1885 | echo "Cancelled." | ||
| 1886 | exit 0 | ||
| 1887 | ;; | ||
| 1888 | esac | ||
| 1889 | else | ||
| 1890 | "$RUNNER" $RUNNER_ARGS --daemon-start | ||
| 1891 | fi | ||
| 1892 | ;; | ||
| 1893 | stop) | ||
| 1894 | "$RUNNER" $RUNNER_ARGS --daemon-stop | ||
| 1895 | ;; | ||
| 1896 | restart) | ||
| 1897 | # Stop if running | ||
| 1898 | "$RUNNER" $RUNNER_ARGS --daemon-stop 2>/dev/null || true | ||
| 1899 | |||
| 1900 | # Clean if --clean was passed | ||
| 1901 | for arg in "${COMMAND_ARGS[@]:1}"; do | ||
| 1902 | if [ "$arg" = "--clean" ]; then | ||
| 1903 | CLEAN_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" | ||
| 1904 | if [ -d "$CLEAN_DIR" ]; then | ||
| 1905 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Cleaning state directory: $CLEAN_DIR" | ||
| 1906 | rm -rf "$CLEAN_DIR" | ||
| 1907 | fi | ||
| 1908 | break | ||
| 1909 | fi | ||
| 1910 | done | ||
| 1911 | |||
| 1912 | # Start | ||
| 1913 | "$RUNNER" $RUNNER_ARGS --daemon-start | ||
| 1914 | ;; | ||
| 1915 | status) | ||
| 1916 | "$RUNNER" $RUNNER_ARGS --daemon-status | ||
| 1917 | ;; | ||
| 1918 | list) | ||
| 1919 | # Show all running memres instances | ||
| 1920 | echo "Running memres instances:" | ||
| 1921 | echo "" | ||
| 1922 | found=0 | ||
| 1923 | tracked_pids="" | ||
| 1924 | for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do | ||
| 1925 | [ -f "$pid_file" ] || continue | ||
| 1926 | pid=$(cat "$pid_file" 2>/dev/null) | ||
| 1927 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then | ||
| 1928 | instance_dir=$(dirname "$pid_file") | ||
| 1929 | instance_name=$(basename "$instance_dir") | ||
| 1930 | echo " ${CYAN}$instance_name${NC}" | ||
| 1931 | echo " PID: $pid" | ||
| 1932 | echo " State: $instance_dir" | ||
| 1933 | if [ -f "$instance_dir/qemu.log" ]; then | ||
| 1934 | # Try to extract port forwards from qemu command line | ||
| 1935 | ports=$(grep -o 'hostfwd=[^,]*' "$instance_dir/qemu.log" 2>/dev/null | sed 's/hostfwd=tcp:://g; s/-/:/' | tr '\n' ' ') | ||
| 1936 | [ -n "$ports" ] && echo " Ports: $ports" | ||
| 1937 | fi | ||
| 1938 | echo "" | ||
| 1939 | found=$((found + 1)) | ||
| 1940 | tracked_pids="$tracked_pids $pid" | ||
| 1941 | fi | ||
| 1942 | done | ||
| 1943 | if [ $found -eq 0 ]; then | ||
| 1944 | echo " (none)" | ||
| 1945 | fi | ||
| 1946 | |||
| 1947 | # Check for zombie/orphan QEMU processes (vdkr or vpdmn) | ||
| 1948 | echo "" | ||
| 1949 | echo "Checking for orphan QEMU processes..." | ||
| 1950 | zombies="" | ||
| 1951 | for qemu_pid in $(pgrep -f "qemu-system.*runtime=(docker|podman)" 2>/dev/null || true); do | ||
| 1952 | # Skip if this PID is already tracked | ||
| 1953 | if echo "$tracked_pids" | grep -qw "$qemu_pid"; then | ||
| 1954 | continue | ||
| 1955 | fi | ||
| 1956 | # Also check other tool's state dirs | ||
| 1957 | other_tracked=false | ||
| 1958 | for vpid_file in "$OTHER_STATE_DIR"/*/daemon.pid; do | ||
| 1959 | [ -f "$vpid_file" ] || continue | ||
| 1960 | vpid=$(cat "$vpid_file" 2>/dev/null) | ||
| 1961 | if [ "$vpid" = "$qemu_pid" ]; then | ||
| 1962 | other_tracked=true | ||
| 1963 | break | ||
| 1964 | fi | ||
| 1965 | done | ||
| 1966 | if [ "$other_tracked" = "true" ]; then | ||
| 1967 | continue | ||
| 1968 | fi | ||
| 1969 | zombies="$zombies $qemu_pid" | ||
| 1970 | done | ||
| 1971 | |||
| 1972 | if [ -n "$zombies" ]; then | ||
| 1973 | echo "" | ||
| 1974 | echo -e "${YELLOW}Orphan QEMU processes found:${NC}" | ||
| 1975 | for zpid in $zombies; do | ||
| 1976 | # Extract runtime from cmdline | ||
| 1977 | cmdline=$(cat /proc/$zpid/cmdline 2>/dev/null | tr '\0' ' ') | ||
| 1978 | runtime=$(echo "$cmdline" | grep -o 'runtime=[a-z]*' | cut -d= -f2) | ||
| 1979 | state_dir=$(echo "$cmdline" | grep -o 'path=[^,]*daemon.sock' | sed 's|path=||; s|/daemon.sock||') | ||
| 1980 | echo "" | ||
| 1981 | echo " ${RED}PID $zpid${NC} (${runtime:-unknown})" | ||
| 1982 | [ -n "$state_dir" ] && echo " State: $state_dir" | ||
| 1983 | echo " Kill with: kill $zpid" | ||
| 1984 | done | ||
| 1985 | echo "" | ||
| 1986 | echo -e "To kill all orphans: ${CYAN}kill$zombies${NC}" | ||
| 1987 | else | ||
| 1988 | echo " (no orphans found)" | ||
| 1989 | fi | ||
| 1990 | ;; | ||
| 1991 | *) | ||
| 1992 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown memres subcommand: $MEMRES_CMD" >&2 | ||
| 1993 | echo "Usage: $VCONTAINER_RUNTIME_NAME memres start|stop|restart|status|list" >&2 | ||
| 1994 | exit 1 | ||
| 1995 | ;; | ||
| 1996 | esac | ||
| 1997 | ;; | ||
| 1998 | |||
| 1999 | *) | ||
| 2000 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown command: $COMMAND" >&2 | ||
| 2001 | echo "Run '$VCONTAINER_RUNTIME_NAME --help' for usage" >&2 | ||
| 2002 | exit 1 | ||
| 2003 | ;; | ||
| 2004 | esac | ||
diff --git a/recipes-containers/vcontainer/files/vcontainer-init-common.sh b/recipes-containers/vcontainer/files/vcontainer-init-common.sh new file mode 100755 index 00000000..872508db --- /dev/null +++ b/recipes-containers/vcontainer/files/vcontainer-init-common.sh | |||
| @@ -0,0 +1,537 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | # vcontainer-init-common.sh | ||
| 7 | # Shared init functions for vdkr and vpdmn | ||
| 8 | # | ||
| 9 | # This file is sourced by vdkr-init.sh and vpdmn-init.sh after they set: | ||
| 10 | # VCONTAINER_RUNTIME_NAME - Tool name (vdkr or vpdmn) | ||
| 11 | # VCONTAINER_RUNTIME_CMD - Container command (docker or podman) | ||
| 12 | # VCONTAINER_RUNTIME_PREFIX - Kernel param prefix (docker or podman) | ||
| 13 | # VCONTAINER_STATE_DIR - Storage directory (/var/lib/docker or /var/lib/containers/storage) | ||
| 14 | # VCONTAINER_SHARE_NAME - virtio-9p share name (vdkr_share or vpdmn_share) | ||
| 15 | # VCONTAINER_VERSION - Version string | ||
| 16 | |||
| 17 | # ============================================================================ | ||
| 18 | # Environment Setup | ||
| 19 | # ============================================================================ | ||
| 20 | |||
| 21 | setup_base_environment() { | ||
| 22 | export LD_LIBRARY_PATH="/lib:/lib64:/usr/lib:/usr/lib64" | ||
| 23 | export PATH="/bin:/sbin:/usr/bin:/usr/sbin" | ||
| 24 | export HOME="/root" | ||
| 25 | export USER="root" | ||
| 26 | export LOGNAME="root" | ||
| 27 | } | ||
| 28 | |||
| 29 | # ============================================================================ | ||
| 30 | # Filesystem Mounts | ||
| 31 | # ============================================================================ | ||
| 32 | |||
| 33 | mount_base_filesystems() { | ||
| 34 | # Mount essential filesystems if not already mounted | ||
| 35 | mountpoint -q /dev || mount -t devtmpfs devtmpfs /dev | ||
| 36 | mountpoint -q /proc || mount -t proc proc /proc | ||
| 37 | mountpoint -q /sys || mount -t sysfs sysfs /sys | ||
| 38 | |||
| 39 | # Mount devpts for pseudo-terminals (needed for interactive mode) | ||
| 40 | mkdir -p /dev/pts | ||
| 41 | mountpoint -q /dev/pts || mount -t devpts devpts /dev/pts | ||
| 42 | |||
| 43 | # Enable IP forwarding (container runtimes check this) | ||
| 44 | echo 1 > /proc/sys/net/ipv4/ip_forward | ||
| 45 | |||
| 46 | # Configure loopback interface | ||
| 47 | ip link set lo up | ||
| 48 | ip addr add 127.0.0.1/8 dev lo 2>/dev/null || true | ||
| 49 | } | ||
| 50 | |||
| 51 | mount_tmpfs_dirs() { | ||
| 52 | # These are tmpfs (rootfs is read-only) | ||
| 53 | mount -t tmpfs tmpfs /tmp | ||
| 54 | mount -t tmpfs tmpfs /run | ||
| 55 | mount -t tmpfs tmpfs /mnt | ||
| 56 | mount -t tmpfs tmpfs /var/run 2>/dev/null || true | ||
| 57 | mount -t tmpfs tmpfs /var/tmp 2>/dev/null || true | ||
| 58 | |||
| 59 | # Create a writable /etc using tmpfs overlay | ||
| 60 | mkdir -p /tmp/etc-overlay | ||
| 61 | cp -a /etc/* /tmp/etc-overlay/ 2>/dev/null || true | ||
| 62 | mount --bind /tmp/etc-overlay /etc | ||
| 63 | } | ||
| 64 | |||
| 65 | setup_cgroups() { | ||
| 66 | mkdir -p /sys/fs/cgroup | ||
| 67 | mount -t cgroup2 none /sys/fs/cgroup 2>/dev/null || { | ||
| 68 | mount -t tmpfs cgroup /sys/fs/cgroup 2>/dev/null || true | ||
| 69 | for subsys in devices memory cpu,cpuacct blkio net_cls freezer pids; do | ||
| 70 | subsys_dir=$(echo $subsys | cut -d, -f1) | ||
| 71 | mkdir -p /sys/fs/cgroup/$subsys_dir | ||
| 72 | mount -t cgroup -o $subsys cgroup /sys/fs/cgroup/$subsys_dir 2>/dev/null || true | ||
| 73 | done | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | # ============================================================================ | ||
| 78 | # Quiet Boot / Logging | ||
| 79 | # ============================================================================ | ||
| 80 | |||
| 81 | # Check for interactive mode (suppresses boot messages) | ||
| 82 | check_quiet_boot() { | ||
| 83 | QUIET_BOOT=0 | ||
| 84 | for param in $(cat /proc/cmdline); do | ||
| 85 | case "$param" in | ||
| 86 | ${VCONTAINER_RUNTIME_PREFIX}_interactive=1) QUIET_BOOT=1 ;; | ||
| 87 | esac | ||
| 88 | done | ||
| 89 | } | ||
| 90 | |||
| 91 | # Logging function - suppresses output in interactive mode | ||
| 92 | log() { | ||
| 93 | [ "$QUIET_BOOT" = "0" ] && echo "$@" | ||
| 94 | } | ||
| 95 | |||
| 96 | # ============================================================================ | ||
| 97 | # Kernel Command Line Parsing | ||
| 98 | # ============================================================================ | ||
| 99 | |||
| 100 | parse_cmdline() { | ||
| 101 | # Initialize variables with defaults | ||
| 102 | RUNTIME_CMD_B64="" | ||
| 103 | RUNTIME_INPUT="none" | ||
| 104 | RUNTIME_OUTPUT="text" | ||
| 105 | RUNTIME_STATE="none" | ||
| 106 | RUNTIME_NETWORK="0" | ||
| 107 | RUNTIME_INTERACTIVE="0" | ||
| 108 | RUNTIME_DAEMON="0" | ||
| 109 | |||
| 110 | for param in $(cat /proc/cmdline); do | ||
| 111 | case "$param" in | ||
| 112 | ${VCONTAINER_RUNTIME_PREFIX}_cmd=*) | ||
| 113 | RUNTIME_CMD_B64="${param#${VCONTAINER_RUNTIME_PREFIX}_cmd=}" | ||
| 114 | ;; | ||
| 115 | ${VCONTAINER_RUNTIME_PREFIX}_input=*) | ||
| 116 | RUNTIME_INPUT="${param#${VCONTAINER_RUNTIME_PREFIX}_input=}" | ||
| 117 | ;; | ||
| 118 | ${VCONTAINER_RUNTIME_PREFIX}_output=*) | ||
| 119 | RUNTIME_OUTPUT="${param#${VCONTAINER_RUNTIME_PREFIX}_output=}" | ||
| 120 | ;; | ||
| 121 | ${VCONTAINER_RUNTIME_PREFIX}_state=*) | ||
| 122 | RUNTIME_STATE="${param#${VCONTAINER_RUNTIME_PREFIX}_state=}" | ||
| 123 | ;; | ||
| 124 | ${VCONTAINER_RUNTIME_PREFIX}_network=*) | ||
| 125 | RUNTIME_NETWORK="${param#${VCONTAINER_RUNTIME_PREFIX}_network=}" | ||
| 126 | ;; | ||
| 127 | ${VCONTAINER_RUNTIME_PREFIX}_interactive=*) | ||
| 128 | RUNTIME_INTERACTIVE="${param#${VCONTAINER_RUNTIME_PREFIX}_interactive=}" | ||
| 129 | ;; | ||
| 130 | ${VCONTAINER_RUNTIME_PREFIX}_daemon=*) | ||
| 131 | RUNTIME_DAEMON="${param#${VCONTAINER_RUNTIME_PREFIX}_daemon=}" | ||
| 132 | ;; | ||
| 133 | esac | ||
| 134 | done | ||
| 135 | |||
| 136 | # Decode the command (not required for daemon mode) | ||
| 137 | RUNTIME_CMD="" | ||
| 138 | if [ -n "$RUNTIME_CMD_B64" ]; then | ||
| 139 | RUNTIME_CMD=$(echo "$RUNTIME_CMD_B64" | base64 -d 2>/dev/null) | ||
| 140 | fi | ||
| 141 | |||
| 142 | # Require command for non-daemon mode | ||
| 143 | if [ -z "$RUNTIME_CMD" ] && [ "$RUNTIME_DAEMON" != "1" ]; then | ||
| 144 | echo "===ERROR===" | ||
| 145 | echo "No command provided (${VCONTAINER_RUNTIME_PREFIX}_cmd= missing)" | ||
| 146 | sleep 2 | ||
| 147 | reboot -f | ||
| 148 | fi | ||
| 149 | |||
| 150 | log "Command: $RUNTIME_CMD" | ||
| 151 | log "Input type: $RUNTIME_INPUT" | ||
| 152 | log "Output type: $RUNTIME_OUTPUT" | ||
| 153 | log "State type: $RUNTIME_STATE" | ||
| 154 | } | ||
| 155 | |||
| 156 | # ============================================================================ | ||
| 157 | # Disk Detection | ||
| 158 | # ============================================================================ | ||
| 159 | |||
| 160 | detect_disks() { | ||
| 161 | log "Waiting for block devices..." | ||
| 162 | sleep 2 | ||
| 163 | |||
| 164 | log "Block devices:" | ||
| 165 | [ "$QUIET_BOOT" = "0" ] && ls -la /dev/vd* 2>/dev/null || log "No /dev/vd* devices" | ||
| 166 | |||
| 167 | # Determine which disk is input and which is state | ||
| 168 | # Drive layout (rootfs.img is always /dev/vda, mounted by preinit as /): | ||
| 169 | # /dev/vda = rootfs.img (already mounted as /) | ||
| 170 | # /dev/vdb = input (if present) | ||
| 171 | # /dev/vdc = state (if both input and state present) | ||
| 172 | # /dev/vdb = state (if only state, no input) | ||
| 173 | |||
| 174 | INPUT_DISK="" | ||
| 175 | STATE_DISK="" | ||
| 176 | |||
| 177 | if [ "$RUNTIME_INPUT" != "none" ] && [ "$RUNTIME_STATE" = "disk" ]; then | ||
| 178 | # Both present: rootfs=vda, input=vdb, state=vdc | ||
| 179 | INPUT_DISK="/dev/vdb" | ||
| 180 | STATE_DISK="/dev/vdc" | ||
| 181 | elif [ "$RUNTIME_STATE" = "disk" ]; then | ||
| 182 | # Only state: rootfs=vda, state=vdb | ||
| 183 | STATE_DISK="/dev/vdb" | ||
| 184 | elif [ "$RUNTIME_INPUT" != "none" ]; then | ||
| 185 | # Only input: rootfs=vda, input=vdb | ||
| 186 | INPUT_DISK="/dev/vdb" | ||
| 187 | fi | ||
| 188 | } | ||
| 189 | |||
| 190 | # ============================================================================ | ||
| 191 | # Input Disk Handling | ||
| 192 | # ============================================================================ | ||
| 193 | |||
| 194 | mount_input_disk() { | ||
| 195 | mkdir -p /mnt/input | ||
| 196 | |||
| 197 | if [ -n "$INPUT_DISK" ] && [ -b "$INPUT_DISK" ]; then | ||
| 198 | log "Mounting input from $INPUT_DISK..." | ||
| 199 | if mount -t ext4 "$INPUT_DISK" /mnt/input 2>&1; then | ||
| 200 | log "SUCCESS: Mounted $INPUT_DISK" | ||
| 201 | log "Input contents:" | ||
| 202 | [ "$QUIET_BOOT" = "0" ] && ls -la /mnt/input/ | ||
| 203 | else | ||
| 204 | log "WARNING: Failed to mount $INPUT_DISK, continuing without input" | ||
| 205 | RUNTIME_INPUT="none" | ||
| 206 | fi | ||
| 207 | elif [ "$RUNTIME_INPUT" != "none" ]; then | ||
| 208 | log "WARNING: No input device found, continuing without input" | ||
| 209 | RUNTIME_INPUT="none" | ||
| 210 | fi | ||
| 211 | } | ||
| 212 | |||
| 213 | # ============================================================================ | ||
| 214 | # Network Configuration | ||
| 215 | # ============================================================================ | ||
| 216 | |||
| 217 | configure_networking() { | ||
| 218 | if [ "$RUNTIME_NETWORK" = "1" ]; then | ||
| 219 | log "Configuring network..." | ||
| 220 | |||
| 221 | # Find the network interface (usually eth0 or enp0s* with virtio) | ||
| 222 | NET_IFACE="" | ||
| 223 | for iface in eth0 enp0s2 enp0s3 ens3; do | ||
| 224 | if [ -d "/sys/class/net/$iface" ]; then | ||
| 225 | NET_IFACE="$iface" | ||
| 226 | break | ||
| 227 | fi | ||
| 228 | done | ||
| 229 | |||
| 230 | if [ -n "$NET_IFACE" ]; then | ||
| 231 | log "Found network interface: $NET_IFACE" | ||
| 232 | |||
| 233 | # Bring up the interface | ||
| 234 | ip link set "$NET_IFACE" up | ||
| 235 | |||
| 236 | # QEMU slirp provides: | ||
| 237 | # Guest IP: 10.0.2.15/24 | ||
| 238 | # Gateway: 10.0.2.2 | ||
| 239 | # DNS: 10.0.2.3 | ||
| 240 | ip addr add 10.0.2.15/24 dev "$NET_IFACE" | ||
| 241 | ip route add default via 10.0.2.2 | ||
| 242 | |||
| 243 | # Configure DNS | ||
| 244 | mkdir -p /etc | ||
| 245 | rm -f /etc/resolv.conf | ||
| 246 | cat > /etc/resolv.conf << 'DNSEOF' | ||
| 247 | nameserver 10.0.2.3 | ||
| 248 | nameserver 8.8.8.8 | ||
| 249 | nameserver 1.1.1.1 | ||
| 250 | DNSEOF | ||
| 251 | |||
| 252 | sleep 1 | ||
| 253 | |||
| 254 | # Verify connectivity | ||
| 255 | log "Testing network connectivity..." | ||
| 256 | if ping -c 1 -W 3 10.0.2.2 >/dev/null 2>&1; then | ||
| 257 | log " Gateway (10.0.2.2): OK" | ||
| 258 | else | ||
| 259 | log " Gateway (10.0.2.2): FAILED" | ||
| 260 | fi | ||
| 261 | |||
| 262 | if ping -c 1 -W 3 8.8.8.8 >/dev/null 2>&1; then | ||
| 263 | log " External (8.8.8.8): OK" | ||
| 264 | else | ||
| 265 | log " External (8.8.8.8): FAILED (may be filtered)" | ||
| 266 | fi | ||
| 267 | |||
| 268 | log "Network configured: $NET_IFACE (10.0.2.15)" | ||
| 269 | [ "$QUIET_BOOT" = "0" ] && ip addr show "$NET_IFACE" | ||
| 270 | [ "$QUIET_BOOT" = "0" ] && ip route | ||
| 271 | [ "$QUIET_BOOT" = "0" ] && cat /etc/resolv.conf | ||
| 272 | else | ||
| 273 | log "WARNING: No network interface found" | ||
| 274 | [ "$QUIET_BOOT" = "0" ] && ls /sys/class/net/ | ||
| 275 | fi | ||
| 276 | else | ||
| 277 | log "Networking: disabled" | ||
| 278 | fi | ||
| 279 | } | ||
| 280 | |||
| 281 | # ============================================================================ | ||
| 282 | # Daemon Mode | ||
| 283 | # ============================================================================ | ||
| 284 | |||
| 285 | run_daemon_mode() { | ||
| 286 | log "=== Daemon Mode ===" | ||
| 287 | |||
| 288 | # Find the virtio-serial port for command channel | ||
| 289 | DAEMON_PORT="" | ||
| 290 | for port in /dev/vport0p1 /dev/vport1p1 /dev/vport2p1 /dev/virtio-ports/${VCONTAINER_RUNTIME_NAME} /dev/hvc1; do | ||
| 291 | if [ -c "$port" ]; then | ||
| 292 | DAEMON_PORT="$port" | ||
| 293 | log "Found virtio-serial port: $port" | ||
| 294 | break | ||
| 295 | fi | ||
| 296 | done | ||
| 297 | |||
| 298 | if [ -z "$DAEMON_PORT" ]; then | ||
| 299 | log "ERROR: Could not find virtio-serial port for daemon mode" | ||
| 300 | log "Available devices:" | ||
| 301 | ls -la /dev/hvc* /dev/vport* /dev/virtio-ports/ 2>/dev/null || true | ||
| 302 | sleep 5 | ||
| 303 | reboot -f | ||
| 304 | fi | ||
| 305 | |||
| 306 | log "Using virtio-serial port: $DAEMON_PORT" | ||
| 307 | |||
| 308 | # Mount virtio-9p shared directory for file I/O | ||
| 309 | mkdir -p /mnt/share | ||
| 310 | MOUNT_ERR=$(mount -t 9p -o trans=virtio,version=9p2000.L,cache=none ${VCONTAINER_SHARE_NAME} /mnt/share 2>&1) | ||
| 311 | if [ $? -eq 0 ]; then | ||
| 312 | log "Mounted virtio-9p share at /mnt/share" | ||
| 313 | else | ||
| 314 | log "WARNING: Could not mount virtio-9p share: $MOUNT_ERR" | ||
| 315 | log "Available filesystems:" | ||
| 316 | cat /proc/filesystems 2>/dev/null | head -20 | ||
| 317 | fi | ||
| 318 | |||
| 319 | # Open bidirectional FD to the virtio-serial port | ||
| 320 | exec 3<>"$DAEMON_PORT" | ||
| 321 | |||
| 322 | log "Daemon ready, waiting for commands..." | ||
| 323 | |||
| 324 | # Command loop | ||
| 325 | while true; do | ||
| 326 | CMD_B64="" | ||
| 327 | if read -r CMD_B64 <&3; then | ||
| 328 | log "Received: '$CMD_B64'" | ||
| 329 | # Handle special commands | ||
| 330 | case "$CMD_B64" in | ||
| 331 | "===PING===") | ||
| 332 | echo "===PONG===" | cat >&3 | ||
| 333 | continue | ||
| 334 | ;; | ||
| 335 | "===SHUTDOWN===") | ||
| 336 | log "Received shutdown command" | ||
| 337 | echo "===SHUTTING_DOWN===" | cat >&3 | ||
| 338 | break | ||
| 339 | ;; | ||
| 340 | esac | ||
| 341 | |||
| 342 | # Decode command | ||
| 343 | CMD=$(echo "$CMD_B64" | base64 -d 2>/dev/null) | ||
| 344 | if [ -z "$CMD" ]; then | ||
| 345 | printf "===ERROR===\nFailed to decode command\n===END===\n" | cat >&3 | ||
| 346 | continue | ||
| 347 | fi | ||
| 348 | |||
| 349 | # Check for interactive command | ||
| 350 | if echo "$CMD" | grep -q "^===INTERACTIVE==="; then | ||
| 351 | CMD="${CMD#===INTERACTIVE===}" | ||
| 352 | log "Interactive command: $CMD" | ||
| 353 | |||
| 354 | printf "===INTERACTIVE_READY===\n" >&3 | ||
| 355 | |||
| 356 | export TERM=linux | ||
| 357 | script -qf -c "$CMD" /dev/null <&3 >&3 2>&1 | ||
| 358 | INTERACTIVE_EXIT=$? | ||
| 359 | |||
| 360 | sleep 0.5 | ||
| 361 | printf "\n===INTERACTIVE_END=%d===\n" "$INTERACTIVE_EXIT" >&3 | ||
| 362 | |||
| 363 | log "Interactive command completed (exit: $INTERACTIVE_EXIT)" | ||
| 364 | continue | ||
| 365 | fi | ||
| 366 | |||
| 367 | # Check if command needs input from shared directory | ||
| 368 | NEEDS_INPUT=false | ||
| 369 | if echo "$CMD" | grep -q "^===USE_INPUT==="; then | ||
| 370 | NEEDS_INPUT=true | ||
| 371 | CMD="${CMD#===USE_INPUT===}" | ||
| 372 | log "Command needs input from shared directory" | ||
| 373 | fi | ||
| 374 | |||
| 375 | log "Executing: $CMD" | ||
| 376 | |||
| 377 | # Verify shared directory has content if needed | ||
| 378 | if [ "$NEEDS_INPUT" = "true" ]; then | ||
| 379 | if ! mountpoint -q /mnt/share; then | ||
| 380 | printf "===ERROR===\nvirtio-9p share not mounted\n===END===\n" | cat >&3 | ||
| 381 | continue | ||
| 382 | fi | ||
| 383 | if [ -z "$(ls -A /mnt/share 2>/dev/null)" ]; then | ||
| 384 | printf "===ERROR===\nShared directory is empty\n===END===\n" | cat >&3 | ||
| 385 | continue | ||
| 386 | fi | ||
| 387 | log "Shared directory contents:" | ||
| 388 | ls -la /mnt/share/ 2>/dev/null || true | ||
| 389 | fi | ||
| 390 | |||
| 391 | # Replace {INPUT} placeholder | ||
| 392 | INPUT_PATH="/mnt/share" | ||
| 393 | CMD=$(echo "$CMD" | sed "s|{INPUT}|$INPUT_PATH|g") | ||
| 394 | |||
| 395 | # Execute command | ||
| 396 | EXEC_OUTPUT="/tmp/daemon_output.txt" | ||
| 397 | EXEC_EXIT_CODE=0 | ||
| 398 | eval "$CMD" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$? | ||
| 399 | |||
| 400 | # Clean up shared directory | ||
| 401 | if [ "$NEEDS_INPUT" = "true" ]; then | ||
| 402 | log "Cleaning shared directory..." | ||
| 403 | rm -rf /mnt/share/* 2>/dev/null || true | ||
| 404 | fi | ||
| 405 | |||
| 406 | # Send response | ||
| 407 | { | ||
| 408 | echo "===OUTPUT_START===" | ||
| 409 | cat "$EXEC_OUTPUT" | ||
| 410 | echo "===OUTPUT_END===" | ||
| 411 | echo "===EXIT_CODE=$EXEC_EXIT_CODE===" | ||
| 412 | echo "===END===" | ||
| 413 | } | cat >&3 | ||
| 414 | |||
| 415 | log "Command completed (exit code: $EXEC_EXIT_CODE)" | ||
| 416 | else | ||
| 417 | sleep 1 | ||
| 418 | fi | ||
| 419 | done | ||
| 420 | |||
| 421 | exec 3>&- | ||
| 422 | log "Daemon shutting down..." | ||
| 423 | } | ||
| 424 | |||
| 425 | # ============================================================================ | ||
| 426 | # Command Execution (non-daemon mode) | ||
| 427 | # ============================================================================ | ||
| 428 | |||
| 429 | prepare_input_path() { | ||
| 430 | INPUT_PATH="" | ||
| 431 | if [ "$RUNTIME_INPUT" = "oci" ] && [ -d "/mnt/input" ]; then | ||
| 432 | INPUT_PATH="/mnt/input" | ||
| 433 | elif [ "$RUNTIME_INPUT" = "tar" ] && [ -d "/mnt/input" ]; then | ||
| 434 | INPUT_PATH=$(find /mnt/input -name "*.tar" -o -name "*.tar.gz" | head -n 1) | ||
| 435 | [ -z "$INPUT_PATH" ] && INPUT_PATH="/mnt/input" | ||
| 436 | elif [ "$RUNTIME_INPUT" = "dir" ]; then | ||
| 437 | INPUT_PATH="/mnt/input" | ||
| 438 | fi | ||
| 439 | export INPUT_PATH | ||
| 440 | } | ||
| 441 | |||
| 442 | execute_command() { | ||
| 443 | # Substitute {INPUT} placeholder | ||
| 444 | RUNTIME_CMD_FINAL=$(echo "$RUNTIME_CMD" | sed "s|{INPUT}|$INPUT_PATH|g") | ||
| 445 | |||
| 446 | log "=== Executing ${VCONTAINER_RUNTIME_CMD} Command ===" | ||
| 447 | log "Command: $RUNTIME_CMD_FINAL" | ||
| 448 | log "" | ||
| 449 | |||
| 450 | if [ "$RUNTIME_INTERACTIVE" = "1" ]; then | ||
| 451 | # Interactive mode | ||
| 452 | export TERM=linux | ||
| 453 | printf '\r\033[K' | ||
| 454 | eval "$RUNTIME_CMD_FINAL" | ||
| 455 | EXEC_EXIT_CODE=$? | ||
| 456 | else | ||
| 457 | # Non-interactive mode | ||
| 458 | EXEC_OUTPUT="/tmp/runtime_output.txt" | ||
| 459 | EXEC_EXIT_CODE=0 | ||
| 460 | eval "$RUNTIME_CMD_FINAL" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$? | ||
| 461 | |||
| 462 | log "Exit code: $EXEC_EXIT_CODE" | ||
| 463 | |||
| 464 | case "$RUNTIME_OUTPUT" in | ||
| 465 | text) | ||
| 466 | echo "===OUTPUT_START===" | ||
| 467 | cat "$EXEC_OUTPUT" | ||
| 468 | echo "===OUTPUT_END===" | ||
| 469 | echo "===EXIT_CODE=$EXEC_EXIT_CODE===" | ||
| 470 | ;; | ||
| 471 | |||
| 472 | tar) | ||
| 473 | if [ -f /tmp/output.tar ]; then | ||
| 474 | dmesg -n 1 | ||
| 475 | echo "===TAR_START===" | ||
| 476 | base64 /tmp/output.tar | ||
| 477 | echo "===TAR_END===" | ||
| 478 | echo "===EXIT_CODE=$EXEC_EXIT_CODE===" | ||
| 479 | else | ||
| 480 | echo "===ERROR===" | ||
| 481 | echo "Expected /tmp/output.tar but file not found" | ||
| 482 | echo "Command output:" | ||
| 483 | cat "$EXEC_OUTPUT" | ||
| 484 | fi | ||
| 485 | ;; | ||
| 486 | |||
| 487 | storage) | ||
| 488 | # This is handled by runtime-specific code | ||
| 489 | handle_storage_output | ||
| 490 | ;; | ||
| 491 | |||
| 492 | *) | ||
| 493 | echo "===ERROR===" | ||
| 494 | echo "Unknown output type: $RUNTIME_OUTPUT" | ||
| 495 | ;; | ||
| 496 | esac | ||
| 497 | fi | ||
| 498 | } | ||
| 499 | |||
| 500 | # ============================================================================ | ||
| 501 | # Graceful Shutdown | ||
| 502 | # ============================================================================ | ||
| 503 | |||
| 504 | graceful_shutdown() { | ||
| 505 | log "=== Shutting down gracefully ===" | ||
| 506 | |||
| 507 | # Runtime-specific cleanup (implemented by sourcing script) | ||
| 508 | if type stop_runtime_daemons >/dev/null 2>&1; then | ||
| 509 | stop_runtime_daemons | ||
| 510 | fi | ||
| 511 | |||
| 512 | sync | ||
| 513 | |||
| 514 | # Unmount state disk if mounted | ||
| 515 | if mount | grep -q "$VCONTAINER_STATE_DIR"; then | ||
| 516 | log "Unmounting state disk..." | ||
| 517 | sync | ||
| 518 | umount "$VCONTAINER_STATE_DIR" || { | ||
| 519 | log "Warning: umount failed, trying lazy unmount" | ||
| 520 | umount -l "$VCONTAINER_STATE_DIR" 2>/dev/null || true | ||
| 521 | } | ||
| 522 | fi | ||
| 523 | |||
| 524 | # Unmount input | ||
| 525 | umount /mnt/input 2>/dev/null || true | ||
| 526 | |||
| 527 | # Final sync and flush | ||
| 528 | sync | ||
| 529 | for dev in /dev/vd*; do | ||
| 530 | [ -b "$dev" ] && blockdev --flushbufs "$dev" 2>/dev/null || true | ||
| 531 | done | ||
| 532 | sync | ||
| 533 | sleep 2 | ||
| 534 | |||
| 535 | log "=== ${VCONTAINER_RUNTIME_NAME} Complete ===" | ||
| 536 | poweroff -f | ||
| 537 | } | ||
diff --git a/recipes-containers/vcontainer/files/vdkr-preinit.sh b/recipes-containers/vcontainer/files/vdkr-preinit.sh new file mode 100644 index 00000000..08738022 --- /dev/null +++ b/recipes-containers/vcontainer/files/vdkr-preinit.sh | |||
| @@ -0,0 +1,133 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | # vdkr-preinit.sh | ||
| 7 | # Minimal init for initramfs - mounts rootfs and does switch_root | ||
| 8 | # | ||
| 9 | # This script runs from the initramfs and: | ||
| 10 | # 1. Mounts essential filesystems | ||
| 11 | # 2. Finds and mounts the rootfs.img (squashfs, read-only) | ||
| 12 | # 3. Creates overlayfs with tmpfs for writes | ||
| 13 | # 4. Executes switch_root to the overlay root filesystem | ||
| 14 | # | ||
| 15 | # The real init (/init or /sbin/init on rootfs) then runs vdkr-init.sh logic | ||
| 16 | |||
| 17 | # Mount essential filesystems first (needed to check cmdline) | ||
| 18 | mount -t proc proc /proc | ||
| 19 | mount -t sysfs sysfs /sys | ||
| 20 | mount -t devtmpfs devtmpfs /dev | ||
| 21 | |||
| 22 | # Check for quiet mode (interactive) | ||
| 23 | QUIET=0 | ||
| 24 | for param in $(cat /proc/cmdline 2>/dev/null); do | ||
| 25 | case "$param" in | ||
| 26 | docker_interactive=1) QUIET=1 ;; | ||
| 27 | esac | ||
| 28 | done | ||
| 29 | |||
| 30 | log() { | ||
| 31 | [ "$QUIET" = "0" ] && echo "$@" | ||
| 32 | } | ||
| 33 | |||
| 34 | log "=== vdkr preinit (squashfs) ===" | ||
| 35 | |||
| 36 | # Wait for block devices to appear | ||
| 37 | log "Waiting for block devices..." | ||
| 38 | sleep 2 | ||
| 39 | |||
| 40 | # Show available block devices | ||
| 41 | log "Block devices:" | ||
| 42 | [ "$QUIET" = "0" ] && ls -la /dev/vd* 2>/dev/null || log "No virtio block devices found" | ||
| 43 | |||
| 44 | # The rootfs.img is always the first virtio-blk device (/dev/vda) | ||
| 45 | # Additional devices (input, state) come after | ||
| 46 | ROOTFS_DEV="/dev/vda" | ||
| 47 | |||
| 48 | if [ ! -b "$ROOTFS_DEV" ]; then | ||
| 49 | echo "ERROR: Rootfs device $ROOTFS_DEV not found!" | ||
| 50 | echo "Available devices:" | ||
| 51 | ls -la /dev/ | ||
| 52 | sleep 10 | ||
| 53 | reboot -f | ||
| 54 | fi | ||
| 55 | |||
| 56 | # Create mount points for overlay setup | ||
| 57 | mkdir -p /mnt/lower # squashfs (read-only) | ||
| 58 | mkdir -p /mnt/upper # tmpfs for overlay upper | ||
| 59 | mkdir -p /mnt/work # tmpfs for overlay work | ||
| 60 | mkdir -p /mnt/root # final overlayfs mount | ||
| 61 | |||
| 62 | # Mount squashfs read-only | ||
| 63 | log "Mounting squashfs rootfs from $ROOTFS_DEV..." | ||
| 64 | |||
| 65 | if ! mount -t squashfs -o ro "$ROOTFS_DEV" /mnt/lower; then | ||
| 66 | # Fallback to ext4 for backwards compatibility | ||
| 67 | log "squashfs mount failed, trying ext4..." | ||
| 68 | if ! mount -t ext4 -o ro "$ROOTFS_DEV" /mnt/lower; then | ||
| 69 | echo "ERROR: Failed to mount rootfs (tried squashfs and ext4)!" | ||
| 70 | sleep 10 | ||
| 71 | reboot -f | ||
| 72 | fi | ||
| 73 | # ext4 fallback - just use it directly without overlay | ||
| 74 | log "Using ext4 rootfs directly (no overlay)" | ||
| 75 | mount --move /mnt/lower /mnt/root | ||
| 76 | else | ||
| 77 | log "Squashfs mounted successfully" | ||
| 78 | |||
| 79 | # Create tmpfs for overlay upper/work directories | ||
| 80 | # Size is generous since container operations need temp space | ||
| 81 | log "Creating tmpfs overlay..." | ||
| 82 | mount -t tmpfs -o size=1G tmpfs /mnt/upper | ||
| 83 | mkdir -p /mnt/upper/upper | ||
| 84 | mkdir -p /mnt/upper/work | ||
| 85 | |||
| 86 | # Create overlayfs combining squashfs (lower) + tmpfs (upper) | ||
| 87 | log "Mounting overlayfs..." | ||
| 88 | if ! mount -t overlay overlay -o lowerdir=/mnt/lower,upperdir=/mnt/upper/upper,workdir=/mnt/upper/work /mnt/root; then | ||
| 89 | echo "ERROR: Failed to mount overlayfs!" | ||
| 90 | sleep 10 | ||
| 91 | reboot -f | ||
| 92 | fi | ||
| 93 | |||
| 94 | log "Overlayfs mounted successfully" | ||
| 95 | fi | ||
| 96 | |||
| 97 | if [ "$QUIET" = "0" ]; then | ||
| 98 | echo "Contents:" | ||
| 99 | ls -la /mnt/root/ | ||
| 100 | fi | ||
| 101 | |||
| 102 | # Verify init exists on rootfs | ||
| 103 | if [ ! -x /mnt/root/init ] && [ ! -x /mnt/root/sbin/init ]; then | ||
| 104 | echo "ERROR: No init found on rootfs!" | ||
| 105 | sleep 10 | ||
| 106 | reboot -f | ||
| 107 | fi | ||
| 108 | |||
| 109 | # Move filesystems to new root before switch_root | ||
| 110 | # This way they persist across switch_root and the new init doesn't need to remount | ||
| 111 | mkdir -p /mnt/root/proc /mnt/root/sys /mnt/root/dev | ||
| 112 | mount --move /proc /mnt/root/proc | ||
| 113 | mount --move /sys /mnt/root/sys | ||
| 114 | mount --move /dev /mnt/root/dev | ||
| 115 | |||
| 116 | # Switch to real root | ||
| 117 | # switch_root will: | ||
| 118 | # 1. Mount the new root | ||
| 119 | # 2. chroot into it | ||
| 120 | # 3. Execute the new init | ||
| 121 | # 4. Delete everything in the old initramfs | ||
| 122 | log "Switching to real root..." | ||
| 123 | |||
| 124 | if [ -x /mnt/root/init ]; then | ||
| 125 | exec switch_root /mnt/root /init | ||
| 126 | elif [ -x /mnt/root/sbin/init ]; then | ||
| 127 | exec switch_root /mnt/root /sbin/init | ||
| 128 | fi | ||
| 129 | |||
| 130 | # If we get here, switch_root failed | ||
| 131 | echo "ERROR: switch_root failed!" | ||
| 132 | sleep 10 | ||
| 133 | reboot -f | ||
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh new file mode 100755 index 00000000..588261ff --- /dev/null +++ b/recipes-containers/vcontainer/files/vrunner.sh | |||
| @@ -0,0 +1,1353 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | # vrunner.sh | ||
| 7 | # Core runner for vdkr/vpdmn: execute container commands in QEMU-emulated environment | ||
| 8 | # | ||
| 9 | # This script is runtime-agnostic and supports both Docker and Podman via --runtime. | ||
| 10 | # | ||
| 11 | # Boot flow: | ||
| 12 | # 1. QEMU loads kernel + tiny initramfs (busybox + preinit) | ||
| 13 | # 2. preinit mounts rootfs.img (/dev/vda) and does switch_root | ||
| 14 | # 3. Real /init runs on actual ext4 filesystem | ||
| 15 | # 4. Container runtime starts, executes command, outputs results | ||
| 16 | # | ||
| 17 | # This two-stage boot is required because runc needs pivot_root, | ||
| 18 | # which doesn't work from initramfs (rootfs isn't a mount point). | ||
| 19 | # | ||
| 20 | # Drive layout: | ||
| 21 | # /dev/vda = rootfs.img (ro, ext4 with container tools) | ||
| 22 | # /dev/vdb = input disk (optional, user data) | ||
| 23 | # /dev/vdc = state disk (optional, persistent container storage) | ||
| 24 | # | ||
| 25 | # Version: 3.4.0 | ||
| 26 | |||
| 27 | set -e | ||
| 28 | |||
| 29 | VERSION="3.4.0" | ||
| 30 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 31 | |||
| 32 | # Runtime selection: docker or podman | ||
| 33 | # This affects blob directory, cmdline prefix, state directory, and log prefix | ||
| 34 | RUNTIME="${VRUNNER_RUNTIME:-docker}" | ||
| 35 | |||
| 36 | # Configuration | ||
| 37 | TARGET_ARCH="${VDKR_ARCH:-${VPDMN_ARCH:-aarch64}}" | ||
| 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" | ||
| 39 | VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" | ||
| 40 | |||
| 41 | # Runtime-specific settings (set after parsing --runtime) | ||
| 42 | set_runtime_config() { | ||
| 43 | case "$RUNTIME" in | ||
| 44 | docker) | ||
| 45 | TOOL_NAME="vdkr" | ||
| 46 | BLOB_SUBDIR="vdkr-blobs" | ||
| 47 | BLOB_SUBDIR_ALT="blobs" | ||
| 48 | CMDLINE_PREFIX="docker" | ||
| 49 | STATE_DIR_BASE="${VDKR_STATE_DIR:-$HOME/.vdkr}" | ||
| 50 | STATE_FILE="docker-state.img" | ||
| 51 | ;; | ||
| 52 | podman) | ||
| 53 | TOOL_NAME="vpdmn" | ||
| 54 | BLOB_SUBDIR="vpdmn-blobs" | ||
| 55 | BLOB_SUBDIR_ALT="blobs/vpdmn" | ||
| 56 | CMDLINE_PREFIX="podman" | ||
| 57 | STATE_DIR_BASE="${VPDMN_STATE_DIR:-$HOME/.vpdmn}" | ||
| 58 | STATE_FILE="podman-state.img" | ||
| 59 | ;; | ||
| 60 | *) | ||
| 61 | echo "ERROR: Unknown runtime: $RUNTIME (use docker or podman)" >&2 | ||
| 62 | exit 1 | ||
| 63 | ;; | ||
| 64 | esac | ||
| 65 | } | ||
| 66 | |||
| 67 | # Blob locations - relative to script for relocatable installation | ||
| 68 | # Determined after runtime is set | ||
| 69 | # Note: If BLOB_DIR was set via --blob-dir argument, don't override it | ||
| 70 | set_blob_dir() { | ||
| 71 | # Skip if already set by command line argument | ||
| 72 | if [ -n "$BLOB_DIR" ]; then | ||
| 73 | return | ||
| 74 | fi | ||
| 75 | if [ -n "${VDKR_BLOB_DIR:-${VPDMN_BLOB_DIR:-}}" ]; then | ||
| 76 | BLOB_DIR="${VDKR_BLOB_DIR:-${VPDMN_BLOB_DIR}}" | ||
| 77 | elif [ -d "$SCRIPT_DIR/$BLOB_SUBDIR" ]; then | ||
| 78 | BLOB_DIR="$SCRIPT_DIR/$BLOB_SUBDIR" | ||
| 79 | elif [ -d "$SCRIPT_DIR/$BLOB_SUBDIR_ALT" ]; then | ||
| 80 | BLOB_DIR="$SCRIPT_DIR/$BLOB_SUBDIR_ALT" | ||
| 81 | else | ||
| 82 | BLOB_DIR="$SCRIPT_DIR/$BLOB_SUBDIR" | ||
| 83 | fi | ||
| 84 | } | ||
| 85 | |||
| 86 | # Colors | ||
| 87 | RED=$'\033[0;31m' | ||
| 88 | GREEN=$'\033[0;32m' | ||
| 89 | YELLOW=$'\033[0;33m' | ||
| 90 | BLUE=$'\033[0;34m' | ||
| 91 | CYAN=$'\033[0;36m' | ||
| 92 | NC=$'\033[0m' | ||
| 93 | |||
| 94 | log() { | ||
| 95 | local level="$1" | ||
| 96 | local message="$2" | ||
| 97 | local prefix="[${TOOL_NAME:-vdkr}]" | ||
| 98 | case "$level" in | ||
| 99 | "INFO") [ "$VERBOSE" = "true" ] && echo -e "${GREEN}${prefix}${NC} $message" >&2 || true ;; | ||
| 100 | "WARN") echo -e "${YELLOW}${prefix}${NC} $message" >&2 ;; | ||
| 101 | "ERROR") echo -e "${RED}${prefix}${NC} $message" >&2 ;; | ||
| 102 | "DEBUG") [ "$VERBOSE" = "true" ] && echo -e "${BLUE}${prefix}${NC} $message" >&2 || true ;; | ||
| 103 | esac | ||
| 104 | } | ||
| 105 | |||
| 106 | show_usage() { | ||
| 107 | cat << 'EOF' | ||
| 108 | vrunner.sh - Execute docker commands in QEMU-emulated environment | ||
| 109 | |||
| 110 | USAGE: | ||
| 111 | vrunner.sh [OPTIONS] -- <docker-command> [args...] | ||
| 112 | |||
| 113 | OPTIONS: | ||
| 114 | --arch <arch> Target architecture (aarch64, x86_64) [default: aarch64] | ||
| 115 | --input <path> Input file/directory for docker command (mounted as {INPUT}) | ||
| 116 | --input-type <type> Input type: none, oci, tar, dir [default: auto-detect] | ||
| 117 | --input-storage <tar> Restore Docker state from tar before running command | ||
| 118 | --state-dir <path> Use persistent directory for Docker storage between runs | ||
| 119 | --output-type <type> Output type: text, tar, storage [default: text] | ||
| 120 | --output <path> Output file for tar/storage output types | ||
| 121 | --blob-dir <path> Directory containing kernel/initramfs blobs | ||
| 122 | --network, -n Enable networking (slirp user-mode, outbound only) | ||
| 123 | --interactive, -it Run in interactive mode (connects terminal to container) | ||
| 124 | --timeout <secs> QEMU timeout [default: 300] | ||
| 125 | --no-kvm Disable KVM acceleration (use TCG emulation) | ||
| 126 | --batch-import Batch import mode: import multiple OCI containers in one session | ||
| 127 | --keep-temp Keep temporary files for debugging | ||
| 128 | --verbose, -v Enable verbose output | ||
| 129 | --help, -h Show this help | ||
| 130 | |||
| 131 | INPUT TYPES: | ||
| 132 | none No input data (docker commands that don't need files) | ||
| 133 | oci OCI container directory (has index.json, blobs/) | ||
| 134 | tar Tar archive (docker save output, etc.) | ||
| 135 | dir Generic directory | ||
| 136 | |||
| 137 | OUTPUT TYPES: | ||
| 138 | text Capture command stdout/stderr as text (default) | ||
| 139 | tar Expect command to create /tmp/output.tar, return as file | ||
| 140 | storage Export entire /var/lib/docker as tar | ||
| 141 | |||
| 142 | PLACEHOLDERS: | ||
| 143 | {INPUT} Replaced with path to mounted input inside QEMU | ||
| 144 | |||
| 145 | EXAMPLES: | ||
| 146 | # List images (no input needed) | ||
| 147 | vrunner.sh -- docker images | ||
| 148 | |||
| 149 | # Load an image from tar | ||
| 150 | vrunner.sh --input myimage.tar -- docker load -i {INPUT} | ||
| 151 | |||
| 152 | # Import an OCI container | ||
| 153 | vrunner.sh --input ./container-oci/ --input-type oci \ | ||
| 154 | -- docker import {INPUT}/blobs/sha256/LARGEST myimage:latest | ||
| 155 | |||
| 156 | # Save an image to tar (after loading) | ||
| 157 | vrunner.sh --input myimage.tar --output-type tar \ | ||
| 158 | -- 'docker load -i {INPUT} && docker save -o /tmp/output.tar myimage:latest' | ||
| 159 | |||
| 160 | # Get full docker storage after operations | ||
| 161 | vrunner.sh --input myimage.tar --output-type storage --output storage.tar \ | ||
| 162 | -- docker load -i {INPUT} | ||
| 163 | |||
| 164 | # Pull an image from a registry (requires --network) | ||
| 165 | vrunner.sh --network -- docker pull alpine:latest | ||
| 166 | |||
| 167 | # Batch import multiple OCI containers in one session | ||
| 168 | vrunner.sh --batch-import --output storage.tar \ | ||
| 169 | -- /path/to/app-oci:myapp:latest /path/to/db-oci:mydb:v1.0 | ||
| 170 | |||
| 171 | # Batch import with existing storage (additive) | ||
| 172 | vrunner.sh --batch-import --input-storage existing.tar --output merged.tar \ | ||
| 173 | -- /path/to/new-oci:newapp:latest | ||
| 174 | |||
| 175 | EOF | ||
| 176 | } | ||
| 177 | |||
| 178 | # Parse arguments | ||
| 179 | INPUT_PATH="" | ||
| 180 | INPUT_TYPE="none" | ||
| 181 | NETWORK="false" | ||
| 182 | INTERACTIVE="false" | ||
| 183 | INPUT_STORAGE="" | ||
| 184 | STATE_DIR="" | ||
| 185 | OUTPUT_TYPE="text" | ||
| 186 | OUTPUT_FILE="" | ||
| 187 | KEEP_TEMP="false" | ||
| 188 | DISABLE_KVM="false" | ||
| 189 | DOCKER_CMD="" | ||
| 190 | PORT_FORWARDS=() | ||
| 191 | |||
| 192 | # Batch import mode | ||
| 193 | BATCH_IMPORT="false" | ||
| 194 | |||
| 195 | # Daemon mode options | ||
| 196 | DAEMON_MODE="" # start, send, stop, status | ||
| 197 | DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files | ||
| 198 | |||
| 199 | while [ $# -gt 0 ]; do | ||
| 200 | case $1 in | ||
| 201 | --runtime) | ||
| 202 | RUNTIME="$2" | ||
| 203 | shift 2 | ||
| 204 | ;; | ||
| 205 | --arch) | ||
| 206 | TARGET_ARCH="$2" | ||
| 207 | shift 2 | ||
| 208 | ;; | ||
| 209 | --input) | ||
| 210 | INPUT_PATH="$2" | ||
| 211 | shift 2 | ||
| 212 | ;; | ||
| 213 | --input-type) | ||
| 214 | INPUT_TYPE="$2" | ||
| 215 | shift 2 | ||
| 216 | ;; | ||
| 217 | --input-storage) | ||
| 218 | INPUT_STORAGE="$2" | ||
| 219 | shift 2 | ||
| 220 | ;; | ||
| 221 | --state-dir) | ||
| 222 | STATE_DIR="$2" | ||
| 223 | shift 2 | ||
| 224 | ;; | ||
| 225 | --output-type) | ||
| 226 | OUTPUT_TYPE="$2" | ||
| 227 | shift 2 | ||
| 228 | ;; | ||
| 229 | --output) | ||
| 230 | OUTPUT_FILE="$2" | ||
| 231 | shift 2 | ||
| 232 | ;; | ||
| 233 | --blob-dir) | ||
| 234 | BLOB_DIR="$2" | ||
| 235 | shift 2 | ||
| 236 | ;; | ||
| 237 | --timeout) | ||
| 238 | TIMEOUT="$2" | ||
| 239 | shift 2 | ||
| 240 | ;; | ||
| 241 | --network|-n) | ||
| 242 | NETWORK="true" | ||
| 243 | shift | ||
| 244 | ;; | ||
| 245 | --port-forward) | ||
| 246 | # Format: host_port:container_port or host_port:container_port/protocol | ||
| 247 | PORT_FORWARDS+=("$2") | ||
| 248 | shift 2 | ||
| 249 | ;; | ||
| 250 | --interactive|-it) | ||
| 251 | INTERACTIVE="true" | ||
| 252 | shift | ||
| 253 | ;; | ||
| 254 | --keep-temp) | ||
| 255 | KEEP_TEMP="true" | ||
| 256 | shift | ||
| 257 | ;; | ||
| 258 | --no-kvm) | ||
| 259 | DISABLE_KVM="true" | ||
| 260 | shift | ||
| 261 | ;; | ||
| 262 | --batch-import) | ||
| 263 | BATCH_IMPORT="true" | ||
| 264 | # Force storage output type for batch import | ||
| 265 | OUTPUT_TYPE="storage" | ||
| 266 | shift | ||
| 267 | ;; | ||
| 268 | --daemon-start) | ||
| 269 | DAEMON_MODE="start" | ||
| 270 | shift | ||
| 271 | ;; | ||
| 272 | --daemon-send) | ||
| 273 | DAEMON_MODE="send" | ||
| 274 | shift | ||
| 275 | ;; | ||
| 276 | --daemon-send-input) | ||
| 277 | DAEMON_MODE="send-input" | ||
| 278 | shift | ||
| 279 | ;; | ||
| 280 | --daemon-interactive) | ||
| 281 | DAEMON_MODE="interactive" | ||
| 282 | shift | ||
| 283 | ;; | ||
| 284 | --daemon-stop) | ||
| 285 | DAEMON_MODE="stop" | ||
| 286 | shift | ||
| 287 | ;; | ||
| 288 | --daemon-status) | ||
| 289 | DAEMON_MODE="status" | ||
| 290 | shift | ||
| 291 | ;; | ||
| 292 | --daemon-socket-dir) | ||
| 293 | DAEMON_SOCKET_DIR="$2" | ||
| 294 | shift 2 | ||
| 295 | ;; | ||
| 296 | --verbose|-v) | ||
| 297 | VERBOSE="true" | ||
| 298 | shift | ||
| 299 | ;; | ||
| 300 | --help|-h) | ||
| 301 | show_usage | ||
| 302 | exit 0 | ||
| 303 | ;; | ||
| 304 | --) | ||
| 305 | shift | ||
| 306 | DOCKER_CMD="$*" | ||
| 307 | break | ||
| 308 | ;; | ||
| 309 | *) | ||
| 310 | # If we hit a non-option, assume rest is docker command | ||
| 311 | DOCKER_CMD="$*" | ||
| 312 | break | ||
| 313 | ;; | ||
| 314 | esac | ||
| 315 | done | ||
| 316 | |||
| 317 | # Initialize runtime-specific configuration | ||
| 318 | set_runtime_config | ||
| 319 | set_blob_dir | ||
| 320 | |||
| 321 | # Daemon mode handling | ||
| 322 | # Set default socket directory based on architecture | ||
| 323 | # If --state-dir was provided, use it for daemon files too | ||
| 324 | if [ -z "$DAEMON_SOCKET_DIR" ]; then | ||
| 325 | if [ -n "$STATE_DIR" ]; then | ||
| 326 | DAEMON_SOCKET_DIR="$STATE_DIR" | ||
| 327 | else | ||
| 328 | DAEMON_SOCKET_DIR="${STATE_DIR_BASE}/${TARGET_ARCH}" | ||
| 329 | fi | ||
| 330 | fi | ||
| 331 | DAEMON_PID_FILE="$DAEMON_SOCKET_DIR/daemon.pid" | ||
| 332 | DAEMON_SOCKET="$DAEMON_SOCKET_DIR/daemon.sock" | ||
| 333 | DAEMON_QEMU_LOG="$DAEMON_SOCKET_DIR/qemu.log" | ||
| 334 | DAEMON_INPUT_IMG="$DAEMON_SOCKET_DIR/daemon-input.img" | ||
| 335 | DAEMON_INPUT_SIZE_MB=2048 # 2GB input disk for daemon mode | ||
| 336 | |||
| 337 | # Daemon helper functions | ||
| 338 | daemon_is_running() { | ||
| 339 | if [ -f "$DAEMON_PID_FILE" ]; then | ||
| 340 | local pid=$(cat "$DAEMON_PID_FILE" 2>/dev/null) | ||
| 341 | if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then | ||
| 342 | return 0 | ||
| 343 | fi | ||
| 344 | fi | ||
| 345 | return 1 | ||
| 346 | } | ||
| 347 | |||
| 348 | daemon_status() { | ||
| 349 | if daemon_is_running; then | ||
| 350 | local pid=$(cat "$DAEMON_PID_FILE") | ||
| 351 | echo "Daemon running (PID: $pid)" | ||
| 352 | echo "Socket: $DAEMON_SOCKET" | ||
| 353 | echo "Architecture: $TARGET_ARCH" | ||
| 354 | return 0 | ||
| 355 | else | ||
| 356 | echo "Daemon not running" | ||
| 357 | return 1 | ||
| 358 | fi | ||
| 359 | } | ||
| 360 | |||
| 361 | daemon_stop() { | ||
| 362 | if ! daemon_is_running; then | ||
| 363 | log "WARN" "Daemon is not running" | ||
| 364 | return 0 | ||
| 365 | fi | ||
| 366 | |||
| 367 | local pid=$(cat "$DAEMON_PID_FILE") | ||
| 368 | log "INFO" "Stopping daemon (PID: $pid)..." | ||
| 369 | |||
| 370 | # Send shutdown command via socket | ||
| 371 | if [ -S "$DAEMON_SOCKET" ]; then | ||
| 372 | echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true | ||
| 373 | sleep 2 | ||
| 374 | fi | ||
| 375 | |||
| 376 | # If still running, kill it | ||
| 377 | if kill -0 "$pid" 2>/dev/null; then | ||
| 378 | log "INFO" "Sending SIGTERM..." | ||
| 379 | kill "$pid" 2>/dev/null || true | ||
| 380 | sleep 2 | ||
| 381 | fi | ||
| 382 | |||
| 383 | # Force kill if necessary | ||
| 384 | if kill -0 "$pid" 2>/dev/null; then | ||
| 385 | log "WARN" "Sending SIGKILL..." | ||
| 386 | kill -9 "$pid" 2>/dev/null || true | ||
| 387 | fi | ||
| 388 | |||
| 389 | rm -f "$DAEMON_PID_FILE" "$DAEMON_SOCKET" | ||
| 390 | log "INFO" "Daemon stopped" | ||
| 391 | } | ||
| 392 | |||
| 393 | daemon_send() { | ||
| 394 | local cmd="$1" | ||
| 395 | |||
| 396 | if ! daemon_is_running; then | ||
| 397 | log "ERROR" "Daemon is not running. Start it with --daemon-start" | ||
| 398 | exit 1 | ||
| 399 | fi | ||
| 400 | |||
| 401 | if [ ! -S "$DAEMON_SOCKET" ]; then | ||
| 402 | log "ERROR" "Daemon socket not found: $DAEMON_SOCKET" | ||
| 403 | exit 1 | ||
| 404 | fi | ||
| 405 | |||
| 406 | # Encode command in base64 and send | ||
| 407 | local cmd_b64=$(echo -n "$cmd" | base64 -w0) | ||
| 408 | |||
| 409 | # Send command and read response using coproc | ||
| 410 | # This allows us to kill socat when we're done reading | ||
| 411 | coproc SOCAT { socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null; } | ||
| 412 | |||
| 413 | local EXIT_CODE=0 | ||
| 414 | local in_output=false | ||
| 415 | local TIMEOUT=60 | ||
| 416 | |||
| 417 | # Send command to socat's stdin | ||
| 418 | echo "$cmd_b64" >&${SOCAT[1]} | ||
| 419 | |||
| 420 | # Read response from socat's stdout with timeout | ||
| 421 | while IFS= read -t $TIMEOUT -r line <&${SOCAT[0]}; do | ||
| 422 | case "$line" in | ||
| 423 | "===OUTPUT_START===") | ||
| 424 | in_output=true | ||
| 425 | ;; | ||
| 426 | "===OUTPUT_END===") | ||
| 427 | in_output=false | ||
| 428 | ;; | ||
| 429 | "===EXIT_CODE="*"===") | ||
| 430 | EXIT_CODE="${line#===EXIT_CODE=}" | ||
| 431 | EXIT_CODE="${EXIT_CODE%===}" | ||
| 432 | ;; | ||
| 433 | "===END===") | ||
| 434 | break | ||
| 435 | ;; | ||
| 436 | *) | ||
| 437 | if [ "$in_output" = "true" ]; then | ||
| 438 | echo "$line" | ||
| 439 | fi | ||
| 440 | ;; | ||
| 441 | esac | ||
| 442 | done | ||
| 443 | |||
| 444 | # Clean up - close FDs and kill socat | ||
| 445 | exec {SOCAT[0]}<&- {SOCAT[1]}>&- | ||
| 446 | kill $SOCAT_PID 2>/dev/null || true | ||
| 447 | wait $SOCAT_PID 2>/dev/null || true | ||
| 448 | |||
| 449 | return ${EXIT_CODE:-0} | ||
| 450 | } | ||
| 451 | |||
| 452 | # Copy input data to shared directory and send command | ||
| 453 | daemon_send_with_input() { | ||
| 454 | local input_path="$1" | ||
| 455 | local input_type="$2" | ||
| 456 | local cmd="$3" | ||
| 457 | |||
| 458 | if ! daemon_is_running; then | ||
| 459 | log "ERROR" "Daemon is not running. Start it with --daemon-start" | ||
| 460 | exit 1 | ||
| 461 | fi | ||
| 462 | |||
| 463 | # Shared directory for virtio-9p | ||
| 464 | local share_dir="$DAEMON_SOCKET_DIR/share" | ||
| 465 | if [ ! -d "$share_dir" ]; then | ||
| 466 | log "ERROR" "Daemon share directory not found: $share_dir" | ||
| 467 | exit 1 | ||
| 468 | fi | ||
| 469 | |||
| 470 | # Clear and populate shared directory | ||
| 471 | log "INFO" "Copying input to shared directory..." | ||
| 472 | rm -rf "$share_dir"/* | ||
| 473 | |||
| 474 | if [ -d "$input_path" ]; then | ||
| 475 | # Directory - copy contents (use -L to dereference symlinks) | ||
| 476 | cp -rL "$input_path"/* "$share_dir/" 2>/dev/null || cp -r "$input_path"/* "$share_dir/" | ||
| 477 | else | ||
| 478 | # Single file - copy it | ||
| 479 | cp "$input_path" "$share_dir/" | ||
| 480 | fi | ||
| 481 | |||
| 482 | # Sync to ensure data is visible to guest | ||
| 483 | sync | ||
| 484 | |||
| 485 | # Mark command as needing input (prefix with special marker) | ||
| 486 | # Guest reads from /mnt/share (virtio-9p mount) | ||
| 487 | local full_cmd="===USE_INPUT===$cmd" | ||
| 488 | |||
| 489 | # Send command via daemon_send | ||
| 490 | daemon_send "$full_cmd" | ||
| 491 | } | ||
| 492 | |||
| 493 | # Run interactive command through daemon (for run -it, exec -it) | ||
| 494 | daemon_interactive() { | ||
| 495 | local cmd="$1" | ||
| 496 | |||
| 497 | if ! daemon_is_running; then | ||
| 498 | log "ERROR" "Daemon is not running" | ||
| 499 | return 1 | ||
| 500 | fi | ||
| 501 | |||
| 502 | if [ ! -S "$DAEMON_SOCKET" ]; then | ||
| 503 | log "ERROR" "Daemon socket not found: $DAEMON_SOCKET" | ||
| 504 | return 1 | ||
| 505 | fi | ||
| 506 | |||
| 507 | # Encode command with interactive prefix | ||
| 508 | local cmd_b64=$(echo -n "===INTERACTIVE===$cmd" | base64 -w0) | ||
| 509 | |||
| 510 | # Use expect to handle sending command then going interactive | ||
| 511 | # expect properly handles PTY creation and signal passthrough | ||
| 512 | if command -v expect >/dev/null 2>&1; then | ||
| 513 | # Disable terminal signal generation so Ctrl+C becomes byte 0x03 | ||
| 514 | local saved_stty="" | ||
| 515 | if [ -t 0 ]; then | ||
| 516 | saved_stty=$(stty -g) | ||
| 517 | stty -isig | ||
| 518 | fi | ||
| 519 | |||
| 520 | expect -c " | ||
| 521 | log_user 0 | ||
| 522 | set timeout -1 | ||
| 523 | spawn socat -,rawer UNIX-CONNECT:$DAEMON_SOCKET | ||
| 524 | send \"$cmd_b64\r\" | ||
| 525 | # Wait for READY signal before showing output | ||
| 526 | expect \"===INTERACTIVE_READY===\" {} | ||
| 527 | log_user 1 | ||
| 528 | # Interactive mode - exit on END marker or EOF | ||
| 529 | interact { | ||
| 530 | -o \"===INTERACTIVE_END\" { | ||
| 531 | return | ||
| 532 | } | ||
| 533 | eof { | ||
| 534 | return | ||
| 535 | } | ||
| 536 | } | ||
| 537 | " 2>/dev/null | ||
| 538 | local rc=$? | ||
| 539 | |||
| 540 | # Restore terminal | ||
| 541 | if [ -n "$saved_stty" ]; then | ||
| 542 | stty "$saved_stty" | ||
| 543 | fi | ||
| 544 | return $rc | ||
| 545 | fi | ||
| 546 | |||
| 547 | # Fallback: no expect available, use basic approach (Ctrl+C won't work well) | ||
| 548 | log "WARN" "expect not found, interactive mode may have issues with Ctrl+C" | ||
| 549 | { | ||
| 550 | echo "$cmd_b64" | ||
| 551 | cat | ||
| 552 | } | socat - "UNIX-CONNECT:$DAEMON_SOCKET" | ||
| 553 | return $? | ||
| 554 | } | ||
| 555 | |||
| 556 | # Handle daemon modes that don't need a docker command | ||
| 557 | if [ "$DAEMON_MODE" = "status" ]; then | ||
| 558 | daemon_status | ||
| 559 | exit $? | ||
| 560 | fi | ||
| 561 | |||
| 562 | if [ "$DAEMON_MODE" = "stop" ]; then | ||
| 563 | daemon_stop | ||
| 564 | exit $? | ||
| 565 | fi | ||
| 566 | |||
| 567 | if [ "$DAEMON_MODE" = "send" ]; then | ||
| 568 | if [ -z "$DOCKER_CMD" ]; then | ||
| 569 | log "ERROR" "No command specified for --daemon-send" | ||
| 570 | exit 1 | ||
| 571 | fi | ||
| 572 | daemon_send "$DOCKER_CMD" | ||
| 573 | exit $? | ||
| 574 | fi | ||
| 575 | |||
| 576 | if [ "$DAEMON_MODE" = "send-input" ]; then | ||
| 577 | if [ -z "$DOCKER_CMD" ]; then | ||
| 578 | log "ERROR" "No command specified for --daemon-send-input" | ||
| 579 | exit 1 | ||
| 580 | fi | ||
| 581 | if [ -z "$INPUT_PATH" ]; then | ||
| 582 | log "ERROR" "No input specified for --daemon-send-input (use --input)" | ||
| 583 | exit 1 | ||
| 584 | fi | ||
| 585 | daemon_send_with_input "$INPUT_PATH" "$INPUT_TYPE" "$DOCKER_CMD" | ||
| 586 | exit $? | ||
| 587 | fi | ||
| 588 | |||
| 589 | if [ "$DAEMON_MODE" = "interactive" ]; then | ||
| 590 | if [ -z "$DOCKER_CMD" ]; then | ||
| 591 | log "ERROR" "No command specified for --daemon-interactive" | ||
| 592 | exit 1 | ||
| 593 | fi | ||
| 594 | daemon_interactive "$DOCKER_CMD" | ||
| 595 | exit $? | ||
| 596 | fi | ||
| 597 | |||
| 598 | # For non-daemon mode, require docker command (unless batch import) | ||
| 599 | if [ -z "$DOCKER_CMD" ] && [ "$DAEMON_MODE" != "start" ] && [ "$BATCH_IMPORT" != "true" ]; then | ||
| 600 | log "ERROR" "No docker command specified" | ||
| 601 | echo "" | ||
| 602 | show_usage | ||
| 603 | exit 1 | ||
| 604 | fi | ||
| 605 | |||
| 606 | # Create temp directory early (needed for batch import and other operations) | ||
| 607 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" | ||
| 608 | mkdir -p "$TEMP_DIR" | ||
| 609 | |||
| 610 | cleanup() { | ||
| 611 | if [ "$KEEP_TEMP" = "true" ]; then | ||
| 612 | log "DEBUG" "Keeping temp directory: $TEMP_DIR" | ||
| 613 | else | ||
| 614 | rm -rf "$TEMP_DIR" 2>/dev/null || true | ||
| 615 | fi | ||
| 616 | } | ||
| 617 | trap cleanup EXIT INT TERM | ||
| 618 | |||
| 619 | # Batch import mode: parse container list and build compound command | ||
| 620 | # Format: path:image:tag path:image:tag ... | ||
| 621 | if [ "$BATCH_IMPORT" = "true" ]; then | ||
| 622 | if [ -z "$DOCKER_CMD" ]; then | ||
| 623 | log "ERROR" "Batch import requires container list: path:image:tag ..." | ||
| 624 | exit 1 | ||
| 625 | fi | ||
| 626 | |||
| 627 | log "INFO" "Batch import mode enabled" | ||
| 628 | |||
| 629 | # Parse container entries | ||
| 630 | BATCH_ENTRIES=() | ||
| 631 | BATCH_PATHS=() | ||
| 632 | BATCH_IMAGES=() | ||
| 633 | |||
| 634 | for entry in $DOCKER_CMD; do | ||
| 635 | # Parse path:image:tag | ||
| 636 | # Handle colons carefully - path might have colons in edge cases | ||
| 637 | # Format is: /path/to/oci:imagename:tag | ||
| 638 | path="${entry%%:*}" | ||
| 639 | rest="${entry#*:}" | ||
| 640 | image="${rest%%:*}" | ||
| 641 | tag="${rest#*:}" | ||
| 642 | |||
| 643 | if [ -z "$path" ] || [ -z "$image" ] || [ -z "$tag" ]; then | ||
| 644 | log "ERROR" "Invalid batch entry: $entry (expected path:image:tag)" | ||
| 645 | exit 1 | ||
| 646 | fi | ||
| 647 | |||
| 648 | if [ ! -d "$path" ]; then | ||
| 649 | log "ERROR" "OCI directory not found: $path" | ||
| 650 | exit 1 | ||
| 651 | fi | ||
| 652 | |||
| 653 | BATCH_ENTRIES+=("$entry") | ||
| 654 | BATCH_PATHS+=("$path") | ||
| 655 | BATCH_IMAGES+=("$image:$tag") | ||
| 656 | log "DEBUG" "Batch entry: $path -> $image:$tag" | ||
| 657 | done | ||
| 658 | |||
| 659 | log "INFO" "Processing ${#BATCH_ENTRIES[@]} containers" | ||
| 660 | |||
| 661 | # Create combined input disk with numbered subdirectories | ||
| 662 | # /0/ = first OCI dir, /1/ = second, etc. | ||
| 663 | BATCH_INPUT_DIR="$TEMP_DIR/batch-input" | ||
| 664 | mkdir -p "$BATCH_INPUT_DIR" | ||
| 665 | |||
| 666 | for i in "${!BATCH_PATHS[@]}"; do | ||
| 667 | src="${BATCH_PATHS[$i]}" | ||
| 668 | dest="$BATCH_INPUT_DIR/$i" | ||
| 669 | log "DEBUG" "Copying $src -> $dest" | ||
| 670 | # Use cp -rL to dereference symlinks (OCI containers often use hardlinks) | ||
| 671 | cp -rL "$src" "$dest" | ||
| 672 | done | ||
| 673 | |||
| 674 | # Override INPUT_PATH to point to combined directory | ||
| 675 | INPUT_PATH="$BATCH_INPUT_DIR" | ||
| 676 | INPUT_TYPE="dir" | ||
| 677 | |||
| 678 | # Build compound skopeo command | ||
| 679 | # Each container: skopeo copy oci:/mnt/input/N docker-daemon:image:tag | ||
| 680 | # Note: VM init script mounts input disk at /mnt/input (see mount_input_disk) | ||
| 681 | COMPOUND_CMD="" | ||
| 682 | for i in "${!BATCH_IMAGES[@]}"; do | ||
| 683 | img="${BATCH_IMAGES[$i]}" | ||
| 684 | if [ "$RUNTIME" = "docker" ]; then | ||
| 685 | CMD="skopeo copy oci:/mnt/input/$i docker-daemon:$img" | ||
| 686 | else | ||
| 687 | CMD="skopeo copy oci:/mnt/input/$i containers-storage:$img" | ||
| 688 | fi | ||
| 689 | |||
| 690 | if [ -z "$COMPOUND_CMD" ]; then | ||
| 691 | COMPOUND_CMD="$CMD" | ||
| 692 | else | ||
| 693 | COMPOUND_CMD="$COMPOUND_CMD && $CMD" | ||
| 694 | fi | ||
| 695 | done | ||
| 696 | |||
| 697 | # Add final images command to show what was imported | ||
| 698 | if [ "$RUNTIME" = "docker" ]; then | ||
| 699 | COMPOUND_CMD="$COMPOUND_CMD && docker images" | ||
| 700 | else | ||
| 701 | COMPOUND_CMD="$COMPOUND_CMD && podman images" | ||
| 702 | fi | ||
| 703 | |||
| 704 | log "DEBUG" "Batch command: $COMPOUND_CMD" | ||
| 705 | DOCKER_CMD="$COMPOUND_CMD" | ||
| 706 | fi | ||
| 707 | |||
| 708 | # Auto-detect input type if input provided but type not specified | ||
| 709 | if [ -n "$INPUT_PATH" ] && [ "$INPUT_TYPE" = "none" ]; then | ||
| 710 | if [ -d "$INPUT_PATH" ]; then | ||
| 711 | if [ -f "$INPUT_PATH/index.json" ] || [ -f "$INPUT_PATH/oci-layout" ]; then | ||
| 712 | INPUT_TYPE="oci" | ||
| 713 | else | ||
| 714 | INPUT_TYPE="dir" | ||
| 715 | fi | ||
| 716 | elif [ -f "$INPUT_PATH" ]; then | ||
| 717 | INPUT_TYPE="tar" | ||
| 718 | fi | ||
| 719 | log "DEBUG" "Auto-detected input type: $INPUT_TYPE" | ||
| 720 | fi | ||
| 721 | |||
| 722 | # Validate output file for types that need it | ||
| 723 | if [ "$OUTPUT_TYPE" = "tar" ] || [ "$OUTPUT_TYPE" = "storage" ]; then | ||
| 724 | if [ -z "$OUTPUT_FILE" ]; then | ||
| 725 | OUTPUT_FILE="/tmp/vdkr-output-$$.tar" | ||
| 726 | log "WARN" "No --output specified, using: $OUTPUT_FILE" | ||
| 727 | fi | ||
| 728 | fi | ||
| 729 | |||
| 730 | log "INFO" "vdkr-run v$VERSION" | ||
| 731 | log "INFO" "Architecture: $TARGET_ARCH" | ||
| 732 | log "INFO" "Docker command: $DOCKER_CMD" | ||
| 733 | [ -n "$INPUT_PATH" ] && log "INFO" "Input: $INPUT_PATH ($INPUT_TYPE)" | ||
| 734 | [ -n "$INPUT_STORAGE" ] && log "INFO" "Input storage: $INPUT_STORAGE" | ||
| 735 | [ -n "$STATE_DIR" ] && log "INFO" "State directory: $STATE_DIR" | ||
| 736 | log "INFO" "Output type: $OUTPUT_TYPE" | ||
| 737 | [ -n "$OUTPUT_FILE" ] && log "INFO" "Output file: $OUTPUT_FILE" | ||
| 738 | [ "$NETWORK" = "true" ] && log "INFO" "Networking: enabled (slirp)" | ||
| 739 | [ "$INTERACTIVE" = "true" ] && log "INFO" "Interactive mode: enabled" | ||
| 740 | |||
| 741 | # Find kernel, initramfs, and rootfs | ||
| 742 | case "$TARGET_ARCH" in | ||
| 743 | aarch64) | ||
| 744 | KERNEL_IMAGE="$BLOB_DIR/aarch64/Image" | ||
| 745 | INITRAMFS="$BLOB_DIR/aarch64/initramfs.cpio.gz" | ||
| 746 | ROOTFS_IMG="$BLOB_DIR/aarch64/rootfs.img" | ||
| 747 | QEMU_CMD="qemu-system-aarch64" | ||
| 748 | QEMU_MACHINE="-M virt -cpu cortex-a57" | ||
| 749 | CONSOLE="ttyAMA0" | ||
| 750 | ;; | ||
| 751 | x86_64) | ||
| 752 | KERNEL_IMAGE="$BLOB_DIR/x86_64/bzImage" | ||
| 753 | INITRAMFS="$BLOB_DIR/x86_64/initramfs.cpio.gz" | ||
| 754 | ROOTFS_IMG="$BLOB_DIR/x86_64/rootfs.img" | ||
| 755 | QEMU_CMD="qemu-system-x86_64" | ||
| 756 | # Use q35 + Skylake-Client to match oe-core qemux86-64 machine | ||
| 757 | QEMU_MACHINE="-M q35 -cpu Skylake-Client" | ||
| 758 | CONSOLE="ttyS0" | ||
| 759 | ;; | ||
| 760 | *) | ||
| 761 | log "ERROR" "Unsupported architecture: $TARGET_ARCH" | ||
| 762 | exit 1 | ||
| 763 | ;; | ||
| 764 | esac | ||
| 765 | |||
| 766 | # Check for kernel | ||
| 767 | if [ ! -f "$KERNEL_IMAGE" ]; then | ||
| 768 | log "ERROR" "Kernel not found: $KERNEL_IMAGE" | ||
| 769 | log "ERROR" "Set VDKR_BLOB_DIR or --blob-dir to location of vdkr blobs" | ||
| 770 | log "ERROR" "Build with: MACHINE=qemuarm64 bitbake vdkr-initramfs-build" | ||
| 771 | exit 1 | ||
| 772 | fi | ||
| 773 | |||
| 774 | # Check for initramfs | ||
| 775 | if [ ! -f "$INITRAMFS" ]; then | ||
| 776 | log "ERROR" "Initramfs not found: $INITRAMFS" | ||
| 777 | log "ERROR" "Build with: MACHINE=qemuarm64 bitbake vdkr-initramfs-build" | ||
| 778 | exit 1 | ||
| 779 | fi | ||
| 780 | |||
| 781 | # Check for rootfs image (ext4 with Docker tools) | ||
| 782 | if [ ! -f "$ROOTFS_IMG" ]; then | ||
| 783 | log "ERROR" "Rootfs image not found: $ROOTFS_IMG" | ||
| 784 | log "ERROR" "Build with: MACHINE=qemuarm64 bitbake vdkr-initramfs-create" | ||
| 785 | exit 1 | ||
| 786 | fi | ||
| 787 | |||
| 788 | # Find QEMU - check PATH and common locations | ||
| 789 | if ! command -v "$QEMU_CMD" >/dev/null 2>&1; then | ||
| 790 | # Try common locations | ||
| 791 | for path in \ | ||
| 792 | "${STAGING_BINDIR_NATIVE:-}" \ | ||
| 793 | "/usr/bin"; do | ||
| 794 | if [ -n "$path" ] && [ -x "$path/$QEMU_CMD" ]; then | ||
| 795 | QEMU_CMD="$path/$QEMU_CMD" | ||
| 796 | break | ||
| 797 | fi | ||
| 798 | done | ||
| 799 | fi | ||
| 800 | |||
| 801 | if ! command -v "$QEMU_CMD" >/dev/null 2>&1 && [ ! -x "$QEMU_CMD" ]; then | ||
| 802 | log "ERROR" "QEMU not found: $QEMU_CMD" | ||
| 803 | exit 1 | ||
| 804 | fi | ||
| 805 | |||
| 806 | log "DEBUG" "Using QEMU: $QEMU_CMD" | ||
| 807 | |||
| 808 | # Check for KVM acceleration (when host matches target) | ||
| 809 | USE_KVM="false" | ||
| 810 | if [ "$DISABLE_KVM" = "true" ]; then | ||
| 811 | log "DEBUG" "KVM disabled by --no-kvm flag" | ||
| 812 | else | ||
| 813 | HOST_ARCH=$(uname -m) | ||
| 814 | if [ "$HOST_ARCH" = "$TARGET_ARCH" ] || \ | ||
| 815 | { [ "$HOST_ARCH" = "x86_64" ] && [ "$TARGET_ARCH" = "x86_64" ]; }; then | ||
| 816 | if [ -w /dev/kvm ]; then | ||
| 817 | USE_KVM="true" | ||
| 818 | # Use host CPU for best performance with KVM | ||
| 819 | case "$TARGET_ARCH" in | ||
| 820 | x86_64) | ||
| 821 | QEMU_MACHINE="-M q35 -cpu host" | ||
| 822 | ;; | ||
| 823 | aarch64) | ||
| 824 | QEMU_MACHINE="-M virt -cpu host" | ||
| 825 | ;; | ||
| 826 | esac | ||
| 827 | log "INFO" "KVM acceleration enabled" | ||
| 828 | else | ||
| 829 | log "DEBUG" "KVM not available (no write access to /dev/kvm)" | ||
| 830 | fi | ||
| 831 | fi | ||
| 832 | fi | ||
| 833 | |||
| 834 | log "DEBUG" "Using initramfs: $INITRAMFS" | ||
| 835 | |||
| 836 | # Create input disk image if needed | ||
| 837 | DISK_OPTS="" | ||
| 838 | if [ -n "$INPUT_PATH" ] && [ "$INPUT_TYPE" != "none" ]; then | ||
| 839 | log "INFO" "Creating input disk image..." | ||
| 840 | INPUT_IMG="$TEMP_DIR/input.img" | ||
| 841 | |||
| 842 | # Calculate size (use -L to dereference hardlinks in OCI containers) | ||
| 843 | if [ -d "$INPUT_PATH" ]; then | ||
| 844 | SIZE_KB=$(du -skL "$INPUT_PATH" | cut -f1) | ||
| 845 | else | ||
| 846 | SIZE_KB=$(($(stat -c%s "$INPUT_PATH") / 1024)) | ||
| 847 | fi | ||
| 848 | SIZE_MB=$(( (SIZE_KB / 1024) + 20 )) | ||
| 849 | [ $SIZE_MB -lt 20 ] && SIZE_MB=20 | ||
| 850 | |||
| 851 | log "DEBUG" "Input size: ${SIZE_KB}KB, Image size: ${SIZE_MB}MB" | ||
| 852 | |||
| 853 | dd if=/dev/zero of="$INPUT_IMG" bs=1M count=$SIZE_MB 2>/dev/null | ||
| 854 | |||
| 855 | if [ -d "$INPUT_PATH" ]; then | ||
| 856 | mke2fs -t ext4 -d "$INPUT_PATH" "$INPUT_IMG" 2>/dev/null | ||
| 857 | else | ||
| 858 | # Single file - create temp dir with the file | ||
| 859 | EXTRACT_DIR="$TEMP_DIR/input-extract" | ||
| 860 | mkdir -p "$EXTRACT_DIR" | ||
| 861 | cp "$INPUT_PATH" "$EXTRACT_DIR/" | ||
| 862 | mke2fs -t ext4 -d "$EXTRACT_DIR" "$INPUT_IMG" 2>/dev/null | ||
| 863 | fi | ||
| 864 | |||
| 865 | DISK_OPTS="-drive file=$INPUT_IMG,if=virtio,format=raw" | ||
| 866 | log "DEBUG" "Input disk: $(ls -lh "$INPUT_IMG" | awk '{print $5}')" | ||
| 867 | fi | ||
| 868 | |||
| 869 | # Create state disk for persistent storage (--state-dir) | ||
| 870 | STATE_DISK_OPTS="" | ||
| 871 | if [ -n "$STATE_DIR" ]; then | ||
| 872 | mkdir -p "$STATE_DIR" | ||
| 873 | STATE_IMG="$STATE_DIR/$STATE_FILE" | ||
| 874 | |||
| 875 | if [ ! -f "$STATE_IMG" ]; then | ||
| 876 | log "INFO" "Creating new state disk at $STATE_IMG..." | ||
| 877 | # Create 2GB state disk for Docker storage | ||
| 878 | dd if=/dev/zero of="$STATE_IMG" bs=1M count=2048 2>/dev/null | ||
| 879 | mke2fs -t ext4 "$STATE_IMG" 2>/dev/null | ||
| 880 | else | ||
| 881 | log "INFO" "Using existing state disk: $STATE_IMG" | ||
| 882 | fi | ||
| 883 | |||
| 884 | # Use cache=directsync to ensure writes are flushed to disk | ||
| 885 | # Combined with graceful shutdown wait, this ensures data integrity | ||
| 886 | STATE_DISK_OPTS="-drive file=$STATE_IMG,if=virtio,format=raw,cache=directsync" | ||
| 887 | log "DEBUG" "State disk: $(ls -lh "$STATE_IMG" | awk '{print $5}')" | ||
| 888 | fi | ||
| 889 | |||
| 890 | # Create state disk from input-storage tar (--input-storage) | ||
| 891 | if [ -n "$INPUT_STORAGE" ] && [ -z "$STATE_DIR" ]; then | ||
| 892 | if [ ! -f "$INPUT_STORAGE" ]; then | ||
| 893 | log "ERROR" "Input storage file not found: $INPUT_STORAGE" | ||
| 894 | exit 1 | ||
| 895 | fi | ||
| 896 | |||
| 897 | log "INFO" "Creating state disk from $INPUT_STORAGE..." | ||
| 898 | STATE_IMG="$TEMP_DIR/state.img" | ||
| 899 | |||
| 900 | # Calculate size from tar + headroom | ||
| 901 | TAR_SIZE_KB=$(($(stat -c%s "$INPUT_STORAGE") / 1024)) | ||
| 902 | STATE_SIZE_MB=$(( (TAR_SIZE_KB / 1024) * 2 + 500 )) # 2x tar size + 500MB headroom | ||
| 903 | [ $STATE_SIZE_MB -lt 500 ] && STATE_SIZE_MB=500 | ||
| 904 | |||
| 905 | log "DEBUG" "Tar size: ${TAR_SIZE_KB}KB, State disk: ${STATE_SIZE_MB}MB" | ||
| 906 | |||
| 907 | dd if=/dev/zero of="$STATE_IMG" bs=1M count=$STATE_SIZE_MB 2>/dev/null | ||
| 908 | mke2fs -t ext4 "$STATE_IMG" 2>/dev/null | ||
| 909 | |||
| 910 | # Mount and extract tar | ||
| 911 | MOUNT_DIR="$TEMP_DIR/state-mount" | ||
| 912 | mkdir -p "$MOUNT_DIR" | ||
| 913 | |||
| 914 | # Use fuse2fs if available, otherwise need root | ||
| 915 | # Note: We exclude special device files that can't be created without root | ||
| 916 | # Docker's backingFsBlockDev is a block device that gets recreated at runtime anyway | ||
| 917 | # IMPORTANT: The tar has paths like docker/image/... but the state disk is mounted | ||
| 918 | # at /var/lib/docker, so we need to strip the docker/ prefix with --strip-components=1 | ||
| 919 | if command -v fuse2fs >/dev/null 2>&1; then | ||
| 920 | fuse2fs "$STATE_IMG" "$MOUNT_DIR" -o rw | ||
| 921 | tar --no-same-owner --strip-components=1 --exclude=volumes/backingFsBlockDev -xf "$INPUT_STORAGE" -C "$MOUNT_DIR" | ||
| 922 | fusermount -u "$MOUNT_DIR" | ||
| 923 | else | ||
| 924 | log "WARN" "fuse2fs not found, using debugfs to inject tar (slower)" | ||
| 925 | # Extract tar to temp, then use mke2fs -d | ||
| 926 | # Use --no-same-owner since we're not root (ownership set to current user) | ||
| 927 | EXTRACT_DIR="$TEMP_DIR/state-extract" | ||
| 928 | mkdir -p "$EXTRACT_DIR" | ||
| 929 | tar --no-same-owner --strip-components=1 --exclude=volumes/backingFsBlockDev -xf "$INPUT_STORAGE" -C "$EXTRACT_DIR" | ||
| 930 | mke2fs -t ext4 -d "$EXTRACT_DIR" "$STATE_IMG" 2>/dev/null | ||
| 931 | fi | ||
| 932 | |||
| 933 | # Use cache=directsync to ensure writes are flushed to disk | ||
| 934 | STATE_DISK_OPTS="-drive file=$STATE_IMG,if=virtio,format=raw,cache=directsync" | ||
| 935 | log "DEBUG" "State disk: $(ls -lh "$STATE_IMG" | awk '{print $5}')" | ||
| 936 | fi | ||
| 937 | |||
| 938 | # Encode command as base64 | ||
| 939 | DOCKER_CMD_B64=$(echo -n "$DOCKER_CMD" | base64 -w0) | ||
| 940 | |||
| 941 | # Build kernel command line | ||
| 942 | # In interactive mode, use 'quiet' to suppress kernel boot messages | ||
| 943 | # Use CMDLINE_PREFIX for runtime-specific parameters (docker_ or podman_) | ||
| 944 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 945 | KERNEL_APPEND="console=$CONSOLE,115200 quiet loglevel=0 init=/init" | ||
| 946 | else | ||
| 947 | KERNEL_APPEND="console=$CONSOLE,115200 init=/init" | ||
| 948 | fi | ||
| 949 | # Tell init script which runtime we're using | ||
| 950 | KERNEL_APPEND="$KERNEL_APPEND runtime=$RUNTIME" | ||
| 951 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_cmd=$DOCKER_CMD_B64" | ||
| 952 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_input=$INPUT_TYPE" | ||
| 953 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_output=$OUTPUT_TYPE" | ||
| 954 | |||
| 955 | # Tell init script if we have a state disk | ||
| 956 | if [ -n "$STATE_DISK_OPTS" ]; then | ||
| 957 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_state=disk" | ||
| 958 | fi | ||
| 959 | |||
| 960 | # Tell init script if networking is enabled | ||
| 961 | if [ "$NETWORK" = "true" ]; then | ||
| 962 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_network=1" | ||
| 963 | fi | ||
| 964 | |||
| 965 | # Tell init script if interactive mode | ||
| 966 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 967 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_interactive=1" | ||
| 968 | fi | ||
| 969 | |||
| 970 | # Build QEMU command | ||
| 971 | # Drive ordering is important: | ||
| 972 | # /dev/vda = rootfs.img (read-only, ext4 with Docker tools) | ||
| 973 | # /dev/vdb = input disk (if any) | ||
| 974 | # /dev/vdc = state disk (if any) | ||
| 975 | # The preinit script in initramfs mounts /dev/vda and does switch_root | ||
| 976 | # Build QEMU options | ||
| 977 | QEMU_OPTS="$QEMU_MACHINE -nographic -smp 2 -m 2048" | ||
| 978 | if [ "$USE_KVM" = "true" ]; then | ||
| 979 | QEMU_OPTS="$QEMU_OPTS -enable-kvm" | ||
| 980 | fi | ||
| 981 | QEMU_OPTS="$QEMU_OPTS -kernel $KERNEL_IMAGE" | ||
| 982 | QEMU_OPTS="$QEMU_OPTS -initrd $INITRAMFS" | ||
| 983 | QEMU_OPTS="$QEMU_OPTS -drive file=$ROOTFS_IMG,if=virtio,format=raw,readonly=on" | ||
| 984 | QEMU_OPTS="$QEMU_OPTS $DISK_OPTS" | ||
| 985 | QEMU_OPTS="$QEMU_OPTS $STATE_DISK_OPTS" | ||
| 986 | |||
| 987 | # Add networking if enabled (slirp user-mode networking) | ||
| 988 | if [ "$NETWORK" = "true" ]; then | ||
| 989 | # Slirp provides NAT'd outbound connectivity without root privileges | ||
| 990 | # Guest gets 10.0.2.15, gateway is 10.0.2.2, DNS is 10.0.2.3 | ||
| 991 | NETDEV_OPTS="user,id=net0" | ||
| 992 | |||
| 993 | # Add port forwards (hostfwd=tcp::host_port-:container_port) | ||
| 994 | for pf in "${PORT_FORWARDS[@]}"; do | ||
| 995 | # Parse host_port:container_port or host_port:container_port/protocol | ||
| 996 | HOST_PORT="${pf%%:*}" | ||
| 997 | CONTAINER_PART="${pf#*:}" | ||
| 998 | CONTAINER_PORT="${CONTAINER_PART%%/*}" | ||
| 999 | |||
| 1000 | # Check for protocol suffix (default to tcp) | ||
| 1001 | if [[ "$CONTAINER_PART" == */* ]]; then | ||
| 1002 | PROTOCOL="${CONTAINER_PART##*/}" | ||
| 1003 | else | ||
| 1004 | PROTOCOL="tcp" | ||
| 1005 | fi | ||
| 1006 | |||
| 1007 | NETDEV_OPTS="$NETDEV_OPTS,hostfwd=$PROTOCOL::$HOST_PORT-:$CONTAINER_PORT" | ||
| 1008 | log "INFO" "Port forward: $HOST_PORT -> $CONTAINER_PORT ($PROTOCOL)" | ||
| 1009 | done | ||
| 1010 | |||
| 1011 | QEMU_OPTS="$QEMU_OPTS -netdev $NETDEV_OPTS -device virtio-net-pci,netdev=net0" | ||
| 1012 | else | ||
| 1013 | # Explicitly disable networking | ||
| 1014 | QEMU_OPTS="$QEMU_OPTS -nic none" | ||
| 1015 | fi | ||
| 1016 | |||
| 1017 | # Daemon mode: add virtio-serial for command channel | ||
| 1018 | if [ "$DAEMON_MODE" = "start" ]; then | ||
| 1019 | # Check for required tools | ||
| 1020 | if ! command -v socat >/dev/null 2>&1; then | ||
| 1021 | log "ERROR" "Daemon mode requires 'socat' but it is not installed." | ||
| 1022 | log "ERROR" "Install with: sudo apt install socat" | ||
| 1023 | exit 1 | ||
| 1024 | fi | ||
| 1025 | |||
| 1026 | # Check if daemon is already running | ||
| 1027 | if daemon_is_running; then | ||
| 1028 | log "ERROR" "Daemon is already running. Use --daemon-stop first." | ||
| 1029 | exit 1 | ||
| 1030 | fi | ||
| 1031 | |||
| 1032 | # Create socket directory | ||
| 1033 | mkdir -p "$DAEMON_SOCKET_DIR" | ||
| 1034 | |||
| 1035 | # Create shared directory for file I/O (virtio-9p) | ||
| 1036 | DAEMON_SHARE_DIR="$DAEMON_SOCKET_DIR/share" | ||
| 1037 | mkdir -p "$DAEMON_SHARE_DIR" | ||
| 1038 | |||
| 1039 | # Add virtio-9p for shared directory access | ||
| 1040 | # Host writes to $DAEMON_SHARE_DIR, guest mounts as /mnt/share | ||
| 1041 | # Use runtime-specific mount tag (vdkr_share or vpdmn_share) | ||
| 1042 | SHARE_TAG="${TOOL_NAME}_share" | ||
| 1043 | # Use security_model=none for simplest file sharing (no permission mapping) | ||
| 1044 | # This allows writes from container (running as root) to propagate to host | ||
| 1045 | QEMU_OPTS="$QEMU_OPTS -virtfs local,path=$DAEMON_SHARE_DIR,mount_tag=$SHARE_TAG,security_model=none,id=$SHARE_TAG" | ||
| 1046 | |||
| 1047 | # Add virtio-serial device for command channel | ||
| 1048 | # Using virtserialport creates /dev/vport0p1 in guest, host sees unix socket | ||
| 1049 | # virtconsole would use hvc* but requires virtio_console kernel module | ||
| 1050 | QEMU_OPTS="$QEMU_OPTS -chardev socket,id=vdkr,path=$DAEMON_SOCKET,server=on,wait=off" | ||
| 1051 | QEMU_OPTS="$QEMU_OPTS -device virtio-serial-pci" | ||
| 1052 | QEMU_OPTS="$QEMU_OPTS -device virtserialport,chardev=vdkr,name=vdkr" | ||
| 1053 | |||
| 1054 | # Tell init script to run in daemon mode | ||
| 1055 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_daemon=1" | ||
| 1056 | |||
| 1057 | # Always enable networking for daemon mode | ||
| 1058 | if [ "$NETWORK" != "true" ]; then | ||
| 1059 | log "INFO" "Enabling networking for daemon mode" | ||
| 1060 | NETWORK="true" | ||
| 1061 | # Build netdev options with any port forwards | ||
| 1062 | DAEMON_NETDEV="user,id=net0" | ||
| 1063 | for pf in "${PORT_FORWARDS[@]}"; do | ||
| 1064 | # Parse host_port:container_port or host_port:container_port/protocol | ||
| 1065 | HOST_PORT="${pf%%:*}" | ||
| 1066 | CONTAINER_PART="${pf#*:}" | ||
| 1067 | CONTAINER_PORT="${CONTAINER_PART%%/*}" | ||
| 1068 | if [[ "$CONTAINER_PART" == */* ]]; then | ||
| 1069 | PROTOCOL="${CONTAINER_PART##*/}" | ||
| 1070 | else | ||
| 1071 | PROTOCOL="tcp" | ||
| 1072 | fi | ||
| 1073 | DAEMON_NETDEV="$DAEMON_NETDEV,hostfwd=$PROTOCOL::$HOST_PORT-:$CONTAINER_PORT" | ||
| 1074 | log "INFO" "Port forward: $HOST_PORT -> $CONTAINER_PORT ($PROTOCOL)" | ||
| 1075 | done | ||
| 1076 | QEMU_OPTS="$QEMU_OPTS -netdev $DAEMON_NETDEV -device virtio-net-pci,netdev=net0" | ||
| 1077 | else | ||
| 1078 | # NETWORK was already true, but check if we need to add port forwards | ||
| 1079 | # that weren't included in the earlier networking setup | ||
| 1080 | # (This happens when NETWORK was set to true before daemon mode was detected) | ||
| 1081 | if [ ${#PORT_FORWARDS[@]} -gt 0 ]; then | ||
| 1082 | # Port forwards should already be included from earlier networking setup | ||
| 1083 | for pf in "${PORT_FORWARDS[@]}"; do | ||
| 1084 | HOST_PORT="${pf%%:*}" | ||
| 1085 | CONTAINER_PART="${pf#*:}" | ||
| 1086 | CONTAINER_PORT="${CONTAINER_PART%%/*}" | ||
| 1087 | log "INFO" "Port forward configured: $HOST_PORT -> $CONTAINER_PORT" | ||
| 1088 | done | ||
| 1089 | fi | ||
| 1090 | fi | ||
| 1091 | |||
| 1092 | log "INFO" "Starting daemon..." | ||
| 1093 | log "DEBUG" "PID file: $DAEMON_PID_FILE" | ||
| 1094 | log "DEBUG" "Socket: $DAEMON_SOCKET" | ||
| 1095 | log "DEBUG" "Command: $QEMU_CMD $QEMU_OPTS -append \"$KERNEL_APPEND\"" | ||
| 1096 | |||
| 1097 | # Start QEMU in background | ||
| 1098 | $QEMU_CMD $QEMU_OPTS -append "$KERNEL_APPEND" > "$DAEMON_QEMU_LOG" 2>&1 & | ||
| 1099 | QEMU_PID=$! | ||
| 1100 | echo "$QEMU_PID" > "$DAEMON_PID_FILE" | ||
| 1101 | |||
| 1102 | log "INFO" "QEMU started (PID: $QEMU_PID)" | ||
| 1103 | |||
| 1104 | # Wait for socket to appear (Docker starting) | ||
| 1105 | # Docker can take 60+ seconds to start, so wait up to 120 seconds | ||
| 1106 | log "INFO" "Waiting for daemon to be ready..." | ||
| 1107 | READY=false | ||
| 1108 | for i in $(seq 1 120); do | ||
| 1109 | if [ -S "$DAEMON_SOCKET" ]; then | ||
| 1110 | # Socket exists, try to connect | ||
| 1111 | # Keep stdin open for 3 seconds to allow response to arrive | ||
| 1112 | RESPONSE=$( { echo "===PING==="; sleep 3; } | timeout 10 socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true) | ||
| 1113 | if echo "$RESPONSE" | grep -q "===PONG==="; then | ||
| 1114 | log "DEBUG" "Got PONG response" | ||
| 1115 | READY=true | ||
| 1116 | break | ||
| 1117 | else | ||
| 1118 | log "DEBUG" "No PONG, got: $RESPONSE" | ||
| 1119 | fi | ||
| 1120 | fi | ||
| 1121 | |||
| 1122 | # Check if QEMU died | ||
| 1123 | if ! kill -0 "$QEMU_PID" 2>/dev/null; then | ||
| 1124 | log "ERROR" "QEMU process died during startup" | ||
| 1125 | cat "$DAEMON_QEMU_LOG" >&2 | ||
| 1126 | rm -f "$DAEMON_PID_FILE" | ||
| 1127 | exit 1 | ||
| 1128 | fi | ||
| 1129 | |||
| 1130 | log "DEBUG" "Waiting... ($i/60)" | ||
| 1131 | sleep 1 | ||
| 1132 | done | ||
| 1133 | |||
| 1134 | if [ "$READY" = "true" ]; then | ||
| 1135 | log "INFO" "Daemon is ready!" | ||
| 1136 | echo "Daemon running (PID: $QEMU_PID)" | ||
| 1137 | echo "Socket: $DAEMON_SOCKET" | ||
| 1138 | exit 0 | ||
| 1139 | else | ||
| 1140 | log "ERROR" "Daemon failed to become ready within 120 seconds" | ||
| 1141 | cat "$DAEMON_QEMU_LOG" >&2 | ||
| 1142 | kill "$QEMU_PID" 2>/dev/null || true | ||
| 1143 | rm -f "$DAEMON_PID_FILE" "$DAEMON_SOCKET" | ||
| 1144 | exit 1 | ||
| 1145 | fi | ||
| 1146 | fi | ||
| 1147 | |||
| 1148 | log "INFO" "Starting QEMU..." | ||
| 1149 | log "DEBUG" "Command: $QEMU_CMD $QEMU_OPTS -append \"$KERNEL_APPEND\"" | ||
| 1150 | |||
| 1151 | # Interactive mode runs QEMU in foreground with stdio connected | ||
| 1152 | if [ "$INTERACTIVE" = "true" ]; then | ||
| 1153 | # Check if stdin is a terminal | ||
| 1154 | if [ ! -t 0 ]; then | ||
| 1155 | log "WARN" "Interactive mode requested but stdin is not a terminal" | ||
| 1156 | fi | ||
| 1157 | |||
| 1158 | # Show a starting message | ||
| 1159 | # The init script will clear this line when the container is ready | ||
| 1160 | if [ -t 1 ]; then | ||
| 1161 | printf "\r\033[0;36m[vdkr]\033[0m Starting container... \r" | ||
| 1162 | fi | ||
| 1163 | |||
| 1164 | # Save terminal settings to restore later | ||
| 1165 | if [ -t 0 ]; then | ||
| 1166 | SAVED_STTY=$(stty -g) | ||
| 1167 | # Put terminal in raw mode so Ctrl+C etc go to guest | ||
| 1168 | stty raw -echo | ||
| 1169 | fi | ||
| 1170 | |||
| 1171 | # Run QEMU with stdio (not in background) | ||
| 1172 | # The -serial mon:stdio connects the serial console to our terminal | ||
| 1173 | $QEMU_CMD $QEMU_OPTS -append "$KERNEL_APPEND" | ||
| 1174 | QEMU_EXIT=$? | ||
| 1175 | |||
| 1176 | # Restore terminal settings | ||
| 1177 | if [ -t 0 ]; then | ||
| 1178 | stty "$SAVED_STTY" | ||
| 1179 | fi | ||
| 1180 | |||
| 1181 | echo "" | ||
| 1182 | log "INFO" "Interactive session ended (exit code: $QEMU_EXIT)" | ||
| 1183 | exit $QEMU_EXIT | ||
| 1184 | fi | ||
| 1185 | |||
| 1186 | # Non-interactive mode: run QEMU in background and capture output | ||
| 1187 | QEMU_OUTPUT="$TEMP_DIR/qemu_output.txt" | ||
| 1188 | timeout $TIMEOUT $QEMU_CMD $QEMU_OPTS -append "$KERNEL_APPEND" > "$QEMU_OUTPUT" 2>&1 & | ||
| 1189 | QEMU_PID=$! | ||
| 1190 | |||
| 1191 | # Monitor for completion | ||
| 1192 | COMPLETE=false | ||
| 1193 | for i in $(seq 1 $TIMEOUT); do | ||
| 1194 | if [ ! -d "/proc/$QEMU_PID" ]; then | ||
| 1195 | log "DEBUG" "QEMU ended after $i seconds" | ||
| 1196 | break | ||
| 1197 | fi | ||
| 1198 | |||
| 1199 | # Check for completion markers based on output type | ||
| 1200 | case "$OUTPUT_TYPE" in | ||
| 1201 | text) | ||
| 1202 | if grep -q "===OUTPUT_END===" "$QEMU_OUTPUT" 2>/dev/null; then | ||
| 1203 | COMPLETE=true | ||
| 1204 | break | ||
| 1205 | fi | ||
| 1206 | ;; | ||
| 1207 | tar) | ||
| 1208 | if grep -q "===TAR_END===" "$QEMU_OUTPUT" 2>/dev/null; then | ||
| 1209 | COMPLETE=true | ||
| 1210 | break | ||
| 1211 | fi | ||
| 1212 | ;; | ||
| 1213 | storage) | ||
| 1214 | if grep -q "===STORAGE_END===" "$QEMU_OUTPUT" 2>/dev/null; then | ||
| 1215 | COMPLETE=true | ||
| 1216 | break | ||
| 1217 | fi | ||
| 1218 | ;; | ||
| 1219 | esac | ||
| 1220 | |||
| 1221 | # Check for error | ||
| 1222 | if grep -q "===ERROR===" "$QEMU_OUTPUT" 2>/dev/null; then | ||
| 1223 | log "ERROR" "Error in QEMU:" | ||
| 1224 | grep -A10 "===ERROR===" "$QEMU_OUTPUT" | ||
| 1225 | break | ||
| 1226 | fi | ||
| 1227 | |||
| 1228 | # Progress indicator | ||
| 1229 | if [ $((i % 30)) -eq 0 ]; then | ||
| 1230 | if grep -q "Docker daemon is ready" "$QEMU_OUTPUT" 2>/dev/null; then | ||
| 1231 | log "INFO" "Docker is running, executing command..." | ||
| 1232 | elif grep -q "Starting Docker" "$QEMU_OUTPUT" 2>/dev/null; then | ||
| 1233 | log "INFO" "Docker is starting..." | ||
| 1234 | fi | ||
| 1235 | fi | ||
| 1236 | |||
| 1237 | sleep 1 | ||
| 1238 | done | ||
| 1239 | |||
| 1240 | # Wait for QEMU to exit gracefully (poweroff from inside flushes disks properly) | ||
| 1241 | # Only kill if it hangs after seeing completion marker | ||
| 1242 | if [ "$COMPLETE" = "true" ] && [ -d "/proc/$QEMU_PID" ]; then | ||
| 1243 | log "DEBUG" "Waiting for QEMU to complete graceful shutdown..." | ||
| 1244 | # Give QEMU up to 30 seconds to poweroff after command completes | ||
| 1245 | for wait_i in $(seq 1 30); do | ||
| 1246 | if [ ! -d "/proc/$QEMU_PID" ]; then | ||
| 1247 | log "DEBUG" "QEMU shutdown complete" | ||
| 1248 | break | ||
| 1249 | fi | ||
| 1250 | sleep 1 | ||
| 1251 | done | ||
| 1252 | fi | ||
| 1253 | |||
| 1254 | # Force kill QEMU only if still running after grace period | ||
| 1255 | if [ -d "/proc/$QEMU_PID" ]; then | ||
| 1256 | log "WARN" "QEMU still running, forcing termination..." | ||
| 1257 | kill $QEMU_PID 2>/dev/null || true | ||
| 1258 | wait $QEMU_PID 2>/dev/null || true | ||
| 1259 | fi | ||
| 1260 | |||
| 1261 | # Extract results | ||
| 1262 | if [ "$COMPLETE" = "true" ]; then | ||
| 1263 | # Get exit code | ||
| 1264 | EXIT_CODE=$(grep -oP '===EXIT_CODE=\K[0-9]+' "$QEMU_OUTPUT" | head -1) | ||
| 1265 | EXIT_CODE="${EXIT_CODE:-0}" | ||
| 1266 | |||
| 1267 | case "$OUTPUT_TYPE" in | ||
| 1268 | text) | ||
| 1269 | log "INFO" "=== Command Output ===" | ||
| 1270 | # Use awk for precise extraction between markers | ||
| 1271 | awk '/===OUTPUT_START===/{capture=1; next} /===OUTPUT_END===/{capture=0} capture' "$QEMU_OUTPUT" | ||
| 1272 | log "INFO" "=== Exit Code: $EXIT_CODE ===" | ||
| 1273 | ;; | ||
| 1274 | |||
| 1275 | tar) | ||
| 1276 | log "INFO" "Extracting tar output..." | ||
| 1277 | # Use awk for precise extraction between markers | ||
| 1278 | # Strip ANSI escape codes and non-base64 characters from serial console output | ||
| 1279 | awk '/===TAR_START===/{capture=1; next} /===TAR_END===/{capture=0} capture' "$QEMU_OUTPUT" | \ | ||
| 1280 | tr -d '\r' | sed 's/\x1b\[[0-9;]*m//g' | tr -cd 'A-Za-z0-9+/=\n' | base64 -d > "$OUTPUT_FILE" 2>"${TEMP_DIR}/b64_errors.txt" | ||
| 1281 | |||
| 1282 | if [ -s "${TEMP_DIR}/b64_errors.txt" ]; then | ||
| 1283 | log "WARN" "Base64 decode warnings: $(cat "${TEMP_DIR}/b64_errors.txt")" | ||
| 1284 | fi | ||
| 1285 | |||
| 1286 | if tar -tf "$OUTPUT_FILE" >/dev/null 2>&1; then | ||
| 1287 | log "INFO" "SUCCESS: Output saved to $OUTPUT_FILE" | ||
| 1288 | log "INFO" "Size: $(ls -lh "$OUTPUT_FILE" | awk '{print $5}')" | ||
| 1289 | else | ||
| 1290 | log "ERROR" "Output file is not a valid tar" | ||
| 1291 | exit 1 | ||
| 1292 | fi | ||
| 1293 | ;; | ||
| 1294 | |||
| 1295 | storage) | ||
| 1296 | log "INFO" "Extracting storage..." | ||
| 1297 | # Use awk for precise extraction: capture lines between markers (not including markers) | ||
| 1298 | # This avoids grep -v "===" which could accidentally remove valid base64 lines | ||
| 1299 | # Pipeline: | ||
| 1300 | # 1. awk: extract lines between STORAGE_START and STORAGE_END markers | ||
| 1301 | # 2. tr -d '\r': remove carriage returns | ||
| 1302 | # 3. sed: remove ANSI escape codes | ||
| 1303 | # 4. grep -v: remove kernel log messages (lines starting with [ followed by timestamp) | ||
| 1304 | # 5. tr -cd: keep only valid base64 characters | ||
| 1305 | awk '/===STORAGE_START===/{capture=1; next} /===STORAGE_END===/{capture=0} capture' "$QEMU_OUTPUT" | \ | ||
| 1306 | tr -d '\r' | \ | ||
| 1307 | sed 's/\x1b\[[0-9;]*m//g' | \ | ||
| 1308 | grep -v '^\[[[:space:]]*[0-9]' | \ | ||
| 1309 | tr -cd 'A-Za-z0-9+/=\n' > "${TEMP_DIR}/storage_b64.txt" | ||
| 1310 | |||
| 1311 | B64_SIZE=$(wc -c < "${TEMP_DIR}/storage_b64.txt") | ||
| 1312 | log "DEBUG" "Base64 data extracted: $B64_SIZE bytes" | ||
| 1313 | |||
| 1314 | # Decode with error reporting (not suppressed) | ||
| 1315 | if ! base64 -d < "${TEMP_DIR}/storage_b64.txt" > "$OUTPUT_FILE" 2>"${TEMP_DIR}/b64_errors.txt"; then | ||
| 1316 | log "ERROR" "Base64 decode failed" | ||
| 1317 | if [ -s "${TEMP_DIR}/b64_errors.txt" ]; then | ||
| 1318 | log "ERROR" "Decode errors: $(cat "${TEMP_DIR}/b64_errors.txt")" | ||
| 1319 | fi | ||
| 1320 | # Show a sample of the base64 data for debugging | ||
| 1321 | log "DEBUG" "First 200 chars of base64: $(head -c 200 "${TEMP_DIR}/storage_b64.txt")" | ||
| 1322 | log "DEBUG" "Last 200 chars of base64: $(tail -c 200 "${TEMP_DIR}/storage_b64.txt")" | ||
| 1323 | exit 1 | ||
| 1324 | fi | ||
| 1325 | |||
| 1326 | DECODED_SIZE=$(wc -c < "$OUTPUT_FILE") | ||
| 1327 | log "DEBUG" "Decoded storage size: $DECODED_SIZE bytes" | ||
| 1328 | |||
| 1329 | if tar -tf "$OUTPUT_FILE" >/dev/null 2>&1; then | ||
| 1330 | log "INFO" "SUCCESS: Docker storage saved to $OUTPUT_FILE" | ||
| 1331 | log "INFO" "Size: $(ls -lh "$OUTPUT_FILE" | awk '{print $5}')" | ||
| 1332 | log "INFO" "" | ||
| 1333 | log "INFO" "To deploy: tar -xf $OUTPUT_FILE -C /var/lib/" | ||
| 1334 | else | ||
| 1335 | log "ERROR" "Storage file is not a valid tar (size: $DECODED_SIZE bytes)" | ||
| 1336 | log "DEBUG" "Tar validation output: $(tar -tf "$OUTPUT_FILE" 2>&1 | head -10)" | ||
| 1337 | exit 1 | ||
| 1338 | fi | ||
| 1339 | ;; | ||
| 1340 | esac | ||
| 1341 | |||
| 1342 | exit "${EXIT_CODE:-0}" | ||
| 1343 | else | ||
| 1344 | log "ERROR" "Command execution failed or timed out" | ||
| 1345 | log "ERROR" "QEMU output saved to: $QEMU_OUTPUT" | ||
| 1346 | |||
| 1347 | if [ "$VERBOSE" = "true" ]; then | ||
| 1348 | log "DEBUG" "=== Last 50 lines of QEMU output ===" | ||
| 1349 | tail -50 "$QEMU_OUTPUT" | ||
| 1350 | fi | ||
| 1351 | |||
| 1352 | exit 1 | ||
| 1353 | fi | ||
diff --git a/recipes-containers/vcontainer/vcontainer-initramfs-create.inc b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc new file mode 100644 index 00000000..a0930a68 --- /dev/null +++ b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc | |||
| @@ -0,0 +1,237 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | # | ||
| 5 | # vcontainer-initramfs-create.inc | ||
| 6 | # =========================================================================== | ||
| 7 | # Shared code for building QEMU boot blobs (vdkr/vpdmn) | ||
| 8 | # =========================================================================== | ||
| 9 | # | ||
| 10 | # This .inc file contains common code for building boot blobs. | ||
| 11 | # Individual recipes (vdkr-initramfs-create, vpdmn-initramfs-create) | ||
| 12 | # set VCONTAINER_RUNTIME and include this file. | ||
| 13 | # | ||
| 14 | # Required variables from including recipe: | ||
| 15 | # VCONTAINER_RUNTIME - "vdkr" or "vpdmn" | ||
| 16 | # | ||
| 17 | # Boot flow: | ||
| 18 | # QEMU boots kernel + tiny initramfs | ||
| 19 | # -> preinit mounts rootfs.img from /dev/vda | ||
| 20 | # -> switch_root into rootfs.img | ||
| 21 | # -> ${VCONTAINER_RUNTIME}-init.sh runs | ||
| 22 | # | ||
| 23 | # =========================================================================== | ||
| 24 | |||
| 25 | HOMEPAGE = "https://git.yoctoproject.org/meta-virtualization/" | ||
| 26 | LICENSE = "MIT" | ||
| 27 | LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" | ||
| 28 | |||
| 29 | inherit deploy | ||
| 30 | |||
| 31 | # Not built by default - user must explicitly request via bitbake or vcontainer-native | ||
| 32 | EXCLUDE_FROM_WORLD = "1" | ||
| 33 | |||
| 34 | # Need squashfs-tools-native for unsquashfs to extract files from rootfs.img | ||
| 35 | DEPENDS = "squashfs-tools-native" | ||
| 36 | |||
| 37 | # Always rebuild - no sstate caching for this recipe | ||
| 38 | # This ensures source file changes (like init scripts in the rootfs) are picked up | ||
| 39 | SSTATE_SKIP_CREATION = "1" | ||
| 40 | do_compile[nostamp] = "1" | ||
| 41 | do_deploy[nostamp] = "1" | ||
| 42 | |||
| 43 | # Only populate native sysroot, skip target sysroot to avoid libgcc conflicts | ||
| 44 | INHIBIT_DEFAULT_DEPS = "1" | ||
| 45 | |||
| 46 | # Dependencies: | ||
| 47 | # 1. The multiconfig rootfs image from same vruntime-* multiconfig | ||
| 48 | # 2. The kernel from main build (not multiconfig) | ||
| 49 | # | ||
| 50 | # Use regular depends for rootfs-image since both recipes are in the same multiconfig | ||
| 51 | # Use mcdepends for kernel since it's from the main (default) config | ||
| 52 | do_compile[depends] = "${VCONTAINER_RUNTIME}-rootfs-image:do_image_complete" | ||
| 53 | do_compile[mcdepends] = "mc:${VCONTAINER_MULTICONFIG}::virtual/kernel:do_deploy" | ||
| 54 | |||
| 55 | # Preinit is shared between vdkr and vpdmn | ||
| 56 | SRC_URI = "file://vdkr-preinit.sh" | ||
| 57 | |||
| 58 | S = "${UNPACKDIR}" | ||
| 59 | B = "${WORKDIR}/build" | ||
| 60 | |||
| 61 | def get_kernel_image_name(d): | ||
| 62 | arch = d.getVar('TARGET_ARCH') | ||
| 63 | if arch == 'aarch64': | ||
| 64 | return 'Image' | ||
| 65 | elif arch in ['x86_64', 'i686', 'i586']: | ||
| 66 | return 'bzImage' | ||
| 67 | elif arch == 'arm': | ||
| 68 | return 'zImage' | ||
| 69 | return 'Image' | ||
| 70 | |||
| 71 | def get_multiconfig_name(d): | ||
| 72 | arch = d.getVar('TARGET_ARCH') | ||
| 73 | if arch == 'aarch64': | ||
| 74 | return 'vruntime-aarch64' | ||
| 75 | elif arch in ['x86_64', 'i686', 'i586']: | ||
| 76 | return 'vruntime-x86-64' | ||
| 77 | return 'vruntime-aarch64' | ||
| 78 | |||
| 79 | def get_blob_arch(d): | ||
| 80 | """Map TARGET_ARCH to vrunner blob architecture (aarch64 or x86_64)""" | ||
| 81 | arch = d.getVar('TARGET_ARCH') | ||
| 82 | if arch == 'aarch64': | ||
| 83 | return 'aarch64' | ||
| 84 | elif arch in ['x86_64', 'i686', 'i586']: | ||
| 85 | return 'x86_64' | ||
| 86 | return 'aarch64' | ||
| 87 | |||
| 88 | KERNEL_IMAGETYPE_INITRAMFS = "${@get_kernel_image_name(d)}" | ||
| 89 | VCONTAINER_MULTICONFIG = "${@get_multiconfig_name(d)}" | ||
| 90 | BLOB_ARCH = "${@get_blob_arch(d)}" | ||
| 91 | |||
| 92 | # Path to the multiconfig build output | ||
| 93 | VCONTAINER_MC_DEPLOY = "${TOPDIR}/tmp-${VCONTAINER_MULTICONFIG}/deploy/images/${MACHINE}" | ||
| 94 | |||
| 95 | do_compile() { | ||
| 96 | mkdir -p ${B} | ||
| 97 | |||
| 98 | # ========================================================================= | ||
| 99 | # PART 1: BUILD TINY INITRAMFS (just for switch_root) | ||
| 100 | # ========================================================================= | ||
| 101 | INITRAMFS_DIR="${B}/initramfs" | ||
| 102 | rm -rf ${INITRAMFS_DIR} | ||
| 103 | mkdir -p ${INITRAMFS_DIR}/bin | ||
| 104 | mkdir -p ${INITRAMFS_DIR}/proc | ||
| 105 | mkdir -p ${INITRAMFS_DIR}/sys | ||
| 106 | mkdir -p ${INITRAMFS_DIR}/dev | ||
| 107 | mkdir -p ${INITRAMFS_DIR}/mnt/root | ||
| 108 | |||
| 109 | bbnote "Building tiny initramfs for switch_root..." | ||
| 110 | |||
| 111 | # Extract busybox from the multiconfig rootfs image (squashfs) | ||
| 112 | MC_TMPDIR="${TOPDIR}/tmp-${VCONTAINER_MULTICONFIG}" | ||
| 113 | ROOTFS_SRC="${MC_TMPDIR}/deploy/images/${MACHINE}/${VCONTAINER_RUNTIME}-rootfs-image-${MACHINE}.rootfs.squashfs" | ||
| 114 | |||
| 115 | if [ ! -f "${ROOTFS_SRC}" ]; then | ||
| 116 | bbfatal "Rootfs image not found at ${ROOTFS_SRC}. Build it first with: bitbake mc:${VCONTAINER_MULTICONFIG}:${VCONTAINER_RUNTIME}-rootfs-image" | ||
| 117 | fi | ||
| 118 | |||
| 119 | # Extract busybox from rootfs using unsquashfs | ||
| 120 | # In usrmerge layouts, busybox is at usr/bin/busybox | ||
| 121 | BUSYBOX_PATH="usr/bin/busybox" | ||
| 122 | EXTRACT_DIR="${B}/squashfs-extract" | ||
| 123 | |||
| 124 | bbnote "Extracting busybox from $BUSYBOX_PATH" | ||
| 125 | # Try native sysroot first, fall back to system unsquashfs | ||
| 126 | UNSQUASHFS="${WORKDIR}/recipe-sysroot-native/usr/bin/unsquashfs" | ||
| 127 | if [ ! -x "$UNSQUASHFS" ]; then | ||
| 128 | UNSQUASHFS="/usr/bin/unsquashfs" | ||
| 129 | fi | ||
| 130 | if [ ! -x "$UNSQUASHFS" ]; then | ||
| 131 | bbfatal "unsquashfs not found in native sysroot or at /usr/bin/unsquashfs" | ||
| 132 | fi | ||
| 133 | bbnote "Using unsquashfs: $UNSQUASHFS" | ||
| 134 | |||
| 135 | rm -rf "${EXTRACT_DIR}" | ||
| 136 | $UNSQUASHFS -d "${EXTRACT_DIR}" "${ROOTFS_SRC}" "$BUSYBOX_PATH" | ||
| 137 | |||
| 138 | if [ ! -f "${EXTRACT_DIR}/${BUSYBOX_PATH}" ]; then | ||
| 139 | bbfatal "Failed to extract busybox from rootfs image" | ||
| 140 | fi | ||
| 141 | cp "${EXTRACT_DIR}/${BUSYBOX_PATH}" "${INITRAMFS_DIR}/bin/busybox" | ||
| 142 | chmod +x ${INITRAMFS_DIR}/bin/busybox | ||
| 143 | |||
| 144 | # Create minimal symlinks | ||
| 145 | cd ${INITRAMFS_DIR}/bin | ||
| 146 | for cmd in sh mount umount mkdir ls cat echo sleep switch_root reboot; do | ||
| 147 | ln -sf busybox $cmd 2>/dev/null || true | ||
| 148 | done | ||
| 149 | cd - | ||
| 150 | |||
| 151 | # Install preinit script as /init | ||
| 152 | cp ${S}/vdkr-preinit.sh ${INITRAMFS_DIR}/init | ||
| 153 | chmod +x ${INITRAMFS_DIR}/init | ||
| 154 | |||
| 155 | # Create tiny initramfs cpio | ||
| 156 | bbnote "Creating tiny initramfs cpio archive..." | ||
| 157 | cd ${INITRAMFS_DIR} | ||
| 158 | find . | cpio -o -H newc 2>/dev/null | gzip -9 > ${B}/initramfs.cpio.gz | ||
| 159 | cd - | ||
| 160 | |||
| 161 | INITRAMFS_SIZE=$(stat -c%s ${B}/initramfs.cpio.gz) | ||
| 162 | bbnote "Tiny initramfs created: ${INITRAMFS_SIZE} bytes ($(expr ${INITRAMFS_SIZE} / 1024)KB)" | ||
| 163 | |||
| 164 | # ========================================================================= | ||
| 165 | # PART 2: COPY ROOTFS FROM MULTICONFIG BUILD | ||
| 166 | # ========================================================================= | ||
| 167 | bbnote "Looking for multiconfig rootfs at: ${MC_TMPDIR}/deploy/images/${MACHINE}" | ||
| 168 | |||
| 169 | # ROOTFS_SRC already set above when extracting busybox | ||
| 170 | cp "${ROOTFS_SRC}" ${B}/rootfs.img | ||
| 171 | ROOTFS_SIZE=$(stat -c%s ${B}/rootfs.img) | ||
| 172 | bbnote "Rootfs image copied: ${ROOTFS_SIZE} bytes ($(expr ${ROOTFS_SIZE} / 1024 / 1024)MB)" | ||
| 173 | |||
| 174 | # ========================================================================= | ||
| 175 | # PART 3: COPY KERNEL | ||
| 176 | # ========================================================================= | ||
| 177 | bbnote "Copying kernel image..." | ||
| 178 | KERNEL_FILE="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE_INITRAMFS}" | ||
| 179 | if [ -f "${KERNEL_FILE}" ]; then | ||
| 180 | cp "${KERNEL_FILE}" ${B}/kernel | ||
| 181 | KERNEL_SIZE=$(stat -c%s ${B}/kernel) | ||
| 182 | bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)" | ||
| 183 | else | ||
| 184 | bbwarn "Kernel not found at ${KERNEL_FILE}" | ||
| 185 | fi | ||
| 186 | } | ||
| 187 | |||
| 188 | do_install[noexec] = "1" | ||
| 189 | do_package[noexec] = "1" | ||
| 190 | do_packagedata[noexec] = "1" | ||
| 191 | do_package_write_rpm[noexec] = "1" | ||
| 192 | do_package_write_ipk[noexec] = "1" | ||
| 193 | do_package_write_deb[noexec] = "1" | ||
| 194 | do_populate_sysroot[noexec] = "1" | ||
| 195 | |||
| 196 | do_deploy() { | ||
| 197 | # Deploy to ${VCONTAINER_RUNTIME}/<arch> subdirectory | ||
| 198 | install -d ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH} | ||
| 199 | |||
| 200 | if [ -f ${B}/initramfs.cpio.gz ]; then | ||
| 201 | install -m 0644 ${B}/initramfs.cpio.gz ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/ | ||
| 202 | bbnote "Deployed initramfs.cpio.gz to ${VCONTAINER_RUNTIME}/${BLOB_ARCH}/" | ||
| 203 | fi | ||
| 204 | |||
| 205 | if [ -f ${B}/rootfs.img ]; then | ||
| 206 | install -m 0644 ${B}/rootfs.img ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/ | ||
| 207 | bbnote "Deployed rootfs.img to ${VCONTAINER_RUNTIME}/${BLOB_ARCH}/" | ||
| 208 | fi | ||
| 209 | |||
| 210 | if [ -f ${B}/kernel ]; then | ||
| 211 | install -m 0644 ${B}/kernel ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/${KERNEL_IMAGETYPE_INITRAMFS} | ||
| 212 | bbnote "Deployed kernel as ${VCONTAINER_RUNTIME}/${BLOB_ARCH}/${KERNEL_IMAGETYPE_INITRAMFS}" | ||
| 213 | fi | ||
| 214 | |||
| 215 | cat > ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/README << EOF | ||
| 216 | ${VCONTAINER_RUNTIME} Boot Blobs | ||
| 217 | ================== | ||
| 218 | |||
| 219 | Built for: ${TARGET_ARCH} | ||
| 220 | Machine: ${MACHINE} | ||
| 221 | Multiconfig: ${VCONTAINER_MULTICONFIG} | ||
| 222 | Date: $(date) | ||
| 223 | |||
| 224 | Files: | ||
| 225 | ${KERNEL_IMAGETYPE_INITRAMFS} - Kernel image for QEMU | ||
| 226 | initramfs.cpio.gz - Tiny initramfs (switch_root only) | ||
| 227 | rootfs.img - Root filesystem with container tools | ||
| 228 | |||
| 229 | Boot flow: | ||
| 230 | QEMU boots kernel + initramfs | ||
| 231 | -> preinit mounts rootfs.img from /dev/vda | ||
| 232 | -> switch_root into rootfs.img | ||
| 233 | -> ${VCONTAINER_RUNTIME}-init.sh runs container commands | ||
| 234 | EOF | ||
| 235 | } | ||
| 236 | |||
| 237 | addtask deploy after do_compile before do_build | ||
diff --git a/recipes-containers/vcontainer/vcontainer-native.bb b/recipes-containers/vcontainer/vcontainer-native.bb new file mode 100644 index 00000000..1d19ac1b --- /dev/null +++ b/recipes-containers/vcontainer/vcontainer-native.bb | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | # | ||
| 5 | # vcontainer-native.bb | ||
| 6 | # =========================================================================== | ||
| 7 | # Native recipe providing vrunner.sh for container cross-installation | ||
| 8 | # =========================================================================== | ||
| 9 | # | ||
| 10 | # This recipe installs vrunner.sh into the native sysroot so that | ||
| 11 | # container-bundle.bbclass and container-cross-install.bbclass can use it | ||
| 12 | # to cross-install containers into target images. | ||
| 13 | # | ||
| 14 | # Note: This does NOT build the blobs. Blobs must be built separately via | ||
| 15 | # multiconfig (see vdkr-initramfs-create, vpdmn-initramfs-create). | ||
| 16 | # | ||
| 17 | # =========================================================================== | ||
| 18 | |||
| 19 | SUMMARY = "Container cross-install runner script" | ||
| 20 | DESCRIPTION = "Provides vrunner.sh for cross-installing containers into images" | ||
| 21 | HOMEPAGE = "https://git.yoctoproject.org/meta-virtualization/" | ||
| 22 | LICENSE = "MIT" | ||
| 23 | LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" | ||
| 24 | |||
| 25 | inherit native | ||
| 26 | |||
| 27 | # Runtime dependencies for vrunner.sh | ||
| 28 | DEPENDS = "coreutils-native socat-native" | ||
| 29 | |||
| 30 | SRC_URI = "\ | ||
| 31 | file://vrunner.sh \ | ||
| 32 | file://vcontainer-common.sh \ | ||
| 33 | " | ||
| 34 | |||
| 35 | S = "${UNPACKDIR}" | ||
| 36 | |||
| 37 | do_install() { | ||
| 38 | install -d ${D}${bindir} | ||
| 39 | install -m 0755 ${S}/vrunner.sh ${D}${bindir}/vrunner.sh | ||
| 40 | install -m 0644 ${S}/vcontainer-common.sh ${D}${bindir}/vcontainer-common.sh | ||
| 41 | } | ||
| 42 | |||
| 43 | BBCLASSEXTEND = "native" | ||
