PXE boot from a MacBook

In the course of installing Debian Linux on an ALIX.2D3 system I needed to setup a Preboot eXecution Environment (PXE) and all I had to hand was a MacBook. This is how I did it.

There are two elements of the PXE boot, the DHCP server and the TFTP server. DHCP tells the client where to find the network bootstrap program and it then retrieves it using TFTP.

To configure the Mac OS DHCP server you will need to edit /etc/bootpd.plist

The options I used are below:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
      <string>My subnet</string>

dhcp_option_66 is the TFTP Server Name
dhcp_option_67 is the Boot File Name
dhcp_option_128 is TFTP Server IP address

I’m not sure which of dhcp_option_66 or dhcp_option_128 is actually being used here, but since they are both the same it doesn’t harm to leave them both in there.

This DHCP server configuration will allocate an IP address to the connected client in the range –

For this to work you will also need to configure your ethernet interface (usually en0) with the static IP address

To start the DHCP server I used ‘/usr/libexec/bootpd -d’ which tells bootp to stay in the foreground and output additional debug information.

Next we will need to configure and start the TFTP server.

For simplicity I used a neat Mac OS application called TftpServer, which is written by Fabrizio La Rosa.

The application lets you configure the TFTP server path as well as checking path permissions and managing the startup and shutdown of the daemon.

With the DHCP and TFTP servers running and the client connected I copied my Debian network bootstrap files to the TFTP path (in my case /private/tftpboot) and then created a few symbolic links:

cd /private/tftpboot
ln -s ./debian-installer/i386/pxelinux.0 pxelinux.
ln -s ./debian-installer/i386/pxelinux.0 pxelinux.0
ln -s ./debian-installer/i386/pxelinux.cfg pxelinux.cfg

The first symlink is not a typo, it is deliberate. After much trial and error (and ethereal packet sniffing) I discovered that for some reason the requested file kept dropping the trailing zero and so by simply adding the symlink everything worked.

With all this in place I was able to power-on the ALIX.2D3, watch it configure via DHCP and then retrieve and execute the installer.

Please leave a comment if you’ve found any of this useful, that way I won’t feel like I’ve wasted my time writing all this up 🙂

Installing Linux on an ALIX.2D3

I was lucky enough to come by a PC Engines ALIX.2D3 low-power server and set about getting a decent operating system onto it, to hopefully replace my existing over-powered Fedora home server.

This project was far from simple, so for the benefit of anyone else embarking on this endeavour, below is a brain dump of what was required. This is not intended as a complete step-by-step guide as I am doing this from memory, but I believe I have captured most of the pertinent information to get you through.

First things first. The 2D3 does not have a video adaptor and so you will require a null-modem serial console cable. I also needed a serial to USB adapter so I could connect from a MacBook.

The second ‘issue’ is that even though the 2D3 has USB ports, it will only boot from a Compact Flash card. I ordered a Kingston 40x 4GB CD card for under a tenner.

After the CF card arrived I spent a frustrating few days trying to install Debian Lenny.

For simplicity I opted to perform a netboot install. I gleaned a lot of useful information about Debian’s serial console support from Philip Tricca’s Installing Lenny on Alix 2D3 over serial console blog post.

With the 2D3 connected via serial I powered it on and hit ‘S’ during the memory test to enter the system BIOS settings. I then changed the baud rate to 9600 (9), enabled PXE boot (E) and enabled HDD wait (W).

I set up my MacBook as a PXE boot server and the instructions are in my blog post – PXE boot from a MacBook

I also had to slightly modify some of the Debian netboot files to add serial console support. More information and the diffs can be found in this Debian bug report – serial console boot not possible

For compatibility sake I wanted to install ‘Lenny’ and not the latest ‘Squeeze’, however the install menu was not obliging. To choose Lenny I had to switch to an advanced install.

After booting from the network (which was attached to my MacBook), I swapped to an open Internet connection and configured an IP address. This allowed me to carry on with the installation and downloads without having to proxy via the MacBook.

When the installation got as far as detecting the hard disk, which in my case was a Compact Flash card, no matter what I did it would not detect the storage. This foxed me for a few days until by chance I had a USB hard disk connected during the installation and this somehow caused the flash disk to be discovered too.

After selecting the CF card (/dev/hda) I was able to partition the disk and finish the Debian installation.

To protect my CF card from way too many read/writes I opted to permanently add a conventional USB hard disk for the more dynamic data partitions – swap, /tmp, /var and /home.

I simply created appropriate partitions using fdisk on the USB drive (/dev/sda), ext3 formatted the partitions using mkfs.ext3 (mkfs.swap in the case of the swap partition) and set about copying the data from the CF card to the USB disk. Don’t forget to mark the swap partition as type 82 (Linux Swap). I used a 525MB swap partition for this 256MB board.

I arranged my partitions as follows:

/dev/sda1 /tmp (Linux ext3) 518.20MB

/dev/sda2 swap (Linux swap) 526.42MB

/dev/sda3 /var (Linux ext3) 3010.46MB

/dev/sda4 /home (Linux ext3) 496050.19MB

Format the partitions:

mkfs.ext3 /dev/sda1

mkfs.ext3 /dev/sda3

mkfs.ext3 /dev/sda4

mkswap /dev/sda2

Create mount points:

mkdir -p /mnt/tmp /mnt/var /mnt/home

Mount the partitions:

mount /dev/sda1 /mnt/tmp

mount /dev/sda3 /mnt/var

mount /dev/sda4 /mnt/home

Set the ‘sticky’ bit on the new /tmp:

chmod 1777 /mnt/tmp

Copy the data from /dev/hda to /dev/sda:

cp -a /var/* /mnt/var

cp -a /home/* /mnt/home

After the copying has completed I added the USB disk partitions to /etc/fstab so that they would mount at boot time. First we need the device IDs:

# blkid /dev/sda*

/dev/sda1: UUID=”8525ccf6-f4ed-4d40-8e51-54251517fe45″ TYPE=”ext3″ SEC_TYPE=”ext2″

/dev/sda2: TYPE=”swap” UUID=”4975cddf-64fd-4961-9b3a-aa092d3b7e07″

/dev/sda3: UUID=”c236f962-633e-42be-9ca5-428aab9bcbc5″ TYPE=”ext3″ SEC_TYPE=”ext2″

/dev/sda4: UUID=”b853c32f-9786-41e1-b006-e5177ffdc74b” TYPE=”ext3″ SEC_TYPE=”ext2″

I made the required changes to /etc/fstab which now looks like this:

# /dev/hda1 / (4GB Compact Flash)

UUID=e8efa91d-65cd-4525-89e8-0e3e46c5ae3e /     ext3 errors=remount-ro,noatime 0 1

# /dev/sda1 /tmp

UUID=8525ccf6-f4ed-4d40-8e51-54251517fe45 /tmp  ext3 defaults,noatime 0 0

# /dev/sda2 swap

UUID=4975cddf-64fd-4961-9b3a-aa092d3b7e07 none  swap sw 0 0

# /dev/sda3 /var

UUID=c236f962-633e-42be-9ca5-428aab9bcbc5 /var  ext3 defaults,noatime 0 0

# /dev/sda4 /home

UUID=b853c32f-9786-41e1-b006-e5177ffdc74b /home ext3 defaults,noatime 0 0

Note that I added the ‘noatime’ option to prevent inode access times from being updated.

Make sure that the previous (now unused) mount points are commented out (including swap).

I also wanted to move root’s home directory to the USB disk and so created a new /mnt/home/root directory and added the following mountcommand to the end of /etc/rc.local:

mount --bind /home/root /root

Finally, before the reboot you’ll need to add a forced delay into the boot sequence to allow the USB drive to be detected before the mountall script starts. More details of this can be found in my previous blog post – Wait for USB.

Now it’s time to be brave and issue a reboot! Don’t forget to disable PXE boot in the BIOS settings as you shouldn’t be needing that again.

After the reboot:

Use ‘mount’ to check that all the partitions are present and correct:

# mount

/dev/hda1 on / type ext3 (rw,noatime,errors=remount-ro)

tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755)

proc on /proc type proc (rw,noexec,nosuid,nodev)

sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)

udev on /dev type tmpfs (rw,mode=0755)

tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)

devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620)

/dev/sda1 on /tmp type ext3 (rw,noatime)

/dev/sda3 on /var type ext3 (rw,noatime)

/dev/sda4 on /home type ext3 (rw,noatime)

Use ‘swapon -s’ to check that swap is mounted on the correct partition (should be /dev/sda2):

# swapon -s

Filename                                Type            Size    Used    Priority

/dev/sda2                               partition       514072  564     -1

You can now start to add packages and configure your server as you want it. As the 2D3 does not come with a RTC battery one of the first things I did was install ntpd to keep the time accurate.

Wait for USB

Having installed Debian GNU/Linux 5.0 (“lenny”) on a PC Engines Alix 2D3 system I ran into problems with the external USB disk drive not being mounted at boot-up.

Mounting local filesystems…mount: special device UUID=… does not exist

These error messages were coming when /etc/rcS.d/S35mountall.sh was attempting to mount all the local filesystems from /etc/fstab. In my case the USB subsystem had not yet initialised and so the entries in /dev had not been created yet.

I could simply wait until booting has finished and mount the partitions manually, or add them to /etc/rc.local, but I was after a more elegant solution.

The answer is to introduce a deliberate pause into the boot sequence to allow the USB subsystem to initialise before the mounts are started.

# Script to force boot sequence to wait for USB disk detection
# Copy to /etc/init.d/waitforusb
# ln -s /etc/init.d/waitforusb /etc/rcS.d/S25waitforusb
# Disk drive mount we're waiting on
# Max time to wait (in seconds)
case "$1" in
  echo "Waiting for USB disk drive $usbdrive"
  for (( i = 0; i <= $maxwait; i++ ))
    [ -b $usbdrive ] && exit 0
    echo -n "."
    sleep 1
  exit 1

Copy this startup file to /etc/init.d and then create a symbolic link in /etc/rcS.d just before the filesystem checks:

ln -s /etc/init.d/waitforusb /etc/rcS.d/S25waitforusb

The script will check for the disk drive entries in /dev every second and allow boot execution to continue when it’s there. There is a safety timeout which allows execution to continue regardless after 30 seconds.