Ctrl K
ComunidadGuías…

scripts/firecracker-menual/install-alpine-rootfs.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)
script_path="${script_dir}/$(basename -- "$0")"

alpine_releases_base='https://dl-cdn.alpinelinux.org/alpine'
alpine_series=${M2IMAGE_DISTRO_SERIES:-3.24}
alpine_version_setting=${M2IMAGE_DISTRO_VERSION:-3.24.1}
artifact_dir="${repo_dir}/images/rootfs"
kernel_artifact_dir="${repo_dir}/images/kernel"
kernel_image_name=''
initrd_image_name=''
extract_vmlinux="${script_dir}/extract-vmlinux"
extract_arm64_image="${script_dir}/extract-arm64-image"
build_dir="${repo_dir}/build/alpine-rootfs"
rootfs_size='512M'
rootfs_hostname='firecrab'
m2image_alias=${M2IMAGE_ALIAS:-}
sbom_output=${M2IMAGE_SBOM_OUTPUT:-}
sbom_generator="${repo_dir}/scripts/m2image_sbom.py"

# Like the Ubuntu builder, this script expands the official base archive into
# a staging directory, installs packages with the distribution's own package
# manager in a temporary chroot, then creates ext4 directly with mkfs.ext4 -d.
# No Docker daemon or OCI runtime is involved.
# linux-virt: Alpine's own officially-maintained cloud/virt kernel package
# (public-docs/guides/images.md) — replaces the self-built vanilla kernel
# every template used to share. Unlike Ubuntu's linux-image-generic,
# virtio_blk/ext4 are modules here rather than builtin, so the initramfs-virt
# Alpine builds alongside it (mkinitfs) has to ship as the VM's initrd too —
# without it the kernel can never reach /dev/vda to mount the real root.
# bash: Shell repository scripts often use #!/bin/bash (same as Ubuntu).
# spdx-licenses-text: Alpine's normal runtime packages intentionally omit most
# license files. Keep the distribution-packaged SPDX License List text corpus
# in the image so release packaging can recover a complete canonical license
# bundle without fetching mutable web content or inventing license text.
rootfs_packages='alpine-baselayout busybox bash openrc agetty iproute2-minimal iputils-ping dhcpcd openssh-server ca-certificates curl procps linux-virt spdx-licenses-text'

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

fail() {
  printf '[FAIL] %s\n' "$1" >&2
  exit 1
}

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

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

abs_dir() {
  path=$1

  mkdir -p "$path"
  cd "$path" && pwd -P
}

detect_alpine_arch() {
  case "${M2IMAGE_ARCH:-$(uname -m 2>/dev/null || printf 'unknown')}" in
    x86_64 | amd64)
      printf '%s\n' 'x86_64'
      ;;
    aarch64 | arm64)
      printf '%s\n' 'aarch64'
      ;;
    *)
      fail 'Unsupported architecture. Alpine rootfs creation supports x86_64 and aarch64.'
      ;;
  esac
}

verify_native_architecture() {
  local host_arch=''

  case "$(uname -m 2>/dev/null || printf unknown)" in
    x86_64 | amd64) host_arch='x86_64' ;;
    aarch64 | arm64) host_arch='aarch64' ;;
    *) fail 'Unsupported build host architecture.' ;;
  esac
  [ "$host_arch" = "$alpine_arch" ] \
    || fail "Host-native Alpine chroot requires a ${alpine_arch} host (current: ${host_arch})."
}

resolve_ssh_public_key() {
  local key_source=${FIRECRAB_SSH_PUBLIC_KEY:-}
  local key_home=''

  if [ -n "$key_source" ]; then
    [ -s "$key_source" ] || fail "FIRECRAB_SSH_PUBLIC_KEY is not a readable public key: ${key_source}"
    printf '%s\n' "$key_source"
    return
  fi

  if [ -n "${SUDO_UID:-}" ] && has_command getent; then
    key_home=$(getent passwd "$SUDO_UID" | cut -d: -f6 || true)
  elif [ -n "${HOME:-}" ]; then
    key_home=$HOME
  fi

  if [ -n "$key_home" ]; then
    for key_source in \
      "$key_home/.ssh/id_ed25519.pub" \
      "$key_home/.ssh/id_ecdsa.pub" \
      "$key_home/.ssh/id_rsa.pub"; do
      if [ -s "$key_source" ]; then
        printf '%s\n' "$key_source"
        return
      fi
    done
  fi

  # SSH is optional: every Firecrab guest has an autologin serial console.
  # Keep an empty source so the configure path stays deterministic without
  # modifying the operator's ~/.ssh directory behind their back.
  key_source="${build_dir}/no-authorized-key.pub"
  : >"$key_source"
  info 'no host SSH public key found; building with serial-console access only' >&2
  printf '%s\n' "$key_source"
}

# Resolve the exact manifest-pinned minirootfs. A branch's
# latest-releases.yaml changes whenever Alpine publishes a patch release, so
# using it would make an alias such as alpine-3.24.1 silently change contents
# while package paths and runtime specs still expect 3.24.1.
resolve_alpine_minirootfs() {
  local branch="v${alpine_series}"
  local archive_name="alpine-minirootfs-${alpine_version_setting}-${alpine_arch}.tar.gz"
  local checksum_url="${alpine_releases_base}/${branch}/releases/${alpine_arch}/${archive_name}.sha256"
  local checksum_file="${build_dir}/${archive_name}.sha256"
  local checksum=''

  if ! curl -fsSL "$checksum_url" -o "${checksum_file}.tmp"; then
    fail "Could not download Alpine checksum: ${checksum_url}"
  fi
  mv "${checksum_file}.tmp" "$checksum_file"
  checksum=$(awk -v file="$archive_name" '$2 == file || $2 == "*" file { print $1; exit }' "$checksum_file")
  [ -n "$checksum" ] || fail "Could not find ${archive_name} in ${checksum_url}"
  printf '%s %s %s %s\n' "$branch" "$alpine_version_setting" "$archive_name" "$checksum"
}

write_configure_script() {
  # Runs as root on the host: extracts the verified minirootfs archive, uses
  # its own apk inside a temporary chroot, configures the guest, and writes a
  # direct ext4 image. All paths are explicit and remain inside the repository.
  cat >"$1" <<'EOF'
#!/bin/sh
set -eu

staging=$1
archive_path=$2
ssh_public_key=$3
kernel_out=$4
out=$5
alpine_branch=$6
alpine_version=$7
alpine_arch=$8
hostname=$9
rootfs_size=${10}
rootfs_packages=${11}
initrd_image_name=${12}

chroot_mounts=''

cleanup_chroot_mounts() {
  target=''
  for target in $chroot_mounts; do
    umount -R "$target" 2>/dev/null || umount -l "$target" 2>/dev/null || true
  done
  chroot_mounts=''
}

mount_chroot_fs() {
  mount -t proc proc "$staging/proc"
  chroot_mounts="$staging/proc $chroot_mounts"
  mount --rbind /sys "$staging/sys"
  mount --make-rslave "$staging/sys"
  chroot_mounts="$staging/sys $chroot_mounts"
  mount --rbind /dev "$staging/dev"
  mount --make-rslave "$staging/dev"
  chroot_mounts="$staging/dev $chroot_mounts"
}

trap cleanup_chroot_mounts EXIT

rm -rf "$staging"
mkdir -p "$staging"
tar --numeric-owner -xzf "$archive_path" -C "$staging"

cat >"${staging}/etc/apk/repositories" <<REPOS
https://dl-cdn.alpinelinux.org/alpine/${alpine_branch}/main
https://dl-cdn.alpinelinux.org/alpine/${alpine_branch}/community
REPOS

# Use the downloaded distribution's own apk rather than requiring apk on the
# host. The temporary resolver is replaced with the guest resolver below.
cp /etc/resolv.conf "${staging}/etc/resolv.conf"
mount_chroot_fs
# shellcheck disable=SC2086
chroot "$staging" /sbin/apk add --no-cache --update-cache $rootfs_packages
cleanup_chroot_mounts

cat >"${staging}/etc/hostname" <<EOF_HOSTNAME
${hostname}
EOF_HOSTNAME

cat >"${staging}/etc/hosts" <<EOF_HOSTS
127.0.0.1 localhost
127.0.1.1 ${hostname}
EOF_HOSTS

cat >"${staging}/etc/fstab" <<'EOF_FSTAB'
/dev/vda / ext4 defaults 0 1
EOF_FSTAB

# firecrab-helper's dnsmasq answers DNS on the bridge gateway itself
# (172.30.0.1) for every guest on the VPC subnet — dhcpcd overwrites this
# from the DHCP-provided options once it runs, so this is really just the
# pre-DHCP fallback value.
cat >"${staging}/etc/resolv.conf" <<'EOF_RESOLV'
nameserver 172.30.0.1
EOF_RESOLV

install -d -m 0755 "${staging}/etc/network"
cat >"${staging}/etc/network/interfaces" <<'EOF_IFACES'
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
EOF_IFACES

# Prints a fixed sentinel line to /dev/console (Firecracker's captured
# stdout) once DHCP + DNS are confirmed working — the signal firecrab-api's
# start pipeline waits on in place of a guest agent event
# (public-docs/guides/networking.md; guest agent/vsock is out of this
# project's competition scope).
cat >"${staging}/etc/init.d/firecrab-network-ready" <<'EOF_SENTINEL'
#!/sbin/openrc-run

description="Firecrab network readiness sentinel"

depend() {
    need net
    after dhcpcd
}

start() {
    # `after dhcpcd` only orders service *starts* — dhcpcd's own start
    # action returns as soon as it forks into the background, well before
    # it has actually completed a DHCP transaction, so checking eth0 right
    # away here routinely runs before the lease exists yet. Poll briefly
    # instead of trusting the ordering dependency to mean "has an address".
    ipv4=""
    for _ in $(seq 1 10); do
        ipv4=$(ip -4 -o addr show eth0 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
        [ -n "$ipv4" ] && break
        sleep 1
    done
    if [ -z "$ipv4" ]; then
        echo "FIRECRAB_NETWORK_FAILED no-ipv4-address" >/dev/console
    elif getent hosts example.com >/dev/null 2>&1; then
        echo "FIRECRAB_NETWORK_READY $ipv4" >/dev/console
    else
        echo "FIRECRAB_NETWORK_FAILED dns-unreachable" >/dev/console
    fi
}
EOF_SENTINEL
chmod 0755 "${staging}/etc/init.d/firecrab-network-ready"

# Serial console getty with autologin, mirroring the Ubuntu agetty setup.
grep -v '^ttyS0::' "${staging}/etc/inittab" >"${staging}/etc/inittab.new"
printf 'ttyS0::respawn:/sbin/agetty --autologin root --noclear --keep-baud 115200,57600,38400,9600 ttyS0 vt100\n' \
  >>"${staging}/etc/inittab.new"
mv "${staging}/etc/inittab.new" "${staging}/etc/inittab"

# Standard OpenRC runlevels for a minimal single-disk VM. hwclock is
# deliberately left out: Firecracker exposes no RTC device, so it only
# fails and drags in modprobe noise for a /lib/modules that doesn't exist
# (this kernel has no loadable module support).
mkdir -p "${staging}/etc/runlevels/sysinit" "${staging}/etc/runlevels/boot" "${staging}/etc/runlevels/default"
for svc in devfs dmesg; do
  ln -sf "/etc/init.d/${svc}" "${staging}/etc/runlevels/sysinit/${svc}"
done
for svc in hostname bootmisc sysctl loopback; do
  ln -sf "/etc/init.d/${svc}" "${staging}/etc/runlevels/boot/${svc}"
done
for svc in local dhcpcd sshd firecrab-network-ready; do
  ln -sf "/etc/init.d/${svc}" "${staging}/etc/runlevels/default/${svc}"
done

if [ -s "$ssh_public_key" ]; then
  install -d -m 0700 "${staging}/root/.ssh"
  install -m 0600 "$ssh_public_key" "${staging}/root/.ssh/authorized_keys"
fi

test -e "${staging}/etc/os-release" || { echo 'missing /etc/os-release' >&2; exit 1; }
test -e "${staging}/bin/sh" || { echo 'missing /bin/sh' >&2; exit 1; }
test -e "${staging}/sbin/init" || { echo 'missing /sbin/init' >&2; exit 1; }
{ test -e "${staging}/sbin/agetty" || test -e "${staging}/usr/sbin/agetty"; } || { echo 'missing agetty' >&2; exit 1; }
test -e "${staging}/sbin/openrc" || { echo 'missing openrc' >&2; exit 1; }
test -e "${staging}/usr/sbin/sshd" || { echo 'missing sshd' >&2; exit 1; }
test -x "${staging}/etc/init.d/firecrab-network-ready" || { echo 'missing firecrab-network-ready init script' >&2; exit 1; }
test -L "${staging}/etc/runlevels/default/firecrab-network-ready" || { echo 'firecrab-network-ready not enabled in default runlevel' >&2; exit 1; }

# linux-virt's boot files land under the staging root, not this container's
# own /boot — pulled out to /kernel-out (mounted from the host) so the host
# side can prepare the architecture-specific Firecracker kernel. x86_64
# needs an uncompressed ELF vmlinux; ARM64 needs the raw Image unwrapped out
# of the EFI zboot wrapper Alpine ships.
test -e "${staging}/boot/vmlinuz-virt" || { echo 'missing boot/vmlinuz-virt (linux-virt)' >&2; exit 1; }
test -e "${staging}/boot/initramfs-virt" || { echo 'missing boot/initramfs-virt (linux-virt)' >&2; exit 1; }
cp "${staging}/boot/vmlinuz-virt" "${kernel_out}/vmlinuz-virt-raw"
cp "${staging}/boot/initramfs-virt" "${kernel_out}/${initrd_image_name}"

rootfs_image="${out}/alpine-rootfs-${alpine_version}-${alpine_arch}.ext4"
tmp_image="${rootfs_image}.tmp"
truncate -s "$rootfs_size" "$tmp_image"
if grep -qw orphan_file /etc/mke2fs.conf 2>/dev/null; then
  mkfs.ext4 -F -O '^orphan_file' -L rootfs -d "$staging" "$tmp_image" >/dev/null
else
  mkfs.ext4 -F -L rootfs -d "$staging" "$tmp_image" >/dev/null
fi
mv "$tmp_image" "$rootfs_image"
ln -sfn "$(basename "$rootfs_image")" "${out}/alpine-rootfs.ext4"

echo "ROOTFS_IMAGE=${rootfs_image}"
EOF
}

restore_output_ownership() {
  if [ -z "${SUDO_UID:-}" ] || [ -z "${SUDO_GID:-}" ]; then
    return
  fi

  chown "${SUDO_UID}:${SUDO_GID}" \
    "${artifact_dir}/alpine-rootfs-${alpine_version}-${alpine_arch}.ext4" \
    "${kernel_artifact_dir}/${kernel_image_name}" \
    "${kernel_artifact_dir}/${initrd_image_name}"
  chown -h "${SUDO_UID}:${SUDO_GID}" "${artifact_dir}/alpine-rootfs.ext4" 2>/dev/null || true
}

# True when the file is a raw Linux/arm64 Image: ARM64_IMAGE_MAGIC ("ARMd")
# at offset 0x38, the field Firecracker's loader checks before it jumps into
# the image. Read, never written — a wrapped kernel (a UKI or an EFI zboot
# image) with the magic stamped on top passes the loader and then dies
# silently in the guest instead of failing here.
is_arm64_image() {
  [ -s "$1" ] && [ "$(od -An -tx1 -j56 -N4 "$1" | tr -d ' \n')" = '41524d64' ]
}

# Prepares the raw vmlinuz-virt copied out to /kernel-out. Firecracker expects
# an uncompressed ELF vmlinux on x86_64 and a raw Linux/arm64 Image on ARM64.
# Alpine's linux-virt arm64 vmlinuz is neither: it is an EFI zboot image, a PE
# wrapper around a gzip-compressed Image, so it has to be unwrapped.
extract_kernel() {
  raw_path="${kernel_artifact_dir}/vmlinuz-virt-raw"
  if [ ! -s "$raw_path" ]; then
    fail "linux-virt's vmlinuz-virt was not copied out to ${raw_path}"
  fi

  kernel_image_path="${kernel_artifact_dir}/${kernel_image_name}"
  kernel_image_tmp="${kernel_image_path}.tmp"
  if [ "$alpine_arch" = aarch64 ]; then
    info "extracting raw ARM64 Image from: ${raw_path}"
    if ! "$extract_arm64_image" "$raw_path" >"$kernel_image_tmp" \
      || ! is_arm64_image "$kernel_image_tmp"; then
      rm -f "$kernel_image_tmp"
      fail "extract-arm64-image could not extract a raw ARM64 Image from ${raw_path}"
    fi
  else
    info "extracting ELF vmlinux from: ${raw_path}"
    if ! "$extract_vmlinux" "$raw_path" >"$kernel_image_tmp"; then
      rm -f "$kernel_image_tmp"
      fail "extract-vmlinux could not extract an ELF vmlinux from ${raw_path}"
    fi
    if ! file "$kernel_image_tmp" | grep -q 'ELF'; then
      rm -f "$kernel_image_tmp"
      fail "extracted kernel is not an ELF image: ${raw_path}"
    fi
  fi
  chmod 0644 "$kernel_image_tmp"
  mv "$kernel_image_tmp" "$kernel_image_path"
  rm -f "$raw_path"
  info "Alpine kernel image: ${kernel_image_path}"

  initrd_image_path="${kernel_artifact_dir}/${initrd_image_name}"
  if [ ! -s "$initrd_image_path" ]; then
    fail "linux-virt's initramfs-virt was not copied out to ${initrd_image_path}"
  fi
  chmod 0644 "$initrd_image_path"
  info "Alpine initrd image: ${initrd_image_path}"
}

main() {
  if [ "$#" -ne 0 ]; then
    fail 'install-alpine-rootfs.sh does not accept arguments.'
  fi

  require_command awk
  require_command cp
  require_command curl
  require_command file
  require_command gzip
  require_command grep
  require_command chroot
  require_command id
  require_command install
  require_command mkdir
  require_command mkfs.ext4
  require_command mount
  require_command mv
  require_command od
  require_command rm
  require_command sha256sum
  require_command tar
  require_command truncate
  require_command umount
  require_command uname
  if [ ! -x "$extract_vmlinux" ]; then
    fail "extract-vmlinux helper not found or not executable: ${extract_vmlinux}"
  fi
  if [ ! -x "$extract_arm64_image" ]; then
    fail "extract-arm64-image helper not found or not executable: ${extract_arm64_image}"
  fi

  if [ "$(id -u)" -ne 0 ]; then
    require_command sudo
    exec sudo env \
      "M2IMAGE_ARCH=${M2IMAGE_ARCH:-}" \
      "M2IMAGE_DISTRO_SERIES=${alpine_series}" \
      "M2IMAGE_DISTRO_VERSION=${alpine_version_setting}" \
      "M2IMAGE_ALIAS=${m2image_alias}" \
      "M2IMAGE_SBOM_OUTPUT=${sbom_output}" \
      "FIRECRAB_SSH_PUBLIC_KEY=${FIRECRAB_SSH_PUBLIC_KEY:-}" \
      "$script_path"
  fi

  build_dir=$(abs_dir "$build_dir")
  artifact_dir=$(abs_dir "$artifact_dir")
  alpine_arch=$(detect_alpine_arch)
  verify_native_architecture
  if [ "$alpine_arch" = aarch64 ]; then
    kernel_image_name='Image-alpine-virt-aarch64'
  else
    kernel_image_name='vmlinux-alpine-virt-x86_64'
  fi
  initrd_image_name="initramfs-alpine-virt-${alpine_arch}"
  ssh_public_key=$(resolve_ssh_public_key)

  info "Alpine architecture: ${alpine_arch}"
  read -r alpine_branch alpine_version archive_name archive_sha256 < <(resolve_alpine_minirootfs)
  if [ -z "$alpine_branch" ] || [ -z "$archive_name" ]; then
    fail "Could not resolve the Alpine minirootfs release for ${alpine_arch}."
  fi
  info "Alpine branch: ${alpine_branch}"
  info "Alpine minirootfs version: ${alpine_version}"

  download_dir="${build_dir}/downloads"
  archive_path="${download_dir}/${archive_name}"
  mkdir -p "$download_dir"

  if [ -f "$archive_path" ]; then
    info "reusing Alpine minirootfs archive: ${archive_path}"
  else
    archive_url="${alpine_releases_base}/${alpine_branch}/releases/${alpine_arch}/${archive_name}"
    info "downloading Alpine minirootfs archive: ${archive_url}"
    if ! curl -fsSL "$archive_url" -o "${archive_path}.tmp"; then
      rm -f "${archive_path}.tmp"
      fail "Could not download Alpine minirootfs archive: ${archive_url}"
    fi
    mv "${archive_path}.tmp" "$archive_path"
  fi

  info 'verifying Alpine minirootfs archive checksum'
  printf '%s  %s\n' "$archive_sha256" "$archive_path" | sha256sum -c -

  configure_script="${build_dir}/configure.sh"
  write_configure_script "$configure_script"

  mount_dir="${build_dir}/mnt"
  rm -rf "$mount_dir"
  mkdir -p "$mount_dir"
  mkdir -p "$kernel_artifact_dir"

  info 'building Alpine rootfs staging + ext4 image via host-native chroot'
  sh "$configure_script" \
    "$mount_dir" "$archive_path" "$ssh_public_key" "$kernel_artifact_dir" "$artifact_dir" \
    "$alpine_branch" "$alpine_version" "$alpine_arch" "$rootfs_hostname" "$rootfs_size" \
    "$rootfs_packages" "$initrd_image_name"

  if [ -n "$sbom_output" ]; then
    [ -n "$m2image_alias" ] || fail 'M2IMAGE_ALIAS is required with M2IMAGE_SBOM_OUTPUT'
    require_command python3
    [ -f "$mount_dir/lib/apk/db/installed" ] \
      || fail 'Alpine package database missing from built staging root'
    python3 "$sbom_generator" \
      --format alpine --distribution alpine \
      --image-alias "$m2image_alias" --image-version "$alpine_version" \
      --architecture "$alpine_arch" \
      --package-db "$mount_dir/lib/apk/db/installed" --output "$sbom_output"
    if [ -n "${SUDO_UID:-}" ] && [ -n "${SUDO_GID:-}" ]; then
      chown "${SUDO_UID}:${SUDO_GID}" "$sbom_output"
    fi
  fi

  extract_kernel
  restore_output_ownership

  rootfs_image="${artifact_dir}/alpine-rootfs-${alpine_version}-${alpine_arch}.ext4"
  rootfs_link="${artifact_dir}/alpine-rootfs.ext4"

  if [ ! -f "$rootfs_image" ]; then
    fail "Alpine rootfs image was not created: ${rootfs_image}"
  fi

  info "Alpine rootfs image created: ${rootfs_image}"
  info "Alpine rootfs symlink: ${rootfs_link} -> $(basename "$rootfs_image")"
}

main "$@"