Ctrl K
ComunidadGuías…

scripts/firecracker-menual/boot-microvm.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash

set -euo pipefail

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH= cd -- "${script_dir}/../.." && pwd -P)

firecracker_bin='firecracker'
config_path="${repo_dir}/firecracker-config.json"
console_log_path="${repo_dir}/firecracker-console.log"
api_socket='/tmp/firecracker.socket'
vcpu_count='1'
mem_size_mib='512'
rootfs_read_only='false'
default_net_dev_name='tap0'
net_dev_name=''
net_iface_id='eth0'
net_guest_mac='06:00:AC:10:14:02'

info() {
  printf '[INFO] %s\n' "$1"
}

warn() {
  printf '[WARN] %s\n' "$1" >&2
}

fail() {
  printf '[FAIL] %s\n' "$1" >&2
  if [ -n "${console_log_path:-}" ]; then
    printf '[FAIL] %s\n' "$1" >>"$console_log_path" || true
  fi
  exit 1
}

usage() {
  cat <<'EOF'
Usage:
  ./scripts/firecracker-menual/boot-microvm.sh <kernel-image> <rootfs-image>
EOF
}

has_command() {
  command -v "$1" >/dev/null 2>&1
}

require_command() {
  if ! has_command "$1"; then
    fail "Required command not found: $1"
  fi
}

is_positive_integer() {
  case "$1" in
    '' | *[!0-9]* | 0)
      return 1
      ;;
    *)
      return 0
      ;;
  esac
}

abs_existing_file() {
  path=$1

  if [ ! -f "$path" ]; then
    fail "File does not exist: $path"
  fi

  dir=$(dirname "$path")
  file=$(basename "$path")
  printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$file"
}

prepare_output_path() {
  path=$1
  dir=$(dirname "$path")
  file=$(basename "$path")

  mkdir -p "$dir"
  printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$file"
}

resolve_executable() {
  executable=$1

  case "$executable" in
    */*)
      if [ ! -x "$executable" ]; then
        fail "Firecracker executable is not runnable: $executable"
      fi
      abs_existing_file "$executable"
      ;;
    *)
      command -v "$executable" || fail "Firecracker executable not found: $executable"
      ;;
  esac
}

json_escape() {
  value=$1
  value=${value//\\/\\\\}
  value=${value//\"/\\\"}
  value=${value//$'\n'/\\n}
  value=${value//$'\r'/\\r}
  value=${value//$'\t'/\\t}
  printf '%s' "$value"
}

default_boot_args() {
  args='console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw'

  case "$(uname -m 2>/dev/null || printf 'unknown')" in
    aarch64 | arm64)
      args="keep_bootcon ${args}"
      ;;
  esac

  printf '%s\n' "$args"
}

check_kvm_access() {
  # Confirm that the host can expose KVM to Firecracker before booting.
  if [ ! -e /dev/kvm ]; then
    fail '/dev/kvm does not exist. Run the KVM readiness check before booting a microVM.'
  fi

  if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
    fail 'Current user cannot read and write /dev/kvm.'
  fi
}

prepare_api_socket() {
  # Remove only a stale Unix socket; do not delete arbitrary files at that path.
  if [ -S "$api_socket" ]; then
    rm -f "$api_socket"
    return
  fi

  if [ -e "$api_socket" ]; then
    fail "API socket path already exists and is not a socket: ${api_socket}"
  fi
}

check_network_inputs() {
  if [ ! -e "/sys/class/net/${default_net_dev_name}" ]; then
    warn "network: host ${default_net_dev_name} does not exist; booting without a network device"
    warn 'network: create it with: sudo ip tuntap add dev tap0 mode tap user "$(id -un)"'
    warn 'network: then run: sudo ip addr replace 172.16.20.1/24 dev tap0'
    warn 'network: then run: sudo ip link set tap0 up'
    return
  fi

  net_dev_name=$default_net_dev_name
}

write_config() {
  # Write the Firecracker configuration used by --config-file boot.
  escaped_kernel_path=$(json_escape "$kernel_path")
  escaped_rootfs_path=$(json_escape "$rootfs_path")
  escaped_boot_args=$(json_escape "$boot_args")
  escaped_net_iface_id=$(json_escape "$net_iface_id")
  escaped_net_guest_mac=$(json_escape "$net_guest_mac")
  escaped_net_dev_name=$(json_escape "$net_dev_name")
  network_config=''

  if [ -n "$net_dev_name" ]; then
    network_config=$(cat <<EOF_NET
,
  "network-interfaces": [
    {
      "iface_id": "${escaped_net_iface_id}",
      "guest_mac": "${escaped_net_guest_mac}",
      "host_dev_name": "${escaped_net_dev_name}"
    }
  ]
EOF_NET
)
  fi

  cat >"$config_path" <<EOF
{
  "boot-source": {
    "kernel_image_path": "${escaped_kernel_path}",
    "boot_args": "${escaped_boot_args}"
  },
  "drives": [
    {
      "drive_id": "rootfs",
      "path_on_host": "${escaped_rootfs_path}",
      "is_root_device": true,
      "is_read_only": ${rootfs_read_only}
    }
  ]${network_config},
  "machine-config": {
    "vcpu_count": ${vcpu_count},
    "mem_size_mib": ${mem_size_mib},
    "smt": false
  }
}
EOF
}

verify_boot_log() {
  # Confirm that the captured console log reached userspace and did not hit kernel boot errors.
  if [ ! -s "$console_log_path" ]; then
    fail "Console log was not written: ${console_log_path}"
  fi

  if grep -Eiq "Kernel panic|not syncing|Unable to mount root fs|Cannot open root device|No working init found|Attempted to kill init|Run /bin/sh as init process|can't access tty; job control turned off" "$console_log_path"; then
    fail "Console log contains a guest boot error: ${console_log_path}"
  fi

  if ! grep -Eiq 'Welcome to|login:|root@[^[:space:]]+|Started .*Getty|Reached target .*Multi-User|Startup finished in' "$console_log_path"; then
    fail "Console log does not contain a userspace boot marker: ${console_log_path}"
  fi

  info "MicroVM userspace boot log detected: ${console_log_path}"
}

main() {
  if [ "$#" -ne 2 ]; then
    usage
    exit 1
  fi

  # Check local tools and host KVM access before creating the VM configuration.
  require_command basename
  require_command dirname
  require_command grep
  require_command mkdir
  require_command rm
  require_command tee
  require_command uname
  # Resolve outputs first so failures from this run do not leave stale logs.
  config_path=$(prepare_output_path "$config_path")
  console_log_path=$(prepare_output_path "$console_log_path")
  : >"$console_log_path"

  # Resolve all inputs to stable paths for the generated JSON config.
  firecracker_bin=$(resolve_executable "$firecracker_bin")
  kernel_path=$(abs_existing_file "$1")
  rootfs_path=$(abs_existing_file "$2")
  boot_args=$(default_boot_args)

  case "$rootfs_read_only" in
    true | false)
      ;;
    *)
      fail 'rootfs_read_only must be true or false.'
      ;;
  esac

  if ! is_positive_integer "$vcpu_count"; then
    fail 'vcpu_count must be a positive integer.'
  fi

  if ! is_positive_integer "$mem_size_mib"; then
    fail 'mem_size_mib must be a positive integer.'
  fi

  check_kvm_access
  check_network_inputs

  # Create the Firecracker config file.
  prepare_api_socket
  write_config

  info "config: ${config_path}"
  info "console log: ${console_log_path}"
  if [ -n "$net_dev_name" ]; then
    info "network: ${net_dev_name} -> ${net_iface_id} (${net_guest_mac})"
  fi
  info 'starting MicroVM; use the guest console to reboot or stop the VM when done'

  # Boot the MicroVM manually from the generated config and capture console output.
  set +e
  "$firecracker_bin" --api-sock "$api_socket" --config-file "$config_path" 2>&1 | tee "$console_log_path"
  firecracker_status=${PIPESTATUS[0]}
  set -e

  if [ "$firecracker_status" -ne 0 ]; then
    fail "Firecracker exited with status ${firecracker_status}."
  fi

  # Check the captured boot output after Firecracker exits.
  verify_boot_log
  info 'MicroVM boot completed successfully.'
}

main "$@"