~10 min read
This post explains how to configure the computer used for network booting devices. The goals are to have this happen automatically in the Hardware CI Lab. This is based on experiences booting FreeBSD on the Pine A64-LTS (64-bit ARM), the BeagleBone Black (32-bit ARM) and the Jetson TK1 (32-bit ARM) over the network.
This post is based on devices that have the support for:
- Ethernet connection
- Serial connection
As the number of devices increases, you will want to use either a network switch or bridge the connections in order to easily add/remove devices.
net/isc-dhcp42-server
sysutil/u-boot-<device_name>
- If exists and preinstalled u-boot is insufficient
You should integrate the device into the power controller before starting. As a result, you should have added a line like
console slot# {
aliases <device_name>;
include device serial; port #;
}
in which case connection to the device is as simple as using console <device_name>
.
However, if you don't have a power controller/conserver setup then you can alternatively use cu -l /dev/cuaU# -115200
. Where cuaU#
is the device that was added when you plugged in the serial connection.
WARNING
If you have permissions issues when using
cu
for the first time, then ensure you are part of thedialer
group.
When everything is configured, you'll want to connect to the serial port before powering on the device.
While this isn't required for any given device, it is helpful to have a FreeBSD compiled and ready to install once everything is set up. Ultimately, this will be handled by Jenkins.
NOTE
This flow is based on the one that Jenkins uses. It is nice if you don't already know where you are going to be installing the files. If you do know you may want to look into using
make installworld distribution installkernel
instead of using the release folder.
In the project directory (i.e. /project/<device_name>
), download FreeBSD using Git or SVN:
NOTE
The below commands download FreeBSD-CURRENT but you may want to get a more stable build for initial testing.
git checkout https://github.com/freebsd/freebsd/ src
or
svnlite checkout https://svn.freebsd.org/base/head/ src
let's say you want to add a new arm64
device. These devices can all use the GENERIC
kernel configuration (unlike some arm
devices). However, since we need to netboot it, you'll need to create an NFS configuration. Save this file under src/sys/<target_arch>/conf
.
NOTE
In the case where you need to use a device specific config (i.e.
BEAGLEBONE
you can simply replaceGENERIC
with desired kernel configuration name).
#
# GENERIC-NFS -- Extends GENERIC to include NFS support
#
include GENERIC
ident GENERIC-NFS
# NFS Support
options NFSCL
options NFSLOCKD
# NFS Root Support
options NFS_ROOT
options BOOTP_NFSROOT
options BOOTP_COMPAT
options BOOTP
options BOOTP_NFSV3
#options BOOTP_WIRED_TO=ue1
NOTE
More information about the options used to enable NFS support can be found in
sys/conf/NOTES
Then cross build FreeBSD you can run the following in the project directory (i.e. /project/<device_name>
):
export BASEDIR=$(pwd)
export JFLAG=$(sysctl -n kern.smp.cpus) # may want to use a smaller number
export TARGET=<TARGET>
export TARGET_ARCH=<TARGET_ARCH>
export SRCCONF=/dev/null
export MAKECONF=/dev/null
export MAKEOBJDIRPREFIX=${BASEDIR}/obj
export KERNCONF=GENERIC-NFS # or whatever the kernel config was called
rm -fr ${MAKEOBJDIRPREFIX} # only include this line if you always want a clean build.
cd src
make -j ${JFLAG} -DNO_CLEAN \
buildworld buildkernel \
TARGET=${TARGET} \
TARGET_ARCH=${TARGET_ARCH} \
__MAKE_CONF=${MAKECONF} \
SRCCONF=${SRCCONF}
cd release
sudo -E make clean
sudo -E make -DNOPORTS -DNOSRC -DNODOC packagesystem \
TARGET=${TARGET} TARGET_ARCH=${TARGET_ARCH} \
MAKE="make -DDB_FROM_SRC __MAKE_CONF=${MAKECONF} SRCCONF=${SRCCONF} KERNCONF=${KERNCONF}"
RELEASEDIR=${BASEDIR}/release
rm -fr ${RELEASEDIR}
mkdir -p ${RELEASEDIR}
mv ${MAKEOBJDIRPREFIX}/${BASEDIR}/src/${TARGET}.${TARGET_ARCH}/release/*.txz ${RELEASEDIR}
mv ${MAKEOBJDIRPREFIX}/${BASEDIR}/src/${TARGET}.${TARGET_ARCH}/release/MANIFEST ${RELEASEDIR}
cd ../..
Installing these "release" files will be discussed later once the NFS root directory is set up.
In /etc/inetd.conf
, uncomment the line that starts with tftp dgram udp wait root ....
.
Then, change the path at the end to the directory where you store bootable files. In my case, it was /b/tftpboot
. For the remainder of this post, it will be referred to as $TFTPROOT
Run the following to enable inetd and start it.
sudo sysrc inetd_enable=YES
sudo service inetd start
Create a test file like $TFTPROOT/testfile
with some dummy content and then run tftp
. You should get a prompt like
tftp>
in this shell run something like the following to
connect localhost
get testfile
You should see something like Received ## bytes during #.# seconds in # blocks
which means it is working.
You'll want to create a DHCP configuration. As below is a sample that adds three devices (on IPs 10.0.0.10
to 10.0.0.12
) and connects them to the local TFTP server (running at 10.0.0.1
). The following /usr/local/etc/dhcpd.conf
should work out of the box, and all adding ~250 devices (which is probably a lot more than you'd need) on the addresses 10.0.0.2
to 10.0.0.254
.
NOTE
More information about DHCPD configurations can be found in the handbook
# dhcpd.conf
#
# Sample device configuration file for ISC dhcpd
#
option root-opts code 130 = string;
log-facility local7;
group {
option root-opts "nolockd";
option routers 10.0.0.1;
subnet 10.0.0.0 netmask 255.255.255.0 {
next-server 10.0.0.1;
}
#host pinea64 {
# hardware ethernet 02:ba:fb:ce:73:7e;
# fixed-address 10.0.0.11;
# filename "pinea64/install/boot/loader.efi";
# option root-path "/b/tftpboot/pinea64/install/";
#}
#host beaglebone {
# hardware ethernet c8:df:84:da:65:00;
# fixed-address 10.0.0.12;
# filename "beaglebone/install/boot/loader.efi";
# option root-path "/b/tftpboot/beaglebone/install/";
#}
}
option root-opts code 130 = string;
: Enables the root-opts
option, so you can specify it later
log-facility local7
: Configures where logs will go. You could specify a file here instead like /var/log/dhcpd.log
. (optional)
subnet <subnet_id> netmask <netmask>
: Create a subnet with the id <subnet_id>
and will include addresses based on <netmask>
. You can determine how many addresses are included in the subnet by using.
NOTE
You can use calculator tools like this to see which range of addresses the subnet makes available. In my case, I was using "Network Class A" with 254 "Hosts per subnet". Note that the TFTP server also counts as a host, so this will allow you to add up to 253 devices.
group
: Allows you to define options for a group of hosts/subnets. Saves you typing.
option root-opts nolockd
: Disables the lockd
service for NFS directories.
option routers <tftp_server_ip>
: This will be the host computer's IP on this subnet. This will make the TFTP server available for downloading files and also for using NFS. This should be the first host IP in the subnet.
next-server <tftp_server_ip>
: The TFTP server that will be used on this subnet. This should match the value of option routers
host <device_name>
: Defines options and configutations for DHCP for a specific device
hardware ethernet <MAC_address>
: The MAC address of the device. This can usually be found in u-boot by running printenv ethaddr
. Otherwise, you may have to look for a sticker or manual for the device's MAC address.
fixed-address <device_ip>
: The IP address the device should use. This is arbitrary but should be with the subnet and shouldn't coincide with either the TFTP server (the lowest address, in my case 10.0.0.1
) or the broadcast address (the highest address, in my case 10.0.0.255
)
option filename
: The file that will be downloaded by default when dhcp
is called from u-boot. For most devices, this should ${TFTPROOT}/<device_dir>/boot/loader.efi
option root-path
: Provides the NFS
root directory to use for this device. This is where FreeBSD should be installed. From here on, this will be referred to as $NFSROOTDIR
. For simplicity, this directory should be under the $TFTPROOT
chosen earlier.
> NOTE
>
> The default scripts expect $NFSROOTDIR
to be $TFTPROOT/$DEVICE/install
To start the dhcp service run the following
sudo sysrc dhcpd_enable=YES
sudo service dhcpd start
Additionally, you should restart the service after modifying this file (i.e. when you add a new device).
You'll need a /etc/exports
file to expose the NFS directories. Since all files under /b
or $TFTPROOT
should accessible over NFS and we also want these file systems to be modifiable by the device, the following simple /etc/exports
file works well.
/b -alldirs -maproot=root
NOTE
The manpage for
exports(5)
is helpful if you want more granular options such as limiting only certain IPs (devices) to use certain NFS directories and additionally making them read-only.
Run the following to enable and start the NFS service and its dependencies:
sudo sysrc rpcbind_enable=YES
sudo sysrc rpc_statd_enable=YES
sudo sysrc rpc_lockd_enable=YES
sudo sysrc nfs_server_enable=YES
sudo sysrc mountd_enable=YES
sudo service rpcbind start
sudo service nfsd start
Assuming you have files in $RELEASEDIR
that are ready to be installed into $NFSROOTDIR
for this device. You can install the files by running the following (based on what's done by Jenkins):
# clean up destdir
sudo mkdir -p ${NFSROOTDIR}
sudo chflags -R noschg ${NFSROOTDIR}
sudo rm -rf ${NFSROOTDIR}/*
# install files
for COMPONENT in ${RELEASEDIR}/*.txz; do
sudo tar Jxf ${COMPONENT} -C ${NFSROOTDIR}
done
# Make DTB files available to u-boot
sudo mkdir -p ${TFTPROOT}/dtb
sudo cp -r ${NFSROOTDIR}/boot/dtb ${TFTPROOT}/dtb
You may also want to add a /etc/fstab
, /boot/loader.conf
and /etc/rc.conf
files for improvements to booting. See freebsd-ci/scripts/test/extract-artifacts.sh for examples.
All that needs to be updated with new devices is the dhcpd.conf
by adding a new host
definition to the group and defining the required variables.
For example, adding the following would add a specific Pine A64-LTS device to the dhcpd.
host pinea64 {
hardware ethernet 02:ba:fb:ce:73:7e;
fixed-address 10.0.0.11;
filename "pinea64/install/boot/loader.efi";
option root-path "/b/tftpboot/pinea64/install/";
}
Depending on the device, you may have to use a slightly different process to load the loader.efi
file and the correct DTB (Device Tree Blob) file.
For devices that have u-boot preinstalled, you'll need to ensure bootefi
is an available command. If so then bootcmd_dhcp
should attempt to run dhcp
or tftpboot
with no arguments or a single argument (i.e. $kernel_addr_r
) to download the EFI file and something like dhcp $fdt_addr_r $fdtfile
or tftpboot $fdt_addr_r $fdtfile
to load the DTB . If not, you can use the following lines to change the command to something similar to below to enable this behaviour.
setenv bootcmd_dhcp <any_initializatin_commands>\; if dhcp ${kernel_addr_r}\; then tftpboot ${fdt_addr_r} dtb/${fdt_file}\; if fdt addr ${fdt_addr_r}\; then bootefi ${kernel_addr_r} ${fdt_addr_r}\; fi\; fi\;
WARNING
Before changing this variable permanently (using
saveenv
) you may want to verify that any initialization that is normally done in this command (i.e.usb start
) is still included.
If you're device does not have u-boot preinstalled or it doesn't have the bootefi
command you'll need to update u-boot. This is different depending on the device, however, you'll probably won't have to recompile it. The new version of u-boot should be available from ports or packages at sysutils/u-boot-<device_name>
. If so, the files needed for installing u-boot will be available in /usr/local/share/u-boot/u-boot-<device_name>
.
The default u-boot installation is very likely to try dhcp
booting last which will slow down the boot process. This can be reconfigured by modifying the boot_targets
variable
setenv boot_targets dhcp $boot_targets
WARNING
After changing any environment variables use
saveenv
to make it permanent (after reboot). Use with care!