diff options
Diffstat (limited to 'recipes-containers/vcontainer')
| -rwxr-xr-x | recipes-containers/vcontainer/files/vdkr.sh | 26 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vpdmn.sh | 26 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 7 | ||||
| -rw-r--r-- | recipes-containers/vcontainer/files/vxn-oci-runtime | 148 |
4 files changed, 167 insertions, 40 deletions
diff --git a/recipes-containers/vcontainer/files/vdkr.sh b/recipes-containers/vcontainer/files/vdkr.sh index 818c831e..29f08877 100755 --- a/recipes-containers/vcontainer/files/vdkr.sh +++ b/recipes-containers/vcontainer/files/vdkr.sh | |||
| @@ -21,5 +21,27 @@ VCONTAINER_STATE_FILE="docker-state.img" | |||
| 21 | VCONTAINER_OTHER_PREFIX="VPDMN" | 21 | VCONTAINER_OTHER_PREFIX="VPDMN" |
| 22 | VCONTAINER_VERSION="3.4.0" | 22 | VCONTAINER_VERSION="3.4.0" |
| 23 | 23 | ||
| 24 | # Source common implementation | 24 | # Auto-detect Xen if not explicitly set |
| 25 | source "$(dirname "${BASH_SOURCE[0]}")/vcontainer-common.sh" "$@" | 25 | if [ -z "${VCONTAINER_HYPERVISOR:-}" ]; then |
| 26 | if command -v xl >/dev/null 2>&1; then | ||
| 27 | export VCONTAINER_HYPERVISOR="xen" | ||
| 28 | fi | ||
| 29 | fi | ||
| 30 | |||
| 31 | # Fall back to vxn blob dir on Dom0 | ||
| 32 | if [ -z "${VDKR_BLOB_DIR:-}" ] && [ -d "/usr/share/vxn" ]; then | ||
| 33 | export VDKR_BLOB_DIR="/usr/share/vxn" | ||
| 34 | fi | ||
| 35 | |||
| 36 | # Two-phase lib lookup: script dir (dev), then /usr/lib/vxn (target) | ||
| 37 | SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | ||
| 38 | if [ -f "${SCRIPT_DIR}/vcontainer-common.sh" ]; then | ||
| 39 | export VCONTAINER_LIBDIR="${SCRIPT_DIR}" | ||
| 40 | source "${SCRIPT_DIR}/vcontainer-common.sh" "$@" | ||
| 41 | elif [ -f "/usr/lib/vxn/vcontainer-common.sh" ]; then | ||
| 42 | export VCONTAINER_LIBDIR="/usr/lib/vxn" | ||
| 43 | source "/usr/lib/vxn/vcontainer-common.sh" "$@" | ||
| 44 | else | ||
| 45 | echo "Error: vcontainer-common.sh not found" >&2 | ||
| 46 | exit 1 | ||
| 47 | fi | ||
diff --git a/recipes-containers/vcontainer/files/vpdmn.sh b/recipes-containers/vcontainer/files/vpdmn.sh index 30775d35..6f0f56d8 100755 --- a/recipes-containers/vcontainer/files/vpdmn.sh +++ b/recipes-containers/vcontainer/files/vpdmn.sh | |||
| @@ -21,5 +21,27 @@ VCONTAINER_STATE_FILE="podman-state.img" | |||
| 21 | VCONTAINER_OTHER_PREFIX="VDKR" | 21 | VCONTAINER_OTHER_PREFIX="VDKR" |
| 22 | VCONTAINER_VERSION="1.2.0" | 22 | VCONTAINER_VERSION="1.2.0" |
| 23 | 23 | ||
| 24 | # Source common implementation | 24 | # Auto-detect Xen if not explicitly set |
| 25 | source "$(dirname "${BASH_SOURCE[0]}")/vcontainer-common.sh" "$@" | 25 | if [ -z "${VCONTAINER_HYPERVISOR:-}" ]; then |
| 26 | if command -v xl >/dev/null 2>&1; then | ||
| 27 | export VCONTAINER_HYPERVISOR="xen" | ||
| 28 | fi | ||
| 29 | fi | ||
| 30 | |||
| 31 | # Fall back to vxn blob dir on Dom0 | ||
| 32 | if [ -z "${VPDMN_BLOB_DIR:-}" ] && [ -d "/usr/share/vxn" ]; then | ||
| 33 | export VPDMN_BLOB_DIR="/usr/share/vxn" | ||
| 34 | fi | ||
| 35 | |||
| 36 | # Two-phase lib lookup: script dir (dev), then /usr/lib/vxn (target) | ||
| 37 | SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | ||
| 38 | if [ -f "${SCRIPT_DIR}/vcontainer-common.sh" ]; then | ||
| 39 | export VCONTAINER_LIBDIR="${SCRIPT_DIR}" | ||
| 40 | source "${SCRIPT_DIR}/vcontainer-common.sh" "$@" | ||
| 41 | elif [ -f "/usr/lib/vxn/vcontainer-common.sh" ]; then | ||
| 42 | export VCONTAINER_LIBDIR="/usr/lib/vxn" | ||
| 43 | source "/usr/lib/vxn/vcontainer-common.sh" "$@" | ||
| 44 | else | ||
| 45 | echo "Error: vcontainer-common.sh not found" >&2 | ||
| 46 | exit 1 | ||
| 47 | fi | ||
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh index aaaaeb61..3f0b3448 100755 --- a/recipes-containers/vcontainer/files/vrunner.sh +++ b/recipes-containers/vcontainer/files/vrunner.sh | |||
| @@ -515,6 +515,13 @@ if [ ! -f "$HV_BACKEND" ]; then | |||
| 515 | fi | 515 | fi |
| 516 | source "$HV_BACKEND" | 516 | source "$HV_BACKEND" |
| 517 | 517 | ||
| 518 | # Xen backend uses vxn-init.sh which is a unified init (no Docker/Podman | ||
| 519 | # daemon in guest). It always parses docker_* kernel parameters regardless | ||
| 520 | # of which frontend (vdkr/vpdmn) invoked us. | ||
| 521 | if [ "$VCONTAINER_HYPERVISOR" = "xen" ]; then | ||
| 522 | CMDLINE_PREFIX="docker" | ||
| 523 | fi | ||
| 524 | |||
| 518 | # Daemon mode handling | 525 | # Daemon mode handling |
| 519 | # Set default socket directory based on architecture | 526 | # Set default socket directory based on architecture |
| 520 | # If --state-dir was provided, use it for daemon files too | 527 | # If --state-dir was provided, use it for daemon files too |
diff --git a/recipes-containers/vcontainer/files/vxn-oci-runtime b/recipes-containers/vcontainer/files/vxn-oci-runtime index 57144aea..02b94fb5 100644 --- a/recipes-containers/vcontainer/files/vxn-oci-runtime +++ b/recipes-containers/vcontainer/files/vxn-oci-runtime | |||
| @@ -37,18 +37,32 @@ BLOB_DIR="/usr/share/vxn" | |||
| 37 | LOG_FILE="/var/log/vxn-oci-runtime.log" | 37 | LOG_FILE="/var/log/vxn-oci-runtime.log" |
| 38 | VXN_LOG="/var/log/vxn-oci-runtime.log" | 38 | VXN_LOG="/var/log/vxn-oci-runtime.log" |
| 39 | 39 | ||
| 40 | # Write a JSON log entry to the shim's --log file (runc-compatible format). | ||
| 41 | # containerd-shim-runc-v2 parses this to extract error messages on failure. | ||
| 42 | _log_json() { | ||
| 43 | local level="$1" msg="$2" dest="$3" | ||
| 44 | local ts | ||
| 45 | ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo "1970-01-01T00:00:00Z") | ||
| 46 | printf '{"level":"%s","msg":"%s","time":"%s"}\n' "$level" "$msg" "$ts" >> "$dest" 2>/dev/null || true | ||
| 47 | } | ||
| 48 | |||
| 40 | log() { | 49 | log() { |
| 41 | local ts | 50 | local ts |
| 42 | ts=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "-") | 51 | ts=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "-") |
| 43 | # Always write to our own log (shim overrides LOG_FILE via --log) | 52 | # Always write plain text to our own log for human debugging |
| 44 | echo "[$ts] $*" >> "$VXN_LOG" 2>/dev/null || true | 53 | echo "[$ts] $*" >> "$VXN_LOG" 2>/dev/null || true |
| 54 | # Write JSON to shim's log file (if different from our log) | ||
| 45 | if [ "$LOG_FILE" != "$VXN_LOG" ]; then | 55 | if [ "$LOG_FILE" != "$VXN_LOG" ]; then |
| 46 | echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null || true | 56 | _log_json "info" "$*" "$LOG_FILE" |
| 47 | fi | 57 | fi |
| 48 | } | 58 | } |
| 49 | 59 | ||
| 50 | die() { | 60 | die() { |
| 51 | log "FATAL: $*" | 61 | log "FATAL: $*" |
| 62 | # Write JSON error to shim log so Docker can extract the message | ||
| 63 | if [ "$LOG_FILE" != "$VXN_LOG" ]; then | ||
| 64 | _log_json "error" "$*" "$LOG_FILE" | ||
| 65 | fi | ||
| 52 | echo "vxn-oci-runtime: $*" >&2 | 66 | echo "vxn-oci-runtime: $*" >&2 |
| 53 | exit 1 | 67 | exit 1 |
| 54 | } | 68 | } |
| @@ -200,10 +214,26 @@ cmd_create() { | |||
| 200 | 214 | ||
| 201 | log " entrypoint='$entrypoint' cwd='$cwd' terminal=$terminal" | 215 | log " entrypoint='$entrypoint' cwd='$cwd' terminal=$terminal" |
| 202 | 216 | ||
| 203 | # Create ext4 disk image from bundle/rootfs/ | 217 | # Read rootfs path from config.json (OCI spec: root.path) |
| 204 | local rootfs_dir="$bundle/rootfs" | 218 | local rootfs_path="" |
| 219 | if command -v jq >/dev/null 2>&1; then | ||
| 220 | rootfs_path=$(jq -r '.root.path // "rootfs"' "$config" 2>/dev/null) | ||
| 221 | else | ||
| 222 | rootfs_path=$(grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' "$config" 2>/dev/null | \ | ||
| 223 | head -1 | sed 's/.*"path"[[:space:]]*:[[:space:]]*"//;s/"$//') | ||
| 224 | [ -z "$rootfs_path" ] && rootfs_path="rootfs" | ||
| 225 | fi | ||
| 226 | # Resolve relative paths against bundle directory | ||
| 227 | case "$rootfs_path" in | ||
| 228 | /*) ;; | ||
| 229 | *) rootfs_path="$bundle/$rootfs_path" ;; | ||
| 230 | esac | ||
| 231 | |||
| 232 | local rootfs_dir="$rootfs_path" | ||
| 205 | local input_img="$dir/input.img" | 233 | local input_img="$dir/input.img" |
| 206 | 234 | ||
| 235 | log " rootfs_dir=$rootfs_dir" | ||
| 236 | |||
| 207 | if [ -d "$rootfs_dir" ] && [ -n "$(ls -A "$rootfs_dir" 2>/dev/null)" ]; then | 237 | if [ -d "$rootfs_dir" ] && [ -n "$(ls -A "$rootfs_dir" 2>/dev/null)" ]; then |
| 208 | # Calculate size: rootfs size + 50% headroom, minimum 64MB | 238 | # Calculate size: rootfs size + 50% headroom, minimum 64MB |
| 209 | local rootfs_size_kb | 239 | local rootfs_size_kb |
| @@ -213,8 +243,14 @@ cmd_create() { | |||
| 213 | 243 | ||
| 214 | log " Creating ext4 image: ${img_size_kb}KB from $rootfs_dir" | 244 | log " Creating ext4 image: ${img_size_kb}KB from $rootfs_dir" |
| 215 | mke2fs -t ext4 -d "$rootfs_dir" -b 4096 "$input_img" "${img_size_kb}K" \ | 245 | mke2fs -t ext4 -d "$rootfs_dir" -b 4096 "$input_img" "${img_size_kb}K" \ |
| 216 | >> "$LOG_FILE" 2>&1 || die "create: failed to create ext4 image" | 246 | >> "$VXN_LOG" 2>&1 || die "create: failed to create ext4 image" |
| 217 | else | 247 | else |
| 248 | # Diagnostics: log what we actually see | ||
| 249 | log " DIAG: bundle contents: $(ls -la "$bundle/" 2>&1)" | ||
| 250 | log " DIAG: rootfs_dir exists=$([ -d "$rootfs_dir" ] && echo yes || echo no)" | ||
| 251 | log " DIAG: rootfs_dir contents: $(ls -la "$rootfs_dir" 2>&1)" | ||
| 252 | log " DIAG: mounts at bundle: $(mount 2>/dev/null | grep "$(dirname "$bundle")" || echo none)" | ||
| 253 | log " DIAG: config.json root: $(grep -o '"root"[^}]*}' "$config" 2>/dev/null)" | ||
| 218 | die "create: $rootfs_dir is empty or does not exist" | 254 | die "create: $rootfs_dir is empty or does not exist" |
| 219 | fi | 255 | fi |
| 220 | 256 | ||
| @@ -271,7 +307,7 @@ XENEOF | |||
| 271 | log " Xen config written to $config_cfg" | 307 | log " Xen config written to $config_cfg" |
| 272 | 308 | ||
| 273 | # Create domain in paused state (OCI spec: create does not start) | 309 | # Create domain in paused state (OCI spec: create does not start) |
| 274 | xl create -p "$config_cfg" >> "$LOG_FILE" 2>&1 || die "create: xl create -p failed" | 310 | xl create -p "$config_cfg" >> "$VXN_LOG" 2>&1 || die "create: xl create -p failed" |
| 275 | 311 | ||
| 276 | log " Domain $domname created (paused)" | 312 | log " Domain $domname created (paused)" |
| 277 | 313 | ||
| @@ -327,33 +363,54 @@ DBGEOF | |||
| 327 | 363 | ||
| 328 | # Monitor process: tracks domain lifecycle and captures output. | 364 | # Monitor process: tracks domain lifecycle and captures output. |
| 329 | # | 365 | # |
| 330 | # Non-terminal mode: xl console captures the domain's serial output. | 366 | # The shim monitors the PID written to --pid-file. The monitor MUST stay |
| 331 | # When the domain dies, xl console exits (PTY closes). We immediately | 367 | # alive through the full create→start→run→exit lifecycle. If the monitor |
| 332 | # extract content between OUTPUT_START/END markers and write to stdout. | 368 | # dies before start is called, the shim skips start and goes to cleanup. |
| 333 | # stdout is the shim's pipe → containerd copies to client FIFO → ctr. | 369 | # |
| 370 | # Non-terminal mode: we poll xl list to wait for the domain to be | ||
| 371 | # unpaused and to run to completion. Once the domain dies, we attach | ||
| 372 | # xl console to read the console ring buffer, extract OUTPUT_START/END | ||
| 373 | # markers, and relay the output to stdout (the shim's pipe). | ||
| 334 | # | 374 | # |
| 335 | # CRITICAL: We use "wait" on xl console instead of polling xl list. | 375 | # IMPORTANT: We cannot run xl console on a paused domain — it exits |
| 336 | # Polling with sleep 5 was too slow — the shim detected "stopped" and | 376 | # immediately with no output. Instead we wait for the domain to finish, |
| 337 | # killed the monitor before it had a chance to output. Using wait gives | 377 | # then read the console ring buffer post-mortem via xl console -r (dmesg). |
| 338 | # us instant reaction when the domain dies. | 378 | # However, xl console on a destroyed domain also fails. So we use a |
| 379 | # two-phase approach: poll for domain to start running, then attach | ||
| 380 | # xl console which will block until the domain dies. | ||
| 339 | # | 381 | # |
| 340 | # Terminal mode (console-socket): the shim owns the PTY exclusively. | 382 | # Terminal mode (console-socket): the shim owns the PTY exclusively. |
| 341 | # We just wait for the domain to exit without capturing console. | 383 | # We just wait for the domain to exit without capturing console. |
| 342 | local _dn="$domname" _logdir="$logdir" _csock="$console_socket" | 384 | local _dn="$domname" _logdir="$logdir" _csock="$console_socket" |
| 343 | ( | 385 | ( |
| 344 | if [ -z "$_csock" ]; then | 386 | if [ -z "$_csock" ]; then |
| 345 | # Non-terminal: capture console to persistent log dir | 387 | # Non-terminal: stay alive until domain finishes, then capture output. |
| 346 | xl console "$_dn" > "$_logdir/console.log" 2>&1 & | 388 | # |
| 347 | _cpid=$! | 389 | # Phase 1: Wait for domain to exist and be unpaused (start called). |
| 348 | 390 | # The domain is created paused — xl console would exit immediately. | |
| 349 | # Wait for xl console to exit — domain death closes the PTY, | 391 | # Poll until it transitions from 'p' (paused) to running, or dies. |
| 350 | # which causes xl console to exit immediately. No polling delay. | 392 | while xl list "$_dn" >/dev/null 2>&1; do |
| 351 | wait $_cpid 2>/dev/null | 393 | # Check if domain is still paused |
| 352 | 394 | local _state | |
| 353 | # Extract output between markers and write to stdout. | 395 | _state=$(xl list "$_dn" 2>/dev/null | awk -v dn="$_dn" '$1 == dn {print $5}') |
| 354 | # stdout IS the shim's pipe (confirmed: fd1=pipe). The shim's | 396 | # States: r=running, b=blocked, p=paused, s=shutdown, c=crashed, d=dying |
| 355 | # io.Copy goroutine reads from this pipe and writes to the | 397 | case "$_state" in |
| 356 | # containerd client FIFO. ctr reads from the FIFO. | 398 | p) sleep 0.2; continue ;; # Still paused, keep waiting |
| 399 | *) break ;; # Running/blocked/other — proceed | ||
| 400 | esac | ||
| 401 | done | ||
| 402 | |||
| 403 | # Phase 2: Domain is running (or already dead). Attach xl console | ||
| 404 | # to capture serial output. xl console blocks until PTY closes | ||
| 405 | # (domain death), then exits. | ||
| 406 | if xl list "$_dn" >/dev/null 2>&1; then | ||
| 407 | xl console "$_dn" > "$_logdir/console.log" 2>&1 || true | ||
| 408 | fi | ||
| 409 | |||
| 410 | # Phase 3: Extract output between markers and write to stdout. | ||
| 411 | # stdout IS the shim's pipe (fd1=pipe). The shim's io.Copy | ||
| 412 | # goroutine reads from this pipe and writes to the containerd | ||
| 413 | # client FIFO. ctr reads from the FIFO. | ||
| 357 | if [ -f "$_logdir/console.log" ]; then | 414 | if [ -f "$_logdir/console.log" ]; then |
| 358 | _relay=false | 415 | _relay=false |
| 359 | while IFS= read -r _line; do | 416 | while IFS= read -r _line; do |
| @@ -409,7 +466,7 @@ cmd_start() { | |||
| 409 | xl list "$domname" >/dev/null 2>&1 || die "start: domain $domname not found" | 466 | xl list "$domname" >/dev/null 2>&1 || die "start: domain $domname not found" |
| 410 | 467 | ||
| 411 | # Unpause the domain | 468 | # Unpause the domain |
| 412 | xl unpause "$domname" >> "$LOG_FILE" 2>&1 || die "start: xl unpause failed" | 469 | xl unpause "$domname" >> "$VXN_LOG" 2>&1 || die "start: xl unpause failed" |
| 413 | 470 | ||
| 414 | # Update state | 471 | # Update state |
| 415 | local pid bundle created | 472 | local pid bundle created |
| @@ -462,11 +519,30 @@ EOF | |||
| 462 | } | 519 | } |
| 463 | 520 | ||
| 464 | cmd_kill() { | 521 | cmd_kill() { |
| 465 | local container_id="$1" | 522 | local container_id="" |
| 466 | local signal="${2:-SIGTERM}" | 523 | local signal="SIGTERM" |
| 524 | local kill_all=false | ||
| 525 | |||
| 526 | # Parse arguments: runc accepts `kill [flags] <container-id> [signal]` | ||
| 527 | # Docker sends: kill --all <container-id> <signal> | ||
| 528 | while [ $# -gt 0 ]; do | ||
| 529 | case "$1" in | ||
| 530 | --all|-a) kill_all=true; shift ;; | ||
| 531 | -*) shift ;; # skip unknown flags | ||
| 532 | *) | ||
| 533 | if [ -z "$container_id" ]; then | ||
| 534 | container_id="$1" | ||
| 535 | else | ||
| 536 | signal="$1" | ||
| 537 | fi | ||
| 538 | shift | ||
| 539 | ;; | ||
| 540 | esac | ||
| 541 | done | ||
| 542 | |||
| 467 | [ -n "$container_id" ] || die "kill: container ID required" | 543 | [ -n "$container_id" ] || die "kill: container ID required" |
| 468 | 544 | ||
| 469 | log "KILL: id=$container_id signal=$signal" | 545 | log "KILL: id=$container_id signal=$signal all=$kill_all" |
| 470 | load_state "$container_id" | 546 | load_state "$container_id" |
| 471 | 547 | ||
| 472 | local dir | 548 | local dir |
| @@ -477,24 +553,24 @@ cmd_kill() { | |||
| 477 | # Normalize signal: accept both numeric and symbolic forms | 553 | # Normalize signal: accept both numeric and symbolic forms |
| 478 | case "$signal" in | 554 | case "$signal" in |
| 479 | 9|SIGKILL|KILL) | 555 | 9|SIGKILL|KILL) |
| 480 | xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true | 556 | xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true |
| 481 | ;; | 557 | ;; |
| 482 | 2|SIGINT|INT) | 558 | 2|SIGINT|INT) |
| 483 | xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true | 559 | xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true |
| 484 | ;; | 560 | ;; |
| 485 | 15|SIGTERM|TERM|"") | 561 | 15|SIGTERM|TERM|"") |
| 486 | xl shutdown "$domname" >> "$LOG_FILE" 2>&1 || true | 562 | xl shutdown "$domname" >> "$VXN_LOG" 2>&1 || true |
| 487 | # Wait briefly for graceful shutdown, then force destroy | 563 | # Wait briefly for graceful shutdown, then force destroy |
| 488 | local i | 564 | local i |
| 489 | for i in 1 2 3 4 5 6 7 8 9 10; do | 565 | for i in 1 2 3 4 5 6 7 8 9 10; do |
| 490 | xl list "$domname" >/dev/null 2>&1 || break | 566 | xl list "$domname" >/dev/null 2>&1 || break |
| 491 | sleep 1 | 567 | sleep 1 |
| 492 | done | 568 | done |
| 493 | xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true | 569 | xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true |
| 494 | ;; | 570 | ;; |
| 495 | *) | 571 | *) |
| 496 | # Unknown signal — treat as SIGTERM | 572 | # Unknown signal — treat as SIGTERM |
| 497 | xl shutdown "$domname" >> "$LOG_FILE" 2>&1 || true | 573 | xl shutdown "$domname" >> "$VXN_LOG" 2>&1 || true |
| 498 | ;; | 574 | ;; |
| 499 | esac | 575 | esac |
| 500 | 576 | ||
| @@ -542,7 +618,7 @@ cmd_delete() { | |||
| 542 | local domname | 618 | local domname |
| 543 | domname=$(cat "$dir/domname") | 619 | domname=$(cat "$dir/domname") |
| 544 | if xl list "$domname" >/dev/null 2>&1; then | 620 | if xl list "$domname" >/dev/null 2>&1; then |
| 545 | xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true | 621 | xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true |
| 546 | fi | 622 | fi |
| 547 | fi | 623 | fi |
| 548 | 624 | ||
