Skip to content

Commit 0cd64e0

Browse files
iavclaude
andcommitted
feat(extensions): add netboot extension for full TFTP+NFS boot
Hooks: - extension_prepare_config: validate variables, compute defaults for NETBOOT_TFTP_PREFIX / NETBOOT_NFS_PATH (shared by LINUXFAMILY/BOARD/BRANCH/RELEASE, or per-host when NETBOOT_HOSTNAME is set), normalize NETBOOT_CLIENT_MAC to PXELINUX 01-<mac> form, fail fast on bad ROOTFS_COMPRESSION/ROOTFS_EXPORT_DIR combinations. - custom_kernel_config: enable ROOT_NFS, NFS_FS, NFS_V3, IP_PNP, IP_PNP_DHCP so root=/dev/nfs ip=dhcp works without an initrd. - post_customize_image: drop armbian-resize-filesystem.service (meaningless on NFS root) and /root/.not_logged_in_yet (the armbian-firstlogin interactive wizard blocks bring-up when there is no interactive console). armbian-firstrun.service stays — it only regenerates SSH host keys. - host_pre_docker_launch: append a bind-mount for ROOTFS_EXPORT_DIR to DOCKER_EXTRA_ARGS when the directory lives outside ${SRC}, using the hook's documented mechanism. - pre_umount_final_image: assemble the TFTP tree (Image/zImage, dtb/, uInitrd), write pxelinux.cfg/{default.example | 01-<mac>} with the right FDT/FDTDIR line and explicit INITRD directive when uInitrd is present, expose a netboot_artifacts_ready hook for userpatches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 81d4c19 commit 0cd64e0

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

extensions/netboot/netboot.sh

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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+
# nfs-root has no local storage — prevent boot partition creation (and the
41+
# resulting phantom /boot fstab entry whose UUID points at nothing).
42+
# $MOUNT/boot/ remains accessible via the bind mount from $SDCARD (line 394
43+
# of partitioning.sh), so pre_umount_final_image__900 still finds kernel/DTB.
44+
declare -g BOOTSIZE=0
45+
declare -g NETBOOT_HOSTNAME="${NETBOOT_HOSTNAME:-}"
46+
declare -g NETBOOT_CLIENT_MAC="${NETBOOT_CLIENT_MAC:-}"
47+
declare -g NETBOOT_TFTP_PREFIX="${NETBOOT_TFTP_PREFIX:-armbian/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}}"
48+
49+
if [[ -n "${NETBOOT_HOSTNAME}" ]]; then
50+
declare -g NETBOOT_NFS_PATH="${NETBOOT_NFS_PATH:-/srv/netboot/rootfs/hosts/${NETBOOT_HOSTNAME}}"
51+
else
52+
declare -g NETBOOT_NFS_PATH="${NETBOOT_NFS_PATH:-/srv/netboot/rootfs/shared/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}}"
53+
fi
54+
55+
if [[ -n "${NETBOOT_CLIENT_MAC}" ]]; then
56+
declare -g NETBOOT_CLIENT_MAC_NORMALIZED="${NETBOOT_CLIENT_MAC//:/-}"
57+
NETBOOT_CLIENT_MAC_NORMALIZED="${NETBOOT_CLIENT_MAC_NORMALIZED,,}"
58+
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
59+
exit_with_error "${EXTENSION}: NETBOOT_CLIENT_MAC must look like aa:bb:cc:dd:ee:ff (got '${NETBOOT_CLIENT_MAC}')"
60+
fi
61+
fi
62+
63+
# Fail-fast on bad ROOTFS_COMPRESSION/ROOTFS_EXPORT_DIR combos before debootstrap,
64+
# not hours later in create_image_from_sdcard_rootfs. The default itself lives
65+
# in rootfs-to-image.sh; here we only validate values the user actually set.
66+
case "${ROOTFS_COMPRESSION}" in
67+
"" | gzip | zstd | zst | none) ;;
68+
*) exit_with_error "${EXTENSION}: unknown ROOTFS_COMPRESSION: '${ROOTFS_COMPRESSION}' (expected: gzip|zstd|none)" ;;
69+
esac
70+
if [[ "${ROOTFS_COMPRESSION}" == "none" && -z "${ROOTFS_EXPORT_DIR}" ]]; then
71+
exit_with_error "${EXTENSION}: ROOTFS_COMPRESSION=none requires ROOTFS_EXPORT_DIR (otherwise nothing is produced)"
72+
fi
73+
}
74+
75+
# Ensure NFS-root client support is built into the kernel.
76+
function custom_kernel_config__netboot_enable_nfs_root() {
77+
opts_y+=("ROOT_NFS" "NFS_FS" "NFS_V3" "IP_PNP" "IP_PNP_DHCP")
78+
}
79+
80+
# armbian-resize-filesystem tries to grow the root fs on first boot via resize2fs.
81+
# On an NFS-mounted root that's always meaningless (and would error) — strip the
82+
# systemd enablement symlink so the unit never runs.
83+
function post_customize_image__netboot_disable_resize_filesystem() {
84+
[[ "${ROOTFS_TYPE}" == "nfs-root" ]] || return 0
85+
display_alert "${EXTENSION}: disabling armbian-resize-filesystem.service" "meaningless on NFS root" "info"
86+
run_host_command_logged find "${SDCARD}/etc/systemd/system/" \
87+
-name "armbian-resize-filesystem.service" -type l -delete
88+
}
89+
90+
# /etc/profile.d/armbian-check-first-login.sh launches the armbian-firstlogin
91+
# whiptail wizard (root password → user → locale …) when /root/.not_logged_in_yet
92+
# exists. On a default (empty) trigger the wizard would demand interactive input
93+
# on the first login — inconvenient when iterating on netboot images. When the
94+
# file is non-empty it contains PRESET_* keys (e.g. from the preset-firstrun
95+
# extension) that let the wizard complete non-interactively, so we leave it alone.
96+
function post_customize_image__netboot_skip_firstlogin_wizard() {
97+
[[ "${ROOTFS_TYPE}" == "nfs-root" ]] || return 0
98+
[[ -f "${SDCARD}/root/.not_logged_in_yet" ]] || return 0
99+
if [[ -s "${SDCARD}/root/.not_logged_in_yet" ]]; then
100+
display_alert "${EXTENSION}: keeping /root/.not_logged_in_yet" "non-empty — presets detected (e.g. preset-firstrun)" "info"
101+
return 0
102+
fi
103+
display_alert "${EXTENSION}: removing empty /root/.not_logged_in_yet" "wizard would block first login without presets" "info"
104+
run_host_command_logged rm -f "${SDCARD}/root/.not_logged_in_yet"
105+
}
106+
107+
# ROOTFS_EXPORT_DIR must be visible inside the build container at the same path the
108+
# in-container rsync writes to — otherwise data lands in the container's private
109+
# filesystem and disappears on umount. Two cases:
110+
# 1) Path already under ${SRC}: core already bind-mounts ${SRC} at
111+
# ${DOCKER_ARMBIAN_TARGET_PATH} (/armbian) inside the container, so the data
112+
# path IS host-visible — but the env var still holds the host path, which
113+
# does not exist in the container. Translate the env var to the container
114+
# path so rsync writes into the bind-mounted volume.
115+
# 2) Path outside ${SRC}: add an explicit bind-mount at the same path.
116+
function host_pre_docker_launch__netboot_mount_export_dir() {
117+
[[ -z "${ROOTFS_EXPORT_DIR}" ]] && return 0
118+
if [[ "${ROOTFS_EXPORT_DIR}" == "${SRC}" || "${ROOTFS_EXPORT_DIR}" == "${SRC}/"* ]]; then
119+
declare container_export_dir="${DOCKER_ARMBIAN_TARGET_PATH:-/armbian}${ROOTFS_EXPORT_DIR#"${SRC}"}"
120+
display_alert "${EXTENSION}: translating ROOTFS_EXPORT_DIR for container" "${ROOTFS_EXPORT_DIR} -> ${container_export_dir}" "info"
121+
mkdir -p "${ROOTFS_EXPORT_DIR}"
122+
DOCKER_EXTRA_ARGS+=("--env" "ROOTFS_EXPORT_DIR=${container_export_dir}")
123+
return 0
124+
fi
125+
mkdir -p "${ROOTFS_EXPORT_DIR}"
126+
display_alert "${EXTENSION}: bind-mounting ROOTFS_EXPORT_DIR into container" "${ROOTFS_EXPORT_DIR}" "info"
127+
DOCKER_EXTRA_ARGS+=("--mount" "type=bind,source=${ROOTFS_EXPORT_DIR},target=${ROOTFS_EXPORT_DIR}")
128+
}
129+
130+
function pre_umount_final_image__900_collect_netboot_artifacts() {
131+
[[ "${ROOTFS_TYPE}" == "nfs-root" ]] || return 0
132+
133+
# shellcheck disable=SC2154 # ${version} is a readonly global set in create_image_from_sdcard_rootfs
134+
declare tftp_out="${FINALDEST}/${version}-netboot-tftp"
135+
declare tftp_prefix_dir="${tftp_out}/${NETBOOT_TFTP_PREFIX}"
136+
declare pxe_dir="${tftp_out}/pxelinux.cfg"
137+
run_host_command_logged mkdir -pv "${tftp_prefix_dir}/dtb" "${pxe_dir}"
138+
139+
# Kernel image: arm64 uses Image, armv7 uses zImage. Preserve source basename
140+
# so U-Boot `booti`/`bootz` still picks the right path via image header.
141+
declare kernel_src="" kernel_name=""
142+
if [[ -f "${MOUNT}/boot/Image" ]]; then
143+
kernel_src="${MOUNT}/boot/Image"
144+
kernel_name="Image"
145+
elif [[ -f "${MOUNT}/boot/zImage" ]]; then
146+
kernel_src="${MOUNT}/boot/zImage"
147+
kernel_name="zImage"
148+
elif [[ -f "${MOUNT}/boot/vmlinuz-${IMAGE_INSTALLED_KERNEL_VERSION}" ]]; then
149+
kernel_src="${MOUNT}/boot/vmlinuz-${IMAGE_INSTALLED_KERNEL_VERSION}"
150+
# vmlinuz-* is a generic bzImage/Image; prefer Image for arm64, zImage otherwise
151+
[[ "${ARCH}" == "arm64" ]] && kernel_name="Image" || kernel_name="zImage"
152+
fi
153+
[[ -n "${kernel_src}" ]] || exit_with_error "${EXTENSION}: kernel image not found under ${MOUNT}/boot"
154+
run_host_command_logged cp -v "${kernel_src}" "${tftp_prefix_dir}/${kernel_name}"
155+
156+
if [[ -d "${MOUNT}/boot/dtb" ]]; then
157+
run_host_command_logged cp -a "${MOUNT}/boot/dtb/." "${tftp_prefix_dir}/dtb/"
158+
fi
159+
160+
declare initrd_line=""
161+
if [[ -f "${MOUNT}/boot/uInitrd" ]]; then
162+
run_host_command_logged cp -v "${MOUNT}/boot/uInitrd" "${tftp_prefix_dir}/uInitrd"
163+
initrd_line="INITRD ${NETBOOT_TFTP_PREFIX}/uInitrd"
164+
fi
165+
166+
# When NETBOOT_SERVER is empty, leave ${serverip} literal in nfsroot= so
167+
# U-Boot expands it at `pxe boot` time from DHCP siaddr (path 2).
168+
declare nfsroot_server="${NETBOOT_SERVER:-\${serverip\}}"
169+
170+
# Intentionally no `console=` in APPEND: hardcoding a baud (e.g. 115200)
171+
# breaks boards like helios64 which run at 1500000. Kernel resolves console
172+
# from DTB `/chosen/stdout-path`; `earlycon` keeps the early output.
173+
174+
# BOOT_FDT_FILE is not set for every board (e.g. helios64) — U-Boot then
175+
# resolves DTB via its own ${fdtfile} env. FDTDIR handles both cases.
176+
declare fdt_line
177+
if [[ -n "${BOOT_FDT_FILE}" && "${BOOT_FDT_FILE}" != "none" ]]; then
178+
# K3/BeagleBone boards declare BOOT_FDT_FILE with a .dts suffix (e.g.
179+
# ti/k3-am625-beagleplay.dts); in the TFTP tree we ship the compiled .dtb.
180+
# Normalize so the PXE stanza references a file that actually exists.
181+
declare fdt_file="${BOOT_FDT_FILE}"
182+
[[ "${fdt_file}" == *.dts ]] && fdt_file="${fdt_file%.dts}.dtb"
183+
fdt_line="FDT ${NETBOOT_TFTP_PREFIX}/dtb/${fdt_file}"
184+
else
185+
fdt_line="FDTDIR ${NETBOOT_TFTP_PREFIX}/dtb"
186+
fi
187+
188+
# Per-MAC override wins over `default` in U-Boot `pxe get`. Multiple boards
189+
# can share one TFTP root with distinct `01-<mac>` files.
190+
declare pxe_file
191+
if [[ -n "${NETBOOT_CLIENT_MAC_NORMALIZED}" ]]; then
192+
pxe_file="01-${NETBOOT_CLIENT_MAC_NORMALIZED}"
193+
else
194+
pxe_file="default.example"
195+
fi
196+
197+
cat > "${pxe_dir}/${pxe_file}" <<- EXTLINUX_CONF
198+
# Generated by ${EXTENSION} for ${BOARD} ${BRANCH} ${RELEASE}
199+
# Target NFS path: ${NETBOOT_NFS_PATH}
200+
DEFAULT armbian
201+
TIMEOUT 30
202+
PROMPT 0
203+
204+
LABEL armbian
205+
MENU LABEL Armbian ${BOARD} ${BRANCH} ${RELEASE} (netboot)
206+
KERNEL ${NETBOOT_TFTP_PREFIX}/${kernel_name}
207+
${fdt_line}${initrd_line:+
208+
${initrd_line}}
209+
APPEND root=/dev/nfs nfsroot=${nfsroot_server}:${NETBOOT_NFS_PATH},tcp,v3 ip=dhcp rw rootwait earlycon loglevel=7 panic=10
210+
EXTLINUX_CONF
211+
212+
display_alert "${EXTENSION}: artifacts ready" "${tftp_out}" "info"
213+
display_alert "${EXTENSION}: TFTP payload" "${NETBOOT_TFTP_PREFIX}/ (${kernel_name}, dtb/${initrd_line:+, uInitrd})" "info"
214+
display_alert "${EXTENSION}: PXE config" "pxelinux.cfg/${pxe_file}" "info"
215+
display_alert "${EXTENSION}: target NFS path" "${NETBOOT_NFS_PATH}" "info"
216+
217+
# Expose context to the deploy hook. rootfs.tgz is built by the NFS ROOTFS_TYPE
218+
# path earlier in the pipeline; its path follows the same ${version} naming.
219+
declare -g NETBOOT_TFTP_OUT="${tftp_out}"
220+
declare -g NETBOOT_PXE_FILE="${pxe_file}"
221+
# ROOTFS_ARCHIVE_PATH is set by create_image_from_sdcard_rootfs after the archive
222+
# is produced (honours ROOTFS_COMPRESSION=gzip|zstd). Empty when ROOTFS_COMPRESSION=none.
223+
declare -g NETBOOT_ROOTFS_ARCHIVE="${ROOTFS_ARCHIVE_PATH:-}"
224+
225+
call_extension_method "netboot_artifacts_ready" <<- 'NETBOOT_HOOK_DOC'
226+
*called after netboot TFTP tree and rootfs are staged*
227+
Implementations can rsync ${NETBOOT_TFTP_OUT} to a TFTP server, extract
228+
${NETBOOT_ROOTFS_ARCHIVE} into ${NETBOOT_NFS_PATH} on an NFS server, etc.
229+
When the build host IS the NFS server, prefer ROOTFS_EXPORT_DIR (skips
230+
the archive step and writes straight into the export path).
231+
Exposed context: NETBOOT_TFTP_OUT, NETBOOT_TFTP_PREFIX, NETBOOT_PXE_FILE,
232+
NETBOOT_NFS_PATH, NETBOOT_ROOTFS_ARCHIVE (may be empty), NETBOOT_HOSTNAME,
233+
NETBOOT_CLIENT_MAC, plus BOARD, LINUXFAMILY, BRANCH, RELEASE.
234+
NETBOOT_HOOK_DOC
235+
}

0 commit comments

Comments
 (0)