Zero Client: Boot kernel and root filesystem from network with a Raspberry Pi2 or Pi3

cover
This blog post was published 7 years ago and may or may not have aged well. While reading please keep in mind that it may no longer be accurate or even relevant.

A so-called “Zero Client” is a computer that has nothing on its permanent storage but a bootloader. It loads everything from the network.

With the method presented in this article, you will be able to boot a Raspberry Pi into a full Debian OS with nothing more on the SD card other than the Raspberry firmware files and the u-boot bootloader on a FAT file system. The Linux kernel and the actual OS will be served over the local ethernet network.

We will only focus on the Raspberry Pi 3, but the instructions should work with minor adaptations also on a Pi 2.

The following instructions assume that you have already built…

  1. a full root file system for the Raspberry
  2. a u-boot binary, and
  3. a Linux kernel

… based on my previous blog post. Thus, you should already have the following directory structure:

~/workspace
  |- rpi23-gen-image
  |- linux
  |- u-boot
  |- raspberry-firmware

We will do all the work inside of the ~/workspace directory.

Preparation of the SD card

You will only need a small SD card with a FAT filesystem on it. The actual storage of files in the running OS will be transparently done over the network. Mount the filesystem on /mnt/sdcard and do the following:

Copy firmware

cp ./raspberry-firmware/* /mnt/sdcard

Copy u-boot bootloader

cp ./u-boot/u-boot.bin /mnt/sdcard

Create config.txt

config.txt is the configuration file read by the Raspberry firmware blobs. Most importantly, it tells the firmware what kernel to load. “Kernel” is a misleading term here, since we will boot u-boot rather than the kernel.

Create /mnt/sdcard/config.txt with the following contents:

avoid\_warnings=2

# boot u-boot kernel
kernel=u-boot.bin

# run in 64bit mode
arm\_control=0x200

# enable serial console
enable\_uart=1

Make an universal boot script for the u-boot bootloader

To achieve maximum flexibility — to avoid the repetitive dance of manually removing the SD card, copying files to it, and re-inserting it — we will make an universal u-boot startup script that does nothing else than loading yet another u-boot script from the network. This way, there is nothing specific about the to-be-loaded Kernel or OS on the SD card at all.

Create a file boot.scr.mkimage  with the following contents:

setenv autoload no
setenv autostart no
dhcp

setenv serverip 192.168.0.250

tftp 0x100000 /netboot-${serial#}.scr

imi
source 0x100000

Replace the server IP with the actual static IP of your server. Note that this script does nothing else other than loading yet another script called netboot-${serial#}.scr  from the server. serial# is the serial number which u-boot extracts from the Raspberry Pi hardware. This is usually the ethernet network device HW address. This way, you can have separate startup scripts for several Raspberry Pi’s if you have more than one. To keep the setup simple, set the file name to something predictable.

Compile the script into an u-boot readable image:

./u-boot/tools/mkimage -A arm64 -O linux -T script \
-C none -a 0x00 -e 0x00 \
-d boot.scr.mkimage \
boot.scr

Copy boot.scr to the SD card:

cp boot.scr /mnt/sdcard

The SD card preparation is complete at this point. We will now focus on the serving of the files necessary for boot.

Preparation of the file server

Do all of the following as ‘root’ user on a regular PC running Debian 9 (“Stretch”). This PC will act as the “server”.  This server will serve the files necessary to network-boot the Raspberry.

The directory /srv/tftp will hold …

  • an u-boot start script file
  • the kernel uImage file
  • and the binary device tree file.

… to be served by a TFTP server.

mkdir /srv/tftp

The directory /srv/rootfs_rpi3 will hold our entire root file system to be served by a NFS server:

mkdir /srv/rootfs_rpi3

You will find installation instructions of both TFTP and NFS servers further down.

Serve the root file system

Let’s copy the pre-built root file system into the directory from where it will be served by the NFS server:

rsync -a ./rpi23-gen-image/images/stretch/build/chroot/ /srv/rootfs_rpi3

(notice the slash at the end of the source directory)

Fix the root file system for network booting

Edit /srv/rootfs_rpi3/etc/fstab  and comment out all lines. We don’t need to mount anything from the SD card.

When network-booting the Linux kernel, the kernel will configure the network device for us (either with a static IP or DHCP). Any userspace programs attempting to re-configure the network device will cause problems, i.e. a loss of conncection to the NFS server. Thus, we need to prevent systemd-networkd from managing the Ethernet device. Make the device unmanaged by removing the folowing ethernet configuration file:

rm /srv/rootfs_rpi3/etc/systemd/network/eth.network

If you don’t do that, you’ll get the following kernel message during boot:

nfs: server not responding, still trying

That is because systemd has shut down and then re-started the ethernet device. Apparently NFS transfers are sensitive to that.

In case you want to log into the chroot to make additional changes that can only be done from within (e.g. running systemctl scripts etc.), you can do:

cp /usr/bin/qemu-aarch64-static /srv/rpi3fs/usr/bin
LANG=C LC_ALL=C chroot /srv/rpi3fs

Serve Kernel uImage

In this step, we create a Linux kernel uImage that can be directly read by the u-boot bootloader. We read Image.gz directly from the Kernel source directory, and output it into the /srv/tftp directory where a TFTP server will serve it to the Raspberry:

./u-boot/tools/mkimage -A arm64 -O linux -T kernel \
-C gzip -a 0x80000 -e 0x80000 \
-d ./linux/arch/arm64/boot/Image.gz \
/srv/tftp/linux-rpi3.uImage

Serve device tree binary

The u-boot bootloader will also need to load the device tree binary and pass it to the Linux kernel, so copy that too into the /srv/tftp directory.

cp ./linux/arch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b.dtb /srv/tftp/

Serve secondary u-boot script loading the kernel

Create a file netboot-rpi3.scr.mkimage with the following contents:

setenv autoload no
setenv autostart no
dhcp

setenv serverip 192.168.0.250

setenv bootargs "earlyprintk console=tty1 dwc\_otg.lpm\_enable=0 root=/dev/nfs rw rootfstype=nfs nfsroot=192.168.0.250:/srv/rpi3fs,udp,vers=3 ip=dhcp nfsrootdebug smsc95xx.turbo\_mode=N elevator=deadline rootdelay cma=256M@512M net.ifnames=1 init=/bin/systemd loglevel=7 systemd.log\_level=debug systemd.log\_target=console"

tftp ${kernel\_addr\_r} linux-rpi3.uImage
tftp ${fdt\_addr\_r} bcm2837-rpi-3-b.dtb
bootm ${kernel\_addr\_r} - ${fdt\_addr\_r}

Replace the server IP with the static IP of your server PC. Then compile this script into an u-boot readable image and output it directly to the /srv/tftp directory:

./u-boot/tools/mkimage -A arm64 -O linux -T script \
-C none -a 0x00 -e 0x00 \
-d netboot-rpi3.scr.mkimage \
/srv/tftp/netboot-0000000012345678.scr

Make sure that the filename of the .scr file matches with whatever file name you’ve set in the universal .scr script that we’ve prepared further above.

Install a NFS server

The NFS server will serve the root file system to the Raspberry and provide transparent storage.

apt-get install nfs-kernel-server

Edit /etc/exports and add:

/srv/rootfs_rpi3  *(rw,sync,no_root_squash,no_subtree_check,insecure)

To apply the changed ‘exports’ configuration, run

exportfs -rv

Useful to know about the NFS server:

You can restart the NFS server by running service nfs-kernel-server restart

Configuration files are /etc/default/nfs-kernel-server  and /etc/default/nfs-common

Test NFS server

If you want to be sure that the NFS server works correctly, do the following on another PC:

apt-get install nfs-common

Mount the root file system (fix the static IP for your server):

mkdir /tmp/testmount
mount 192.168.0.250:/srv/rootfs_rpi3 /tmp/testmount
ls -al /tmp/testmount

Install a TFTP server

To install:

apt-get install tftpd-hpa

After installation, check if the TFTP server is running:

ps -ejHf | grep ftp

This command will tell you the default serving directory (/srv/tftp):

/usr/sbin/in.tftpd --listen --user tftp --address 0.0.0.0:69 --secure /srv/tftp

Here is another command that tells you if the TFTP server is listening:

netstat -l -u | grep ftp

To get help about this server: man tftpd

Test TFTP

If you want to be sure that the TFTP server works correctly, do the following on another PC:

apt-get install tftp-hpa

Then see if the server serves the Linux kernel we’ve installed before:

tftp 192.168.0.250
tftp> get linux-rpi3.uImage
tftp> quit

You now should have a local copy of the linux-rpi3.uImage file.

Completion

If you’ve done all of the above correctly, you can insert the prepared SD card into your Raspberry Pi and reboot it. The following will happen:

  1. The Raspberry Pi GPU will load the firmware blobs from the SD card.
  2. The firmware blobs will boot the image specified in config.txt. In our case, this is the u-boot binary on the SD card.
  3. The u-boot bootloader will boot.
  4. The u-boot bootloader loads and runs the universal boot.scr script from the SD card.
  5. The boot.scr downloads the specified secondary boot script from the network and runs it.
  6. The secondary boot script …
    • downloads the device tree binary from the network and loads it into memory.
    • downloads the Linux kernel from the network and loads it into memory
    • passes the device tree binary to the kernel, and boots the kernel
  7. the Linux kernel will bring up the ethernet device, connect to the NFS server, and load the regular OS from there.

Many things can go wrong in this rather long sequence, so if you run into trouble, check the Raspberry boot messages output on an attached screen or serial console, and the log files of the NFS and TFTP servers on your server PC.

Resources

https://www.raspberrypi.org/documentation/linux/kernel/building.md

http://www.whaleblubber.ca/boot-raspberry-pi-nfs/

https://cellux.github.io/articles/moving-to-nfs-root/

http://billauer.co.il/blog/2011/01/diskless-boot-nfs-cobbler/

https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt

http://wiki.linux-nfs.org/wiki/index.php/General_troubleshooting_recommendations

https://wiki.archlinux.org/index.php/NFS

If you found a mistake in this blog post, or would like to suggest an improvement to this blog post, please me an e-mail to michael@franzl.name; as subject please use the prefix "Comment to blog post" and append the post title.
 
Copyright © 2023 Michael Franzl