|
| 1 | +# |
| 2 | +# SPDX-License-Identifier: GPL-2.0 |
| 3 | +# Copyright (c) 2026 Igor Velkov |
| 4 | +# This file is a part of the Armbian Build Framework https://github.com/armbian/build/ |
| 5 | +# |
| 6 | +# Netboot: produce kernel + DTB + extlinux.conf + rootfs.tgz for TFTP/NFS root |
| 7 | +# boot without local storage. See Developer-Guide_Netboot.md for server setup |
| 8 | +# (tftpd-hpa + nfs-kernel-server + router DHCP options) and for the |
| 9 | +# `netboot_artifacts_ready` hook used to auto-deploy artifacts to a server. |
| 10 | +# |
| 11 | +# Variables: |
| 12 | +# NETBOOT_SERVER IP of TFTP/NFS server. If empty, nfsroot= uses |
| 13 | +# ${serverip} (filled by U-Boot from DHCP siaddr). |
| 14 | +# NETBOOT_TFTP_PREFIX Path prefix inside TFTP root. Default: |
| 15 | +# armbian/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE} |
| 16 | +# NETBOOT_NFS_PATH Absolute NFS path of rootfs on the server. |
| 17 | +# Default depends on NETBOOT_HOSTNAME — see below. |
| 18 | +# NETBOOT_HOSTNAME Per-host deployment. When set, default NFS path |
| 19 | +# becomes /srv/netboot/rootfs/hosts/<hostname> |
| 20 | +# (each machine owns a full writable rootfs copy). |
| 21 | +# When empty, shared/${LINUXFAMILY}/${BOARD}/... is used. |
| 22 | +# NETBOOT_CLIENT_MAC Client MAC (aa:bb:cc:dd:ee:ff or aa-bb-cc-dd-ee-ff). |
| 23 | +# When set, PXE config is written as `01-<mac>` |
| 24 | +# (PXELINUX per-MAC override) instead of `default`; |
| 25 | +# multiple boards can then coexist on one TFTP root. |
| 26 | +# |
| 27 | +# Hook: |
| 28 | +# netboot_artifacts_ready Called after all artifacts are staged. Exposed |
| 29 | +# context: NETBOOT_TFTP_OUT, NETBOOT_TFTP_PREFIX, |
| 30 | +# NETBOOT_NFS_PATH, NETBOOT_PXE_FILE, |
| 31 | +# NETBOOT_ROOTFS_ARCHIVE (may be empty if |
| 32 | +# ROOTFS_COMPRESSION=none), plus BOARD/LINUXFAMILY/ |
| 33 | +# BRANCH/RELEASE. Use it from userpatches to rsync |
| 34 | +# to a netboot server, unpack the rootfs archive, |
| 35 | +# etc. For builder-as-NFS-server workflows prefer |
| 36 | +# ROOTFS_EXPORT_DIR to skip the archive step. |
| 37 | + |
| 38 | +function extension_prepare_config__netboot_defaults_and_validate() { |
| 39 | + declare -g NETBOOT_SERVER="${NETBOOT_SERVER:-}" |
| 40 | + declare -g NETBOOT_HOSTNAME="${NETBOOT_HOSTNAME:-}" |
| 41 | + declare -g NETBOOT_CLIENT_MAC="${NETBOOT_CLIENT_MAC:-}" |
| 42 | + declare -g NETBOOT_TFTP_PREFIX="${NETBOOT_TFTP_PREFIX:-armbian/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}}" |
| 43 | + |
| 44 | + if [[ -n "${NETBOOT_HOSTNAME}" ]]; then |
| 45 | + declare -g NETBOOT_NFS_PATH="${NETBOOT_NFS_PATH:-/srv/netboot/rootfs/hosts/${NETBOOT_HOSTNAME}}" |
| 46 | + else |
| 47 | + declare -g NETBOOT_NFS_PATH="${NETBOOT_NFS_PATH:-/srv/netboot/rootfs/shared/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}}" |
| 48 | + fi |
| 49 | + |
| 50 | + if [[ -n "${NETBOOT_CLIENT_MAC}" ]]; then |
| 51 | + declare -g NETBOOT_CLIENT_MAC_NORMALIZED="${NETBOOT_CLIENT_MAC//:/-}" |
| 52 | + NETBOOT_CLIENT_MAC_NORMALIZED="${NETBOOT_CLIENT_MAC_NORMALIZED,,}" |
| 53 | + if [[ ! "${NETBOOT_CLIENT_MAC_NORMALIZED}" =~ ^[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}$ ]]; then |
| 54 | + exit_with_error "${EXTENSION}: NETBOOT_CLIENT_MAC must look like aa:bb:cc:dd:ee:ff (got '${NETBOOT_CLIENT_MAC}')" |
| 55 | + fi |
| 56 | + fi |
| 57 | + |
| 58 | + # Fail-fast on bad ROOTFS_COMPRESSION/ROOTFS_EXPORT_DIR combos before debootstrap, |
| 59 | + # not hours later in create_image_from_sdcard_rootfs. |
| 60 | + case "${ROOTFS_COMPRESSION:-gzip}" in |
| 61 | + gzip | zstd | none) ;; |
| 62 | + *) exit_with_error "${EXTENSION}: unknown ROOTFS_COMPRESSION: '${ROOTFS_COMPRESSION}' (expected: gzip|zstd|none)" ;; |
| 63 | + esac |
| 64 | + if [[ "${ROOTFS_COMPRESSION:-gzip}" == "none" && -z "${ROOTFS_EXPORT_DIR}" ]]; then |
| 65 | + exit_with_error "${EXTENSION}: ROOTFS_COMPRESSION=none requires ROOTFS_EXPORT_DIR (otherwise nothing is produced)" |
| 66 | + fi |
| 67 | +} |
| 68 | + |
| 69 | +# Ensure NFS-root client support is built into the kernel. |
| 70 | +function custom_kernel_config__netboot_enable_nfs_root() { |
| 71 | + opts_y+=("ROOT_NFS" "NFS_FS" "NFS_V3" "IP_PNP" "IP_PNP_DHCP") |
| 72 | +} |
| 73 | + |
| 74 | +# armbian-resize-filesystem tries to grow the root fs on first boot via resize2fs. |
| 75 | +# On an NFS-mounted root that's always meaningless (and would error) — strip the |
| 76 | +# systemd enablement symlink so the unit never runs. |
| 77 | +function post_customize_image__netboot_disable_resize_filesystem() { |
| 78 | + [[ "${ROOTFS_TYPE}" == "nfs-root" ]] || return 0 |
| 79 | + display_alert "${EXTENSION}: disabling armbian-resize-filesystem.service" "meaningless on NFS root" "info" |
| 80 | + run_host_command_logged find "${SDCARD}/etc/systemd/system/" \ |
| 81 | + -name "armbian-resize-filesystem.service" -type l -delete |
| 82 | +} |
| 83 | + |
| 84 | +# /etc/profile.d/armbian-check-first-login.sh launches the armbian-firstlogin |
| 85 | +# whiptail wizard (root password → user → locale …) when /root/.not_logged_in_yet |
| 86 | +# exists. On netboot there is often no interactive console on first boot, so the |
| 87 | +# wizard blocks the whole bring-up. Drop the trigger flag; default root/1234 login |
| 88 | +# keeps working, and armbian-firstrun.service still regenerates SSH host keys. |
| 89 | +function post_customize_image__netboot_skip_firstlogin_wizard() { |
| 90 | + [[ "${ROOTFS_TYPE}" == "nfs-root" ]] || return 0 |
| 91 | + [[ -f "${SDCARD}/root/.not_logged_in_yet" ]] || return 0 |
| 92 | + display_alert "${EXTENSION}: skipping armbian-firstlogin wizard" "no interactive console assumed on netboot" "info" |
| 93 | + run_host_command_logged rm -f "${SDCARD}/root/.not_logged_in_yet" |
| 94 | +} |
| 95 | + |
| 96 | +# ROOTFS_EXPORT_DIR must be visible inside the build container at the same path the |
| 97 | +# in-container rsync writes to — otherwise data lands in the container's private |
| 98 | +# filesystem and disappears on umount. Two cases: |
| 99 | +# 1) Path already under ${SRC}: core already bind-mounts ${SRC} at |
| 100 | +# ${DOCKER_ARMBIAN_TARGET_PATH} (/armbian) inside the container, so the data |
| 101 | +# path IS host-visible — but the env var still holds the host path, which |
| 102 | +# does not exist in the container. Translate the env var to the container |
| 103 | +# path so rsync writes into the bind-mounted volume. |
| 104 | +# 2) Path outside ${SRC}: add an explicit bind-mount at the same path. |
| 105 | +function host_pre_docker_launch__netboot_mount_export_dir() { |
| 106 | + [[ -z "${ROOTFS_EXPORT_DIR}" ]] && return 0 |
| 107 | + if [[ "${ROOTFS_EXPORT_DIR}" == "${SRC}"* ]]; then |
| 108 | + declare container_export_dir="${DOCKER_ARMBIAN_TARGET_PATH:-/armbian}${ROOTFS_EXPORT_DIR#"${SRC}"}" |
| 109 | + display_alert "${EXTENSION}: translating ROOTFS_EXPORT_DIR for container" "${ROOTFS_EXPORT_DIR} -> ${container_export_dir}" "info" |
| 110 | + mkdir -p "${ROOTFS_EXPORT_DIR}" |
| 111 | + DOCKER_EXTRA_ARGS+=("--env" "ROOTFS_EXPORT_DIR=${container_export_dir}") |
| 112 | + return 0 |
| 113 | + fi |
| 114 | + mkdir -p "${ROOTFS_EXPORT_DIR}" |
| 115 | + display_alert "${EXTENSION}: bind-mounting ROOTFS_EXPORT_DIR into container" "${ROOTFS_EXPORT_DIR}" "info" |
| 116 | + DOCKER_EXTRA_ARGS+=("--mount" "type=bind,source=${ROOTFS_EXPORT_DIR},target=${ROOTFS_EXPORT_DIR}") |
| 117 | +} |
| 118 | + |
| 119 | +function pre_umount_final_image__900_collect_netboot_artifacts() { |
| 120 | + [[ "${ROOTFS_TYPE}" == "nfs-root" ]] || return 0 |
| 121 | + |
| 122 | + # shellcheck disable=SC2154 # ${version} is a readonly global set in create_image_from_sdcard_rootfs |
| 123 | + declare tftp_out="${DEST}/images/${version}-netboot-tftp" |
| 124 | + declare tftp_prefix_dir="${tftp_out}/${NETBOOT_TFTP_PREFIX}" |
| 125 | + declare pxe_dir="${tftp_out}/pxelinux.cfg" |
| 126 | + run_host_command_logged mkdir -pv "${tftp_prefix_dir}/dtb" "${pxe_dir}" |
| 127 | + |
| 128 | + # Kernel image: arm64 uses Image, armv7 uses zImage. Preserve source basename |
| 129 | + # so U-Boot `booti`/`bootz` still picks the right path via image header. |
| 130 | + declare kernel_src="" kernel_name="" |
| 131 | + if [[ -f "${MOUNT}/boot/Image" ]]; then |
| 132 | + kernel_src="${MOUNT}/boot/Image" |
| 133 | + kernel_name="Image" |
| 134 | + elif [[ -f "${MOUNT}/boot/zImage" ]]; then |
| 135 | + kernel_src="${MOUNT}/boot/zImage" |
| 136 | + kernel_name="zImage" |
| 137 | + elif [[ -f "${MOUNT}/boot/vmlinuz-${IMAGE_INSTALLED_KERNEL_VERSION}" ]]; then |
| 138 | + kernel_src="${MOUNT}/boot/vmlinuz-${IMAGE_INSTALLED_KERNEL_VERSION}" |
| 139 | + # vmlinuz-* is a generic bzImage/Image; prefer Image for arm64, zImage otherwise |
| 140 | + [[ "${ARCH}" == "arm64" ]] && kernel_name="Image" || kernel_name="zImage" |
| 141 | + fi |
| 142 | + [[ -n "${kernel_src}" ]] || exit_with_error "${EXTENSION}: kernel image not found under ${MOUNT}/boot" |
| 143 | + run_host_command_logged cp -v "${kernel_src}" "${tftp_prefix_dir}/${kernel_name}" |
| 144 | + |
| 145 | + if [[ -d "${MOUNT}/boot/dtb" ]]; then |
| 146 | + run_host_command_logged cp -a "${MOUNT}/boot/dtb/." "${tftp_prefix_dir}/dtb/" |
| 147 | + fi |
| 148 | + |
| 149 | + declare initrd_line="" |
| 150 | + if [[ -f "${MOUNT}/boot/uInitrd" ]]; then |
| 151 | + run_host_command_logged cp -v "${MOUNT}/boot/uInitrd" "${tftp_prefix_dir}/uInitrd" |
| 152 | + initrd_line="INITRD ${NETBOOT_TFTP_PREFIX}/uInitrd" |
| 153 | + fi |
| 154 | + |
| 155 | + # When NETBOOT_SERVER is empty, leave ${serverip} literal in nfsroot= so |
| 156 | + # U-Boot expands it at `pxe boot` time from DHCP siaddr (path 2). |
| 157 | + declare nfsroot_server="${NETBOOT_SERVER:-\${serverip\}}" |
| 158 | + |
| 159 | + # Intentionally no `console=` in APPEND: hardcoding a baud (e.g. 115200) |
| 160 | + # breaks boards like helios64 which run at 1500000. Kernel resolves console |
| 161 | + # from DTB `/chosen/stdout-path`; `earlycon` keeps the early output. |
| 162 | + |
| 163 | + # BOOT_FDT_FILE is not set for every board (e.g. helios64) — U-Boot then |
| 164 | + # resolves DTB via its own ${fdtfile} env. FDTDIR handles both cases. |
| 165 | + declare fdt_line |
| 166 | + if [[ -n "${BOOT_FDT_FILE}" && "${BOOT_FDT_FILE}" != "none" ]]; then |
| 167 | + fdt_line="FDT ${NETBOOT_TFTP_PREFIX}/dtb/${BOOT_FDT_FILE}" |
| 168 | + else |
| 169 | + fdt_line="FDTDIR ${NETBOOT_TFTP_PREFIX}/dtb" |
| 170 | + fi |
| 171 | + |
| 172 | + # Per-MAC override wins over `default` in U-Boot `pxe get`. Multiple boards |
| 173 | + # can share one TFTP root with distinct `01-<mac>` files. |
| 174 | + declare pxe_file |
| 175 | + if [[ -n "${NETBOOT_CLIENT_MAC_NORMALIZED}" ]]; then |
| 176 | + pxe_file="01-${NETBOOT_CLIENT_MAC_NORMALIZED}" |
| 177 | + else |
| 178 | + pxe_file="default.example" |
| 179 | + fi |
| 180 | + |
| 181 | + cat > "${pxe_dir}/${pxe_file}" <<- EXTLINUX_CONF |
| 182 | + # Generated by ${EXTENSION} for ${BOARD} ${BRANCH} ${RELEASE} |
| 183 | + # Target NFS path: ${NETBOOT_NFS_PATH} |
| 184 | + DEFAULT armbian |
| 185 | + TIMEOUT 30 |
| 186 | + PROMPT 0 |
| 187 | +
|
| 188 | + LABEL armbian |
| 189 | + MENU LABEL Armbian ${BOARD} ${BRANCH} ${RELEASE} (netboot) |
| 190 | + KERNEL ${NETBOOT_TFTP_PREFIX}/${kernel_name} |
| 191 | + ${fdt_line}${initrd_line:+ |
| 192 | + ${initrd_line}} |
| 193 | + APPEND root=/dev/nfs nfsroot=${nfsroot_server}:${NETBOOT_NFS_PATH},tcp,v3 ip=dhcp rw rootwait earlycon loglevel=7 panic=10 |
| 194 | + EXTLINUX_CONF |
| 195 | + |
| 196 | + display_alert "${EXTENSION}: artifacts ready" "${tftp_out}" "info" |
| 197 | + display_alert "${EXTENSION}: TFTP payload" "${NETBOOT_TFTP_PREFIX}/ (${kernel_name}, dtb/, uInitrd)" "info" |
| 198 | + display_alert "${EXTENSION}: PXE config" "pxelinux.cfg/${pxe_file}" "info" |
| 199 | + display_alert "${EXTENSION}: target NFS path" "${NETBOOT_NFS_PATH}" "info" |
| 200 | + |
| 201 | + # Expose context to the deploy hook. rootfs.tgz is built by the NFS ROOTFS_TYPE |
| 202 | + # path earlier in the pipeline; its path follows the same ${version} naming. |
| 203 | + declare -g NETBOOT_TFTP_OUT="${tftp_out}" |
| 204 | + declare -g NETBOOT_PXE_FILE="${pxe_file}" |
| 205 | + # ROOTFS_ARCHIVE_PATH is set by create_image_from_sdcard_rootfs after the archive |
| 206 | + # is produced (honours ROOTFS_COMPRESSION=gzip|zstd). Empty when ROOTFS_COMPRESSION=none. |
| 207 | + declare -g NETBOOT_ROOTFS_ARCHIVE="${ROOTFS_ARCHIVE_PATH:-}" |
| 208 | + |
| 209 | + call_extension_method "netboot_artifacts_ready" <<- 'NETBOOT_HOOK_DOC' |
| 210 | + *called after netboot TFTP tree and rootfs are staged* |
| 211 | + Implementations can rsync ${NETBOOT_TFTP_OUT} to a TFTP server, extract |
| 212 | + ${NETBOOT_ROOTFS_ARCHIVE} into ${NETBOOT_NFS_PATH} on an NFS server, etc. |
| 213 | + When the build host IS the NFS server, prefer ROOTFS_EXPORT_DIR (skips |
| 214 | + the archive step and writes straight into the export path). |
| 215 | + Exposed context: NETBOOT_TFTP_OUT, NETBOOT_TFTP_PREFIX, NETBOOT_PXE_FILE, |
| 216 | + NETBOOT_NFS_PATH, NETBOOT_ROOTFS_ARCHIVE (may be empty), NETBOOT_HOSTNAME, |
| 217 | + NETBOOT_CLIENT_MAC, plus BOARD, LINUXFAMILY, BRANCH, RELEASE. |
| 218 | + NETBOOT_HOOK_DOC |
| 219 | +} |
0 commit comments