[2/3] Netboot Pi - Prepare netboot OS environment for Raspberry Pi’s

14 minute read

This second article in the series is about preparing the server side for each of the Raspberry Pi’s that will boot over the network. The architecture principle that I have defined is that each Raspberry Pi will get it’s own unique and isolated configuration.

This article will describe in detail the build-up of the script I have created during my testing to make it work. A scripted approach helps me automate the steps required to create a repeatable solution. Going over all the steps in the script will provide you a full understanding of my reasoning behind these steps to.

Here is a list of articles related to this project in order to achieve a working environment:

  1. Preparing the server – Setup PXE and NFS on PiServer
  2. Preparing the guest OS configurations – Prepare netboot OS environment for Raspberry Pi’s
  3. Enable the Raspberry Pi’s for booting over the network – (under construction)

Requirements

This scripted approach requires 2 files:

  • nodeList – A configuration file that contains information for each Raspberry Pi that you want to enable for booting over the network. Information is stored per line for each Pi with the column order of: serialnumber nodename
  • prepWorkerImage.sh – The script that creates the separated Raspberry Pi OS configuration environments based on the input consumed from the file nodeList

Prepare the nodeList file

The content of the nodeList file has columned information. The current script will read each line and take only into account the first two columns.

  • Column 1: Contains the serial number of the Raspberry Pi that it uses to identify itself for unique configuration during the PXE boot to get from the tftp share.
    • Note: The prepHost.sh script adds a configuration that the PXE environment also accepts a MAC address as unique identifier. See article: Setup PXE and NFS on PiServer
    • The MAC address should be denoted with hyphens as: 11-22-33-AA-BB-CC
  • Column 2: Contains the hostname to be assigned to the specific Raspberry Pi configuration

An example of a nodeList file could look lite this:

1
2
3
5d24fd10    pinode01
c9b08141    pinode02
dc-a6-32-75-c8-1c    pinode03

Explaining the script

The script contains a list of steps in a logical order to enable a separated configuration for a Raspberry Pi to boot from. These steps are wrapped in a function that is called and enriched with the information from an input file containing unique information for each Raspberry Pi. This is preceded by some preparation work not unique for each system to configure.

Now let us dive in to the script starting from the top. The whole script can be found at the bottom of this article.

Step 1. Project directory

To keep things organized I use a project directory netBoot where all the files files will downloaded to and consumed from.

1
2
3
## Create if not exist and enter project directory:
[ ! -d ~/netBoot ] && mkdir -p ~/netBoot
cd ~/netBoot

Step 2. Import and define configuration information

The script consumes information from the nodelist file we have prepared earlier in this article. At this location I also specify values for variables I use later in the script.

1
2
3
## Collect some details for a specific Raspberry Pi to work with this configuration:
INPUT_FILE=~/nodeList
KICKSTART_IP=10.16.200.1
  • INPUT_FILE specifies where the script can find the nodelist file.
  • The variable KICKSTART_IP defines the master Raspberry Pi server IP address that hosts the NFS and tftp shares used for the PXE boot mechanism.

Step 3. Download latest Raspberry Pi OS image

We need to have an image file of the Raspberry Pi OS file on the Pi server that will be used as source for the different Raspberry Pi nodes. To save time and avoid double downloads and unzip actions I have build in some additional logic:

1
2
3
4
5
6
7
8
9
10
11
12
## Download and unzip the latest Raspbian Buster Lite image:
# Download only if newer timestamp than local file
curl -RLo latest-buster-lite.zip -z latest-buster-lite.zip 
    https://downloads.raspberrypi.org/raspios_lite_armhf_latest

# Get the name of the file in the ZIP:
FILE_IN_ZIP=$(zipinfo -1 latest-buster-lite.zip)

# Extract only if not already extracted:
if [ ! -f ${FILE_IN_ZIP} ]; then
    unzip latest-buster-lite.zip
fi
  • The download with curl will check if the timestamp of the file on the server is newer than the one on the local disk.
  • Next the name of the (first) file in the zip file is retrieved to compare if …
  • Compare if there is already a file with the same name on disk. If not, unzip the zip file.

Step 4. Read input file and execute

At the bottom of the script you find the part that reads the information from the file and iterates through the lines and calling the function doPrepWorker. You find this after the functions you call because a bash script is read in to memory from beginning to end before it executes.

1
2
3
4
5
6
7
8
## Read the input file and execute
while read line ; do
    set $line
    PI_ID=$1
    PI_NAME=$2
    echo -e "\nProcessing node:" ${PI_NAME}
    doPrepWorker ${PI_ID} ${PI_NAME}
done < "${INPUT_FILE}"
  • The input file is read line by line
  • For each line the values as we put them in columns are split out into two named variables
  • With doPrepWorker ${PI_ID} ${PI_NAME} the function is called and the two named variables are passed along

Step 5. Function doPrepWorker

We jump back to the function doPrepWorker. This function contains basically the actual script to be executed for each unique Raspberry Pi system. The function is repeated for each line in the nodeList file.

Step 5.1 Creating unique directories

As we want to have full separation of systems for each system a unique NFS and TFTP directory. These directories are created with the information stored in the variables to enable uniqueness.

1
2
3
    # Make a directory to contain the network boot client image:
    echo -e "\nCreate NFS folder for node ..."
    sudo mkdir -p /nfs/${PI_ID}

Step 5.2 Mounting the OS image

We need to make the downloaded image accessible to get to it’s contents.

1
2
3
4
5
6
7
8
9
10
11
12
    # Mount the Raspberry Pi OS image:
    echo -e "\nMount Raspberry Pi OS Image ..."
    # Create mount points for the image
    [ -d rootmnt ] || mkdir rootmnt
    [ -d bootmnt ] || mkdir bootmnt

    # Make the image file accessible
    sudo kpartx -a -v ${FILE_IN_ZIP}

    # Mount the partitions in the image to the mountpoints
    sudo mount /dev/mapper/loop0p2 rootmnt/
    sudo mount /dev/mapper/loop0p1 bootmnt/
  • First we create mount points for the two partitions in the OS image file
  • Next we make the OS image file accessible to the system
  • Lastly we mount the partitions to know mount points to get access to the files and folders

Step 5.3 Copy contents to node directories

Now that the image file is mounted we need to copy over the contents of both partitions to the two respective folders we uniquely created for each Raspberry Pi system. Once completed, the mounts are un-mounted.

1
2
3
4
5
6
7
8
    # Copy the Raspbian Buster Lite image to the network boot client image directory created above:
    echo -e "\nCopy content from root mount ..."
    sudo cp -a rootmnt/* /nfs/${PI_ID}/
    echo -e "\nCopy content from boot mount ..."
    sudo cp -a bootmnt/* /nfs/${PI_ID}/boot/
    echo -e "\nDone. Unmounting ..."
    sudo umount rootmnt
    sudo umount bootmnt

Step 5.4 Replace some boot files

As we are still dealing with beta functionality we need to replace two other files required for the boot process. We will delete the old ones, replace them with new ones and set appropriate permissions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    # We need to replace the default rPI firmware files with the latest version by running the following commands:
    echo -e "\nUpdate some firmware files to enable netboot ..."

    # Remove current
    sudo rm /nfs/${PI_ID}/boot/start4.elf
    sudo rm /nfs/${PI_ID}/boot/fixup4.dat

    # Download latest
    sudo wget https://github.com/Hexxeh/rpi-firmware/raw/stable/start4.elf -P /nfs/${PI_ID}/boot/
    sudo wget https://github.com/Hexxeh/rpi-firmware/raw/stable/fixup4.dat -P /nfs/${PI_ID}/boot/

    # Correct permissions
    sudo chmod 755 /nfs/${PI_ID}/boot/start4.elf
    sudo chmod 755 /nfs/${PI_ID}/boot/fixup4.dat

Step 5.5 Remove SD-Card partition mount points

While using the boot information from the TFTP share all mount points to the SD-Card partitions needs to be removed.

1
2
3
    # Ensure the network boot client image doesn't attempt to look for filesystems on the SD Card:
    echo -e "\nRemove any SD Card mount-points ..."
    sudo sed -i /UUID/d /nfs/${PI_ID}/etc/fstab

Step 5.6 Add NFS share mount point

Now that the mount-points to the SD-Card are removed, we need to add to the boot configuration file /nfs/${PI_ID}/boot/cmdline.txt the unique share and folder location for each individual system.

1
2
3
    # Replace the boot command in the network boot client image to boot from a network share.
    echo -e "\nUpdate cmdline.txt to boot from NFS share ..."
    echo "console=serial0,115200 console=tty root=/dev/nfs nfsroot=${KICKSTART_IP}:/nfs/${PI_ID},vers=3 rw ip=dhcp rootwait elevator=deadline modprobe.blacklist=bcm2835_v4l2" | sudo tee /nfs/${PI_ID}/boot/cmdline.txt

Step 5.7 Enable SSH at boot

By adding an empty file called ssh to the boot folder in the unique NFS share, the new Raspberry Pi will enable SSH access for remote management like with setting up a system for headless operations.

1
2
3
    # Enable SSH in the network boot client image:
    echo -e "\nEnable SSH at first boot of Raspberry Pi ..."
    sudo touch /nfs/${PI_ID}/boot/ssh

Step 5.8 Add NFS share to the server’s configuration

Next we need to add the configuration of the NFS share to the server system to enable the share exposure.

1
2
3
4
5
    # Create a network share containing the network boot client image:
    echo -e "\nCreate NFS share for node ..."
    if [[ $(grep -L "/nfs/${PI_ID}" /etc/exports) ]]; then
        echo "/nfs/${PI_ID} *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
    fi

Step 5.9 Prepare the TrivialFTP (TFTP) folder

Although this step is only required once at the first time to setup a boot environment, It cannot hurt to have it in there to have it automatically updated once a newer Raspberry Pi OS image is used.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    # Create a TrivialFTP folder containing boot code for all network boot clients
    echo -e "\nCreate TFTP root folder ..."
    [ -d /tftpboot ] || mkdir -p /tftpboot
    [ -f /tftpboot/bootcode.bin ] || sudo cp /nfs/${PI_ID}/boot/bootcode.bin /tftpboot/bootcode.bin
    sudo chmod 777 /tftpboot

    # Create a directory for the first network boot client in the /tftpboot 
    echo -e "\nCreate TFTP boot folder for node ..."
    sudo mkdir -p /tftpboot/${PI_ID}

    # Copy the boot directory from the /nfs/${PI_ID} directory to the new directory 
    # in /tftpboot:
    echo -e "\nCopy boot content to TFTP boot folder ..."
    sudo cp -a /nfs/${PI_ID}/boot/* /tftpboot/${PI_ID}/
  • Copy over the latest bootcode.bin file to the /tftpboot folder
  • Create system specific folder for the boot client
  • Copy over the contents of the boot folder from the new systems NFS share for the netboot procedure.

How it works: What happens is that a Raspberry Pi receives the information where to find the files required to boot. First thing is that the bootcode.bin file is read and executed. Next is that the Raspberry Pi has a mechanism built-in to uniquely identify itself with the last 8 characters of it’s serial number as we have specified in the variable ${PI_ID}. OR if the system is enabled to use MAC addresses for identification, the MAC address is used. The possibility to use MAC addresses is enabled in the server side setup of this project.

Step 5.10 Assign hostname to the Raspberry Pi configuration

Each system should have it’s own unique name. One of the advantages of using a netboot environment is that the configuration files of the individual systems is already accessible for pre-configuration. So we edit here the /etc/hosts and the /etc/hostname file that contain the details for the systems hostname.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    ## Update the /etc/hosts file with the new name.
    echo -e "\nAssigning new computername to the Raspberry Pi image ..."
    # Modify hosts file
    if grep -Fq "127.0.1.1" /nfs/${PI_ID}/etc/hosts
    then
        ## If found, replace the line
        sudo sed -i "/127.0.1.1/c\127.0.1.1    ${PI_NAME}" /nfs/${PI_ID}/etc/hosts
    else
        ## If not found, add the line
        echo '127.0.1.1    '${PI_NAME} &>> /nfs/${PI_ID}/etc/hosts
    fi
    # Modify hostname file
    sudo sed -i "/raspberry/c\\${PI_NAME}" /nfs/${PI_ID}/etc/hostname

    ## The End
    echo -e "\nDone for ${PI_ID} ${PI_NAME}"

Step 6. Reboot the server

Now that all the environments and configurations are created for the Raspberry Pi’s listed in the nodeList file, they are ready to boot over the network. Almost. As we have added some new NFS and TFPT shares to the server configuration, I found it is required to reboot the server as restarting the services does not provide me the desired results. Once rebooted, the Raspberry Pi’s can boot over the network without any issues.

1
sudo reboot

The full script

In this GitHub repository you can find the files discussed in this article:
Git repository Netboot Pi scripts


I hope you like the articles and learned something new.

Here is the link to the full scripts: Git repository Netboot Pi scripts