diff --git a/meta-balena-common/classes/image-balena.bbclass b/meta-balena-common/classes/image-balena.bbclass index 54296dd84f..d41b964e4f 100644 --- a/meta-balena-common/classes/image-balena.bbclass +++ b/meta-balena-common/classes/image-balena.bbclass @@ -459,20 +459,19 @@ python do_image_size_check() { image_size_aligned = int(disk_aligned(d, os.stat(imgfile).st_size / 1024)) available = int(disk_aligned(d, available_space(ext4file, d))) - # Calculate /boot directory size - copied to volume during HUP - boot_dir = os.path.join(d.getVar("IMAGE_ROOTFS"), "boot") - boot_size_kb = get_dir_size_kb(boot_dir) - boot_size_aligned = int(disk_aligned(d, boot_size_kb)) + # mobynit init only is stored on the inactive root slot during HUP + boot_init = os.path.join(d.getVar("IMAGE_ROOTFS"), "boot", "init") + boot_init_size_kb = int(disk_aligned(d, os.path.getsize(boot_init) / 1024 if os.path.isfile(boot_init) else 0)) - # Total space required = docker image + boot volume - total_required = image_size_aligned + boot_size_aligned + # Total space required = docker image + mobynit init on root slot + total_required = image_size_aligned + boot_init_size_kb if total_required > available: - bb.fatal("HUP size check failed: docker image (%d KiB) + /boot volume (%d KiB) = %d KiB exceeds available space %d KiB" - % (image_size_aligned, boot_size_aligned, total_required, available)) + bb.fatal("HUP size check failed: docker image (%d KiB) + mobynit init (%d KiB) = %d KiB exceeds available space %d KiB" + % (image_size_aligned, boot_init_size_kb, total_required, available)) - bb.debug(1, 'HUP size check: docker image %d KiB, /boot volume %d KiB, total %d KiB, available %d KiB' - % (image_size_aligned, boot_size_aligned, total_required, available)) + bb.debug(1, 'HUP size check: docker image %d KiB, mobynit init %d KiB, total %d KiB, available %d KiB' + % (image_size_aligned, boot_init_size_kb, total_required, available)) } # Equivalent to: diff --git a/meta-balena-common/classes/image_types_balena.bbclass b/meta-balena-common/classes/image_types_balena.bbclass index 99729e7bf6..145943d962 100644 --- a/meta-balena-common/classes/image_types_balena.bbclass +++ b/meta-balena-common/classes/image_types_balena.bbclass @@ -1,5 +1,7 @@ inherit image_types +require ${@os.path.join(d.getVar('BALENA_COREBASE'), 'recipes-containers/docker-disk/balena-data-partition.inc')} + # # Create a raw image that can by written onto a storage device using dd/etcher. # @@ -124,6 +126,7 @@ BALENA_BOOT_FINGERPRINT_PATH ?= "${WORKDIR}/${BALENA_FINGERPRINT_FILENAME}.${BAL BALENA_IMAGE_BOOTLOADER ?= "virtual/bootloader" BALENA_RAW_IMG_COMPRESSION ?= "" BALENA_DATA_FS ?= "${DEPLOY_DIR_IMAGE}/${BALENA_DATA_FS_LABEL}.img" +BALENA_DATA_STAGING_TAR ?= "${DEPLOY_DIR_IMAGE}/${BALENA_DATA_STAGING}" BALENA_BOOT_FS = "${WORKDIR}/${BALENA_BOOT_FS_LABEL}.img" BALENA_ROOTB_FS = "${WORKDIR}/${BALENA_ROOTB_FS_LABEL}.img" BALENA_STATE_FS ?= "${WORKDIR}/${BALENA_STATE_FS_LABEL}.img" @@ -180,7 +183,7 @@ IMAGE_CMD:balenaos-img () { # resin-data if [ -n "${BALENA_DATA_FS}" ]; then - BALENA_DATA_SIZE=`du -bks ${BALENA_DATA_FS} | awk '{print $1}'` + BALENA_DATA_SIZE=$(expr ${BALENA_DATA_PARTITION_SIZE_MB} \* 1024) BALENA_DATA_SIZE_ALIGNED=$(expr ${BALENA_DATA_SIZE} \+ ${BALENA_IMAGE_ALIGNMENT} \- 1) BALENA_DATA_SIZE_ALIGNED=$(expr ${BALENA_DATA_SIZE_ALIGNED} \- ${BALENA_DATA_SIZE_ALIGNED} \% ${BALENA_IMAGE_ALIGNMENT}) else @@ -331,8 +334,25 @@ IMAGE_CMD:balenaos-img () { bbfatal "Rootfs labeling for type '${BALENA_ROOT_FSTYPE}' has not been implemented!" fi + # Build resin-data.img: docker-disk is saved as a tar, we will add boot-A dir into it with all boot files. if [ -n "${BALENA_DATA_FS}" ]; then - e2label ${BALENA_DATA_FS} ${BALENA_DATA_FS_LABEL} + if [ ! -f "${BALENA_DATA_STAGING_TAR}" ]; then + bbfatal "resin-data: ${BALENA_DATA_STAGING_TAR} missing" + fi + if [ ! -d "${IMAGE_ROOTFS}/boot" ] || [ -z "$(ls -A "${IMAGE_ROOTFS}/boot" 2>/dev/null)" ]; then + bbfatal "resin-data: ${IMAGE_ROOTFS}/boot is empty" + fi + _balena_data_root=$(mktemp -d) + tar -xf "${BALENA_DATA_STAGING_TAR}" -C "${_balena_data_root}" + mkdir -p "${_balena_data_root}/boot-A" + cp -a "${IMAGE_ROOTFS}/boot/." "${_balena_data_root}/boot-A/" + rm -f "${BALENA_DATA_FS}" + dd if=/dev/zero of="${BALENA_DATA_FS}" bs=1M count=0 seek="${BALENA_DATA_PARTITION_SIZE_MB}" + mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 -T default -b ${BALENA_DATA_FS_BLOCK_SIZE} -i 8192 -d "${_balena_data_root}" -F "${BALENA_DATA_FS}" || \ + bbfatal "resin-data: mkfs.ext4 failed (try increasing BALENA_DATA_PARTITION_SIZE_MB)" + e2label "${BALENA_DATA_FS}" "${BALENA_DATA_FS_LABEL}" + rm -rf "${_balena_data_root}" + bbnote "Built ${BALENA_DATA_FS} from ${BALENA_DATA_STAGING_TAR} and boot-A" fi # diff --git a/meta-balena-common/recipes-bsp/grub/grub-conf/grub.cfg_internal_template b/meta-balena-common/recipes-bsp/grub/grub-conf/grub.cfg_internal_template index 35b40d9071..447f5e81d6 100644 --- a/meta-balena-common/recipes-bsp/grub/grub-conf/grub.cfg_internal_template +++ b/meta-balena-common/recipes-bsp/grub/grub-conf/grub.cfg_internal_template @@ -39,29 +39,44 @@ function get_root_uuid { done } -menuentry 'boot'{ -if [ ${bootcount} = 2 ] ; then - if [ ${resin_root_part} = 2 ] ; then - get_root_uuid "resin-rootB" - else - get_root_uuid "resin-rootA" - fi -else - if [ ${resin_root_part} = 2 ] ; then - get_root_uuid "resin-rootA" +function select_root_slot_and_boot_dir { + if [ ${bootcount} = 2 ] ; then + if [ ${resin_root_part} = 2 ] ; then + set root_slot_label=resin-rootB + set data_boot_dir=/boot-B + else + set root_slot_label=resin-rootA + set data_boot_dir=/boot-A + fi else - get_root_uuid "resin-rootB" + if [ ${resin_root_part} = 2 ] ; then + set root_slot_label=resin-rootA + set data_boot_dir=/boot-A + else + set root_slot_label=resin-rootB + set data_boot_dir=/boot-B + fi fi -fi -linux /boot/@@KERNEL_IMAGETYPE@@ root=UUID=${root_uuid} @@KERNEL_CMDLINE@@ ${extra_os_cmdline} ${extra_os_firmware_class_path} +} + +menuentry 'boot' { + select_root_slot_and_boot_dir + get_root_uuid ${root_slot_label} + set root_slot_uuid=${root_uuid} + get_root_uuid "resin-data" + linux ${data_boot_dir}/@@KERNEL_IMAGETYPE@@ root=UUID=${root_slot_uuid} @@KERNEL_CMDLINE@@ ${extra_os_cmdline} ${extra_os_firmware_class_path} } menuentry 'manualfallbackA' { - get_root_uuid "resin-rootA" - linux /boot/@@KERNEL_IMAGETYPE@@ root=UUID=${root_uuid} @@KERNEL_CMDLINE@@ ${extra_os_cmdline} ${extra_os_firmware_class_path} + get_root_uuid "resin-rootA" + set root_slot_uuid=${root_uuid} + get_root_uuid "resin-data" + linux /boot-A/@@KERNEL_IMAGETYPE@@ root=UUID=${root_slot_uuid} @@KERNEL_CMDLINE@@ ${extra_os_cmdline} ${extra_os_firmware_class_path} } menuentry 'manualfallbackB' { - get_root_uuid "resin-rootB" - linux /boot/@@KERNEL_IMAGETYPE@@ root=UUID=${root_uuid} @@KERNEL_CMDLINE@@ ${extra_os_cmdline} ${extra_os_firmware_class_path} + get_root_uuid "resin-rootB" + set root_slot_uuid=${root_uuid} + get_root_uuid "resin-data" + linux /boot-B/@@KERNEL_IMAGETYPE@@ root=UUID=${root_slot_uuid} @@KERNEL_CMDLINE@@ ${extra_os_cmdline} ${extra_os_firmware_class_path} } diff --git a/meta-balena-common/recipes-containers/docker-disk/balena-data-partition.inc b/meta-balena-common/recipes-containers/docker-disk/balena-data-partition.inc new file mode 100644 index 0000000000..3ed226d6ec --- /dev/null +++ b/meta-balena-common/recipes-containers/docker-disk/balena-data-partition.inc @@ -0,0 +1,4 @@ +# Shared resin-data partition settings (docker-disk seed + balenaos-img mkfs). +BALENA_DATA_PARTITION_SIZE_MB ?= "256" +BALENA_DATA_FS_BLOCK_SIZE ?= "4k" +BALENA_DATA_STAGING ?= "resin-data-staging.tar" diff --git a/meta-balena-common/recipes-containers/docker-disk/docker-disk.bb b/meta-balena-common/recipes-containers/docker-disk/docker-disk.bb index 9795fe37f8..8a12d8c769 100644 --- a/meta-balena-common/recipes-containers/docker-disk/docker-disk.bb +++ b/meta-balena-common/recipes-containers/docker-disk/docker-disk.bb @@ -12,11 +12,9 @@ B = "${S}/build" inherit deploy require docker-disk.inc +require balena-data-partition.inc require recipes-containers/balena-supervisor/balena-supervisor.inc -PARTITION_SIZE ?= "192" -FS_BLOCK_SIZE ?= "4k" - PV = "${HOSTOS_VERSION}" RDEPENDS:${PN} = "balena" @@ -29,10 +27,6 @@ do_compile () { if [ -z "${SUPERVISOR_FLEET}" ] || [ -z "${SUPERVISOR_VERSION}" ]; then bbfatal "docker-disk: SUPERVISOR_FLEET and/or SUPERVISOR_VERSION not set." fi - if [ -z "${PARTITION_SIZE}" ]; then - bbfatal "docker-disk: PARTITION_SIZE needs to have a value (megabytes)." - fi - # At this point we really need internet connectivity for building the # docker image if [ "x${@connected(d)}" != "xyes" ]; then @@ -65,8 +59,7 @@ do_compile () { -e HOSTAPP_PLATFORM="${HOSTAPP_PLATFORM}" \ -e BALENA_API_ENV="${BALENA_API_ENV}" \ -e BALENA_API_TOKEN="${_token}" \ - -e PARTITION_SIZE="${PARTITION_SIZE}" \ - -e FS_BLOCK_SIZE="${FS_BLOCK_SIZE}" \ + -e BALENA_DATA_STAGING="${BALENA_DATA_STAGING}" \ -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v ${B}:/build \ --name ${_container_name} ${_image_name} $DOCKER rmi -f ${_image_name} @@ -82,7 +75,7 @@ do_install () { FILES:${PN} += "/etc/hostapp-extensions.conf" do_deploy () { - install -m 644 ${B}/resin-data.img ${DEPLOYDIR}/resin-data.img + install -m 644 ${B}/${BALENA_DATA_STAGING} ${DEPLOYDIR}/${BALENA_DATA_STAGING} } addtask deploy before do_package after do_install diff --git a/meta-balena-common/recipes-containers/docker-disk/files/entry.sh b/meta-balena-common/recipes-containers/docker-disk/files/entry.sh index 0f130bc43e..9c103c8b67 100644 --- a/meta-balena-common/recipes-containers/docker-disk/files/entry.sh +++ b/meta-balena-common/recipes-containers/docker-disk/files/entry.sh @@ -6,7 +6,6 @@ set -o nounset DOCKER_TIMEOUT=20 # Wait 20 seconds for docker to start DATA_VOLUME=/resin-data BUILD=/build -PARTITION_SIZE=${PARTITION_SIZE:-1024} DOCKER_HOST=unix:///var/run/docker.sock finish() { @@ -74,8 +73,4 @@ kill -TERM "$(cat /var/run/docker.pid)" # don't let wait() error out and crash the build if the docker daemon has already been stopped wait "$(cat /var/run/docker.pid)" || true -# Export the final data filesystem -dd if=/dev/zero of=${BUILD}/resin-data.img bs=1M count=0 seek="${PARTITION_SIZE}" - -# Usage type default, block size 4k defined in recipe. See https://github.com/tytso/e2fsprogs/issues/50 -mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 -T default -b ${FS_BLOCK_SIZE} -i 8192 -d ${DATA_VOLUME} -F ${BUILD}/resin-data.img +tar -cf ${BUILD}/${BALENA_DATA_STAGING} -C ${DATA_VOLUME} . diff --git a/meta-balena-common/recipes-containers/hostapp-update/files/hostapp-update b/meta-balena-common/recipes-containers/hostapp-update/files/hostapp-update index 9aa505c071..b6b1bb98a9 100644 --- a/meta-balena-common/recipes-containers/hostapp-update/files/hostapp-update +++ b/meta-balena-common/recipes-containers/hostapp-update/files/hostapp-update @@ -23,6 +23,77 @@ ERROR() { fi } +# Map the inactive root slot to resin-data boot dir (boot-A / boot-B). +data_boot_slot() { + if [ ! -e /dev/disk/by-state/inactive ] || [ ! -e /dev/disk/by-state/resin-rootA ]; then + return 1 + fi + inactive_dev=$(readlink -f /dev/disk/by-state/inactive) + roota_dev=$(readlink -f /dev/disk/by-state/resin-rootA) + if [ "$inactive_dev" = "$roota_dev" ]; then + echo "boot-A" + else + echo "boot-B" + fi +} + +# Stage hostapp /boot on resin-data boot-{A|B} for GRUB +stage_boot_to_data_partition() { + container_id="$1" + slot="" + + if ! mountpoint -q /mnt/data 2>/dev/null; then + INFO "Skipping data-partition boot staging (/mnt/data not mounted)" + return 0 + fi + + if ! slot=$(data_boot_slot); then + INFO "Skipping data-partition boot staging (inactive root slot unknown)" + return 0 + fi + + data_boot_dir="/mnt/data/${slot}" + data_boot_new="${data_boot_dir}.new" + + rm -rf "$data_boot_new" + mkdir -p "$data_boot_new" + if ! _out=$(balena cp "${container_id}:/boot/." "${data_boot_new}/" 2>&1); then + ERROR "Failed to copy boot files to data partition: ${_out}" + exit 1 + fi + if [ -z "$(ls -A "$data_boot_new" 2>/dev/null)" ]; then + ERROR "Hostapp /boot is empty" + exit 1 + fi + sync -f /mnt/data + + rm -rf "${data_boot_dir}.old" 2>/dev/null || true + if [ -d "$data_boot_dir" ]; then + mv "$data_boot_dir" "${data_boot_dir}.old" + fi + mv -T "$data_boot_new" "$data_boot_dir" + sync -f /mnt/data + rm -rf "${data_boot_dir}.old" 2>/dev/null || true + + INFO "Staged boot payload to ${data_boot_dir}/" +} + +# Root-slot boot dir holds mobynit init only +install_hostapp_init() { + container_id="$1" + hostapp_boot="$2" + + mkdir -p "$hostapp_boot" + if ! _out=$(balena cp "${container_id}:/boot/init" "${hostapp_boot}/" 2>&1); then + ERROR "Failed to copy mobynit init: ${_out}" + exit 1 + fi + if [ ! -x "${hostapp_boot}/init" ]; then + ERROR "Hostapp /boot/init missing or not executable" + exit 1 + fi +} + run_current_hooks_and_recover () { if [ "$hooks_rollback" = 1 ]; then # Run the current ones to cleanup the system. @@ -124,13 +195,13 @@ elif [ "$remote_image" != "" ]; then _out="$(balena pull "$HOSTAPP_IMAGE" 2>&1)" INFO "${_out}" fi -CONTAINER_ID=$(balena create --runtime="bare" --volume=/boot "$HOSTAPP_IMAGE" /bin/sh) -BOOTSTRAP=$(balena inspect -f "{{range .Mounts}}{{.Destination}} {{.Source}}{{end}}" "$CONTAINER_ID" | awk '$1 == "/boot" { print $2 }' | head -n1) +CONTAINER_ID=$(balena create --runtime="bare" "$HOSTAPP_IMAGE" /bin/sh) + +stage_boot_to_data_partition "$CONTAINER_ID" -# Create boot entry rm -rf "$SYSROOT/hostapps/.new" mkdir -p "$SYSROOT/hostapps/.new" -ln -sr "$BOOTSTRAP" "$SYSROOT/hostapps/.new/boot" +install_hostapp_init "$CONTAINER_ID" "$SYSROOT/hostapps/.new/boot" sync -f "$SYSROOT" mv -T "$SYSROOT/hostapps/.new" "$SYSROOT/hostapps/$CONTAINER_ID" sync -f "$SYSROOT"