Raspberry Pi - Diskless Cluster using Ubuntu 20.04.1

I'm attempting to get Ubuntu 20.04 to boot diskless on a Raspberry PI so I can run Ubuntu on a cluster.

So far, I moved the images from the boot partition on an installed RPi4b with Ubuntu installed, into a dnsmasq/TFTP server under the /srv/tftpboot/<serial>/ which is the default for a Raspberry Pi, Ive updated the bootloader and got the PI to boot.

The dnsmasq log shows the files being loaded at boot:

/var/log/dnsmasq.log

==> /var/log/dnsmasq.log <==
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 available DHCP subnet: 192.168.254.254/255.255.255.0
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 vendor class: PXEClient:Arch:00000:UNDI:002001
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 PXE(eth0) dc:a6:32:b4:48:4f proxy
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 tags: eth0
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 broadcast response
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 sent size: 1 option: 53 message-type 2
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 sent size: 4 option: 54 server-identifier 192.168.254.254
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 sent size: 9 option: 60 vendor-class 50:58:45:43:6c:69:65:6e:74
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 sent size: 17 option: 97 client-machine-id 00:52:50:69:34:14:31:d0:00:32:b4:48:4f:f6...
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 sent size: 32 option: 43 vendor-encap 06:01:03:0a:04:00:50:58:45:09:14:00:00:11...
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 available DHCP subnet: 192.168.254.254/255.255.255.0
Sep 6 14:25:03 dnsmasq-dhcp[4799]: 1527100745 vendor class: PXEClient:Arch:00000:UNDI:002001
Sep 6 14:25:03 dnsmasq-tftp[4799]: error 0 Early terminate received from 192.168.254.253
Sep 6 14:25:03 dnsmasq-tftp[4799]: failed sending /srv/tftpboot/6b0bb1f6/start4.elf to 192.168.254.253
Sep 6 14:25:03 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/config.txt to 192.168.254.253
Sep 6 14:25:03 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/pieeprom.sig not found
Sep 6 14:25:03 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/recover4.elf not found
Sep 6 14:25:03 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/recovery.elf not found
Sep 6 14:25:04 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/start4.elf to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/fixup4.dat to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/recovery.elf not found
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/config.txt to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/syscfg.txt to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/usercfg.txt to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/dt-blob.bin to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/recovery.elf not found
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/config.txt to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/syscfg.txt to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/usercfg.txt to 192.168.254.253
Sep 6 14:25:05 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/bootcfg.txt not found
Sep 6 14:25:06 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/bcm2711-rpi-4-b.dtb to 192.168.254.253
Sep 6 14:25:06 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/overlays/overlay_map.dtb to 192.168.254.253
Sep 6 14:25:06 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/config.txt to 192.168.254.253
Sep 6 14:25:06 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/syscfg.txt to 192.168.254.253
Sep 6 14:25:06 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/usercfg.txt to 192.168.254.253
Sep 6 14:25:06 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/cmdline.txt to 192.168.254.253
Sep 6 14:25:08 dnsmasq-tftp[4799]: file /srv/tftpboot/6b0bb1f6/armstub8-gic.bin not found
Sep 6 14:25:08 dnsmasq-tftp[4799]: error 0 Early terminate received from 192.168.254.253
Sep 6 14:25:08 dnsmasq-tftp[4799]: failed sending /srv/tftpboot/6b0bb1f6/uboot_rpi_4.bin to 192.168.254.253
Sep 6 14:25:08 dnsmasq-tftp[4799]: sent /srv/tftpboot/6b0bb1f6/uboot_rpi_4.bin to 192.168.254.253

Typically you would use an NFS server and update your cmdline.txt to contain the locations of the rootfs so once the initrd loads, you can use a root FS stored on the network.

I can't find much info on the options required for Ubuntu so I took them directly from the kernel docs at:

So I updated my cmdline.txt on the TFTP server to:

cmdline.txt

net.ifnames=0 dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 nfsrootdebug elevator=deadline rootwait fixrtc init=initrd.img ip=dhcp rootfstype=nfs4 root=/dev/nfs nfsroot=192.168.254.254/srv/nfs/6b0bb1f6 rw

I only got as far as the U-Boot console. The vmlinuz image wasn't being loaded so I decompressed my vmlinuz-5.4.0-1016-raspi to vmlinux on my installed pi running off the sdcard:

zcat /boot/vmlinuz-5.4.0-1016-raspi > vmlinux

and placed this into the TFTP server then updated my config.txt in the [all] section to make use of the decompressed image.

config.txt

[all]
arm_64bit=1
device_tree_address=0x03000000
enable_uart=1
cmdline=cmdline.txt
include syscfg.txt
include usercfg.txt
kernel=vmlinux
initramfs initrd.img followkernel

I have managed to get as far as the initrd loading and the NFS mount scripts are not mounting the NFS share....

nfsmount: need a path
Begin: Retrying nfs mount ...

Extracting the initrd:

mkdir /root/initrd-expanded
cd /root/initrd-expanded
lz4 -cd /boot/initrd.img-5.4.0-1016-raspi | cpio -id

and looking inside the nfs script:

# NFS root mounting
nfs_mount_root()
{ nfs_top # For DHCP modprobe af_packet wait_for_udev 10 # Default delay is around 180s delay=${ROOTDELAY:-180} # loop until nfsmount succeeds nfs_mount_root_impl ret=$? nfs_retry_count=0 while [ ${nfs_retry_count} -lt "${delay}" ] \ && [ $ret -ne 0 ] ; do [ "$quiet" != "y" ] && log_begin_msg "Retrying nfs mount" sleep 1 nfs_mount_root_impl ret=$? nfs_retry_count=$(( nfs_retry_count + 1 )) [ "$quiet" != "y" ] && log_end_msg done
}

Nothing was hitting the NFS server, tcpdump port nfs and also adding debug logging:

/etc/default/nfs-kernel-server

RPCMOUNTDOPTS="--manage-gids --debug all"

It appears that the path being passed to the nfsroot is incorrect...now this is where it get's a little confusing.

The handling for the nfsroot= on a base image install

nfsroot=192.168.254.254/srv/nfs/6b0bb1f6 -> works
nfsroot=192.168.254.254:/srv/nfs/6b0bb1f6 -> fails

If you take the installation from an installed SDCard after doing an apt update:

nfsroot=192.168.254.254/srv/nfs/6b0bb1f6 -> fails
nfsroot=192.168.254.254:/srv/nfs/6b0bb1f6 -> works

That was 5 hours spent pulling my hair out.

You cannot (or at least I couldn't) create a bootable NFS installed system from the images provided by Ubuntu for Raspberry PI. Go figure! If you attempt it you will get all sorts of errors about services not starting up.

So, Ill answer my own question.

2 Answers

Create a fully diskless system on Ubuntu 20.04.1 on a Raspberry PI 4b

1) Install a RaspberryPi Lite onto an SDCard

2) Boot the Rpi4 with the Raspberry OS SDCard, login and run the following to enable ssh:

cd /boot
touch ssh
reboot

3) Update the boot loader, from another machine. If you know the IP address of the PI and you enabled ssh (above), this script will set the boot code to 0xf12 which means it will try the network, sdcard, reboot in that order over and over. You might want to ensure the firmware version if this post gets old. It will also give you an env file which contains the serial and mac address which is handy, stored in a <uuid>.rpi.env file

./update-bootloader.sh <ip-address-of-the-pi> <ip-address-of-your-nfs-server>

e.g.

./update-bootloader.sh 192.168.0.254 192.169.0.254

#!/usr/bin/env bash
# update-bootloader.sh - update the boot loader for Rpi4
RPI_IP=$1
KICKSTART_IP=$2
RPI_DEFAULT_PASS="raspberry"
PI_EEPROM_DATE="2020-07-31"
PI_EEPROM_VERSION="pieeprom-${PI_EEPROM_DATE}"
PI_EEPROM_FILE="${PI_EEPROM_VERSION}.bin"
PI_EEPROM_LINK=""
UBUNTU_IMAGE_NAME="ubuntu-20.04.1-preinstalled-server-arm64+raspi.img"
UBUNTU_IMAGE_FILE="${UBUNTU_IMAGE_NAME}.xz"
UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
ssh-keygen -R ${RPI_IP}
ssh-keyscan -H ${RPI_IP} >> ~/.ssh/known_hosts
sshpass -p "${RPI_DEFAULT_PASS}" ssh pi@${RPI_IP} << EOF
if [[ -f ${PI_EEPROM_FILE} ]];then rm ${PI_EEPROM_FILE} echo 'removed eeprom file'
fi
rm *.rpi.env
echo 'removed old env'
rm bootconf.txt
echo 'removed bootconf.txt'
if [[ ! -f ${PI_EEPROM_FILE} ]];then wget ${PI_EEPROM_LINK}
fi
echo "extracting boot config from eeprom"
sudo rpi-eeprom-config ${PI_EEPROM_FILE} > bootconf.txt
echo "updating bootconfig"
sed -i 's/BOOT_ORDER=.*/BOOT_ORDER=0xf12/g' bootconf.txt
echo "MAX_RESTARTS=5" | sudo tee -a bootconf.txt
echo "writing eeprom"
sudo rpi-eeprom-config --out ${PI_EEPROM_VERSION}-netboot.bin --config bootconf.txt ${PI_EEPROM_FILE}
echo "updating eeprom on rpi"
sudo rpi-eeprom-update -d -f ./${PI_EEPROM_VERSION}-netboot.bin
echo "getting serial and mac"
cat /proc/cpuinfo | grep Serial | awk -F ': ' '{print \$2}' | tail -c 9 | awk '{print "RPI_SERIAL="\$1}' > ${UUID}.rpi.env
ip addr show eth0 | grep ether | awk '{print \$2}' | awk '{print "RPI_MAC="\$1}' >> ${UUID}.rpi.env
EOF
# copy the pi env back to get the serial and mac
sshpass -p "${RPI_DEFAULT_PASS}" scp -r pi@${RPI_IP}:~/${UUID}.rpi.env ~/${UUID}.rpi.env
sshpass -p "${RPI_DEFAULT_PASS}" ssh pi@${RPI_IP} << EOF
sudo reboot
EOF
cat ~/${UUID}.rpi.env

Your pi will now have been rebooted and will be pinging your network for the DHCP response.

4) Install Ubuntu 20.04.1 onto the SD Card using the PI Imager

5) Plug the SDCard into your Pi and boot it up

6) Do an

apt update -y; apt upgrade -y

setup your pass, install some packages, put vim on there (obviously, who uses emacs or nano anyway??)

7) shutdown the pi

halt -p

On your server

8) Take the Ubuntu 20.04.1 installed SDCard, plug it into your server, mount the drive, copy the OS files from partition 2, to your nfs location for sharing the root to the PI, probably /srv/nfs/<serial>/

# find the partition where your CD card is (mine was sda on a rpi)
fdisk -l
# mount the sdcard - copy the second partition off (should contain the boot files, you can ignore the boot partition, we can use the files off the root partition)
mkdir /root/p2
mount /dev/sda2 /root/p2
# cp using using -ax will give you a correct copy, man cp if needed
cp -vax /root/p2/. /srv/nfs/<serial>/.
# clean up
umount /root/p2
rm -rf /root/p2

9) Mount the boot folder of the Rpi files (stored in the NFS share) to your TFTP location so your TFTP can serve up the boot files, I'm using a PI here to serve the other PIs so edit as needed)

#/etc/fstab
LABEL=writable / ext4 defaults 0 0
LABEL=system-boot /boot/firmware vfat defaults 0 1
/srv/nfs/<serial>/boot /srv/tftpboot/<serial> none defaults,bind 0 0

Then mount the new location

mount -a

10) Extract your vmlinuz on the boot folder of your nfs share into vmlinux as the pi wont decompress the vmlinuz kernel

zcat /srv/nfs/<serial>/boot/vmlinuz-5.4.0-1016-raspi > /src/nfs/<serial>/boot/vmlinux-5.4.0-1016-raspi

11) Create symlinks inside the /srv/nfs/<serial>/boot partition to point to the bcm2711-rpi-4-b.dtb, start4.elf, fixup4.dat files which are missing in the boot folder for the TFTP to find them in the dtb and firmware folders

Optional - clean out all the junk that is no longer needed - look at my ls -al output to see whats should be there and what I removed.

lrwxrwxrwx 1 root root 41 Sep 7 08:19 bcm2711-rpi-4-b.dtb -> dtbs/5.4.0-1016-raspi/bcm2711-rpi-4-b.dtb
-rw-r--r-- 1 root root 216 Sep 7 08:23 cmdline.txt
-rw-r--r-- 1 root root 220286 Aug 13 15:09 config-5.4.0-1016-raspi
-rw-r--r-- 1 root root 231 Sep 7 08:45 config.txt
lrwxrwxrwx 1 root root 43 Sep 6 20:34 dtb -> dtbs/5.4.0-1016-raspi/./bcm2711-rpi-4-b.dtb
lrwxrwxrwx 1 root root 43 Sep 6 20:34 dtb-5.4.0-1016-raspi -> dtbs/5.4.0-1016-raspi/./bcm2711-rpi-4-b.dtb
drwxr-xr-x 3 root root 4096 Sep 7 07:27 dtbs
drwxr-xr-x 2 root root 4096 Sep 7 07:56 firmware
lrwxrwxrwx 1 root root 19 Sep 7 08:15 fixup4.dat -> firmware/fixup4.dat
lrwxrwxrwx 1 root root 27 Sep 6 20:32 initrd.img -> initrd.img-5.4.0-1016-raspi
-rw-r--r-- 1 root root 29579888 Sep 6 20:34 initrd.img-5.4.0-1016-raspi
lrwxrwxrwx 1 root root 19 Sep 7 07:26 start4.elf -> firmware/start4.elf
-rw-r--r-- 1 root root 327 Sep 7 08:04 syscfg.txt
-rw-r--r-- 1 root root 4162247 Aug 13 15:09 System.map-5.4.0-1016-raspi
-rw-r--r-- 1 root root 200 Sep 7 08:04 usercfg.txt
-rw-r--r-- 1 root root 25907712 Sep 7 08:13 vmlinux-5.4.0-1016-raspi
lrwxrwxrwx 1 root root 24 Sep 6 20:32 vmlinuz -> vmlinuz-5.4.0-1016-raspi
-rw-r--r-- 1 root root 8420251 Aug 13 15:09 vmlinuz-5.4.0-1016-raspi

12) update some configs in the /srv/nfs/<serial>/boot partition

#/srv/nfs/<serial>/boot/config.txt
[pi4]
max_framebuffers=2
[all]
arm_64bit=1
device_tree_address=0x03000000
enable_uart=1
cmdline=cmdline.txt
include syscfg.txt
include usercfg.txt
kernel=vmlinux-5.4.0-1016-raspi
initramfs initrd.img-5.4.0-1016-raspi followkernel
#/srv/nfs/<serial>/boot/cmdline.txt
net.ifnames=0 dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 nfsrootdebug elevator=deadline rootwait fixrtc init=initrd.img ip=dhcp rootfstype=nfs4 root=/dev/nfs nfsroot=<nfs ip>:/srv/nfs/<serial> rw

13) Update the fstab - this is the fstab which is sent to the pi

#/srv/nfs/<serial>/etc/fstab
proc /proc proc defaults 0 0
<nfs ip>:/srv/nfs/<serial> / nfs4 defaults,rw,nolock 0 0 # data to be shared to server
<nfs ip>:/srv/nfs/<serial>/boot/firmware /boot/firmware nfs4 defaults,rw,nolock 0 1 # data to be shared to server
none /tmp tmpfs defaults 0 0
none /var/run tmpfs defaults 0 0
none /var/lock tmpfs defaults 0 0
none /var/tmp tmpfs defaults 0 0

14) Install an NFS server (google this) to serve the Pi

#/etc/exports
/srv/nfs/<serial> *(insecure,rw,async,no_root_squash)
exportfs -ra

15) Install a dnsmasq server (google is your friend) to serve up the dhcp options and tftp the boot images

#/etc/dnsmasq.conf
dhcp-range=<your network subnet>,proxy # e.g. 192.168.254.254,proxy
log-dhcp
enable-tftp
tftp-root=/srv/tftpboot
pxe-service=0,"Raspberry Pi Boot"
log-facility=/var/log/dnsmasq.log

16) Clusters? You have more than one Pi? You can use an overlayfs mount on your server to provide multiple Pis their operating system using a single base root file system and then using overlays to give each Pi its own space for storage and FS modifications.

If you got this far then this should be easy:

On your proper server - not a pi

17) Create mounts for overlay fs based mounts so we can use the root fs as a lower dir (google overlayfs)

#/etc/fstab
overlay /srv/nfs/6b0bb1f6 overlay defaults,lowerdir=/srv/nfs/ubuntu-rpi4-lower,upperdir=/srv/nfs/6b0bb1f6-upper,workdir=/srv/nfs/6b0bb1f6-work,nfs_export=on,index=on 0 0
overlay /srv/nfs/68e71308 overlay defaults,lowerdir=/srv/nfs/ubuntu-rpi4-lower,upperdir=/srv/nfs/68e71308-upper,workdir=/srv/nfs/68e71308-work,nfs_export=on,index=on 0 0

18) Create the FS system to support the overlays, mine looks like this for 3 pis.

# this is inside /srv/nfs
drwxr-xr-x 1 root root 4096 Sep 7 12:47 68e71308
drwxr-xr-x 3 root root 4096 Sep 7 12:47 68e71308-upper
drwxr-xr-x 3 root root 4096 Sep 7 13:25 68e71308-work
drwxr-xr-x 1 root root 4096 Sep 7 12:13 6b0bb1f6
drwxr-xr-x 2 root root 4096 Sep 7 12:13 6b0bb1f6-upper
drwxr-xr-x 4 root root 4096 Sep 7 13:25 6b0bb1f6-work
drwxr-xr-x 1 root root 4096 Sep 7 12:47 917c9833
drwxr-xr-x 2 root root 4096 Sep 7 11:49 917c9833-upper
drwxr-xr-x 2 root root 4096 Sep 7 11:34 917c9833-work
drwxr-xr-x 21 root root 4096 Sep 6 19:58 ubuntu-rpi4-lower

19) You need to put an /etc/fstab inside the merged folder for the mount (not the upper or work dirs, just the plain serial named one), which will override the ubuntu-rpi4-lower provided ones. Google fusefs or overlayfs for more info, (it's how docker containers work don't ya know :)

20) Create a cmdline.txt inside each merged folder inside /srv/nfs/<serial>/boot/cmdline.txt

21) Export the merged folders over nfs so ours pis can use them as before:

#/etc/exports
/srv/nfs/6b0bb1f6 *(rw,sync,no_subtree_check,no_root_squash,fsid=1)
/srv/nfs/917c9833 *(rw,sync,no_subtree_check,no_root_squash,fsid=2)
/srv/nfs/68e71308 *(rw,sync,no_subtree_check,no_root_squash,fsid=3)
exportfs -ra

22) Adding a new Pi is then just a case:

22.0) Update the boot loader

22.1 Creating three empty folders on the server

mkdir /srv/nfs/<serial>
mkdir /srv/nfs/<serial>-work
mkdir /srv/nfs/<serial>-upper

22.2) Adding an fstab with the mount options for the new serial and upper/work dirs

22.3) Adding an cmdline.txt with the right NFS location

23) Uber automation If you want you can create a hook script inside the initrd of your RaspberryPi SD card which updates the bootloader for you and pings a web server with its serial which then will add the mounts by the time the Pi has rebooted itself, its already booting off the network. Ill provide that at some point.

5

Thank you for the manual. I would add few things. Newer firmwares seem to be fine with uncompressing the kernel. (tested on 5.8 and higher) For me I had issues with permissions on the vmlinuz file though.

Also, this is messing up snap. I guess there are many people against it anyway, but if you would needed. You can add

network inet,
network inet6,

into /etc/apparmor.d/usr.lib.snapd.snap-confine.real

1

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

You Might Also Like