HowTo: Install Nodegrid via PXE Boot

HowTo: Install Nodegrid via PXE Boot

PXE Boot Setup for ZPE Nodegrid Console Servers


Overview

This guide describes how to set up PXE (Pre-boot eXecution Environment) network booting for ZPE Nodegrid Console Servers. Both Legacy BIOS and UEFI boot modes are supported. PXE boot allows Nodegrid devices to install or upgrade their operating system entirely over the network -- no USB drives or physical console access required for imaging.

The guide is divided into two parts:
  1. Part 1 (Lab): Minimal setup for a single device and a single OS version. Suitable for testing and proof-of-concept.
  2. Part 2 (Scalable): Scales the lab setup to support multiple devices and multiple OS versions simultaneously, with per-device version targeting.
Where BIOS and UEFI differ, both modes are documented inline. The overall architecture is identical -- only the bootloader binary, symlink paths, and a few UEFI-specific requirements (signature files, colon-format HTTP symlinks) change.

This guide uses Alpine Linux with ISC Kea, tftpd-hpa, and Apache httpd for all examples. The architecture is not tied to these specific packages -- any software that provides the same DHCP, TFTP, and HTTP service roles will work. Where distribution-specific commands differ (package installation, service management), common alternatives are shown inline.

How PXE Boot Works (Summary)

  1. Device powers on and its NIC firmware (Intel PXE ROM) broadcasts a DHCP request
  2. The DHCP server responds with an IP address, TFTP server address, and boot file path
  3. The PXE ROM downloads a GRUB bootloader via TFTP (core.0 for BIOS, core.efi for UEFI)
  4. GRUB loads its configuration file (grub.cfg) via TFTP
  5. GRUB downloads the Linux kernel and initrd via HTTP
  6. The kernel boots and downloads the root filesystem via HTTP
  7. The OS is installed to disk and the device reboots
Three network services are required: DHCP, TFTP, and HTTP. All three can run on a single Linux server.

2. Prerequisites

Server Requirements

  1. A Linux server accessible on the same Layer 2 network as the Nodegrid devices. Any modern distribution works (Alpine, Ubuntu, RHEL, Debian).
  2. Root or sudo access on the server.
  3. A static IP address for the server on the PXE subnet.

Software Packages

ServiceThis Guide UsesAlternatives
DHCP serverISC KeaISC DHCP Server (dhcpd), dnsmasq
TFTP servertftpd-hpaatftpd, dnsmasq (built-in TFTP)
HTTP serverApache httpdnginx, lighttpd, any server that follows symlinks

Note: Any software that provides the required service role will work. The key requirements are: DHCP must support per-host reservations with next-server and boot-file-name, TFTP must support chroot (-s flag or equivalent), and HTTP must follow symlinks. Commands and config paths in this guide are specific to Alpine Linux with the packages listed above -- adapt as needed for your distribution and software choices.

Nodegrid NetBoot Tarball

Obtain the Nodegrid NetBoot tarball from ZPE Systems for each OS version you intend to deploy. The tarball naming convention is:
Nodegrid_NetBoot_v<VERSION>_<DATE>.tar.gz

Network Information

Gather the following before starting:

ItemPlaceholderExample
PXE subnet<PXE_SUBNET>172.16.90.0/24
Server IP (on PXE subnet)<SERVER_IP>172.16.90.10
Server management IP<MGMT_IP>192.168.1.20
Default gateway<GATEWAY>172.16.90.1
DNS server<DNS_SERVER>8.8.8.8
Domain name<DOMAIN>example.com
DHCP pool start<POOL_START>172.16.90.200
DHCP pool end<POOL_END>172.16.90.254
DHCP server listening interface<IFACE>eth0
Device MAC address<DEVICE_MAC>e4:1a:2c:00:35:32
Device static IP<DEVICE_IP>172.16.90.50
Nodegrid OS version<VERSION>6.0.36

Paths Used in This Guide

PlaceholderDefault PathPurpose
<TFTP_ROOT>/var/tftpbootTFTP server root (chroot)
<HTTP_DOCROOT>/var/www/localhost/htdocsApache document root
<KEA_CONF>/etc/kea/kea-dhcp4.confKea DHCPv4 configuration file
<KEA_LOG>/var/log/kea/kea-dhcp4.logKea log file

3. Part 1: Lab Setup (Single Version)

This section walks through setting up PXE boot for a single Nodegrid device with a single OS version. By the end, you will be able to power cycle the device and have it install the OS from the network.

3.1 Install Services

Install the three required services. Commands vary by distribution:

Alpine Linux:
apk add kea-dhcp4 tftp-hpa apache2

Ubuntu / Debian:
apt install kea-dhcp4-server tftpd-hpa apache2

RHEL / Rocky / AlmaLinux:
dnf install kea-dhcp4 tftp-server httpd

Create the TFTP root and Kea log directories if they do not exist:
mkdir -p <TFTP_ROOT>
mkdir -p /var/log/kea
mkdir -p /var/lib/kea

3.2 Extract the Nodegrid NetBoot Tarball

Upload the NetBoot tarball to the server and extract it to a staging area:
mkdir -p /var/staging
tar xzf Nodegrid_NetBoot_v<VERSION>_*.tar.gz -C /var/staging/

Examine the extracted contents. You should see a directory structure similar to:
/var/staging/<extracted-dir>/
├── boot/
│ └── grub/
│ ├── grub.cfg
│ ├── grub.cfg.sig
│ ├── fonts/
│ ├── i386-pc/
│ │ ├── core.0 GRUB binary for Legacy BIOS boot
│ │ └── modinfo.sh
│ └── x86_64-efi/
│ └── core.efi GRUB binary for UEFI boot
└── nodegrid-<VERSION>/
├── version
├── vmlinuz
├── vmlinuz.sig
├── initrd
├── initrd.sig
├── rootfs_main.img.gz
└── rootfs_main.img.gz.sig

BIOS vs UEFI: Each tarball ships with both bootloader binaries. i386-pc/core.0 is used for Legacy BIOS devices and x86_64-efi/core.efi is used for UEFI devices. Both share the same grub.cfg, kernel, initrd, and rootfs. The boot mode is selected at provisioning time -- there is no need to choose separate tarballs.

Move the extracted directory to the TFTP root:
mv /var/staging/<extracted-dir> <TFTP_ROOT>/
rm -rf /var/staging

ImportantImportant: The extracted directory becomes the top level under <TFTP_ROOT>. After this step, the GRUB bootloader should be at <TFTP_ROOT>/boot/grub/i386-pc/core.0 and the version-specific files at <TFTP_ROOT>/nodegrid-<VERSION>/.


Verify the critical files exist:
test -f <TFTP_ROOT>/boot/grub/i386-pc/core.0      && echo "OK: core.0 (BIOS)"
test -f <TFTP_ROOT>/boot/grub/x86_64-efi/core.efi && echo "OK: core.efi (UEFI)"
test -f <TFTP_ROOT>/boot/grub/grub.cfg && echo "OK: grub.cfg found"
test -f <TFTP_ROOT>/nodegrid-<VERSION>/version && echo "OK: version file found"
test -f <TFTP_ROOT>/nodegrid-<VERSION>/vmlinuz && echo "OK: kernel found"
test -f <TFTP_ROOT>/nodegrid-<VERSION>/initrd && echo "OK: initrd found"
test -f <TFTP_ROOT>/nodegrid-<VERSION>/rootfs_main.img.gz && echo "OK: rootfs found"

3.3 Configure TFTP

Configure tftpd-hpa to serve from <TFTP_ROOT> with a chroot (-s flag).

Alpine Linux (/etc/conf.d/in.tftpd):
INTFTPD_OPTS="-s <TFTP_ROOT>"

Ubuntu / Debian (/etc/default/tftpd-hpa):
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="<TFTP_ROOT>"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure"

RHEL (/etc/sysconfig/tftpd or systemd override):
TFTP_FLAGS="-s <TFTP_ROOT>"

Why -s (secure / chroot)? The TFTP server chroots to <TFTP_ROOT>, so all client-requested paths are relative to this directory. A request for boot/grub/i386-pc/core.0 resolves to <TFTP_ROOT>/boot/grub/i386-pc/core.0. This also means symlinks within the chroot must use relative paths that resolve entirely within <TFTP_ROOT>.

3.4 Configure HTTP (Apache)

Apache serves the large OS files (kernel, initrd, rootfs) over HTTP. Create symlinks in the HTTP document root that point to the TFTP root:
ln -s <TFTP_ROOT>/boot <HTTP_DOCROOT>/boot
ln -s <TFTP_ROOT>/nodegrid-<VERSION> <HTTP_DOCROOT>/nodegrid-<VERSION>

Ensure Apache has FollowSymLinks enabled for the document root. In your Apache config (e.g., /etc/apache2/httpd.conf or a vhost config):
<Directory "<HTTP_DOCROOT>">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

Why symlinks? The real files live under <TFTP_ROOT> (single source of truth). The HTTP docroot contains only symlinks. This avoids file duplication and ensures both TFTP and HTTP serve the same files.

Verify HTTP will serve the files:
# After starting Apache (step 3.7), test from the server itself:
curl -s -o /dev/null -w "%{http_code}" http://localhost/nodegrid-<VERSION>/version
# Expected: 200

3.5 Configure DHCP (ISC Kea)

Create the Kea DHCPv4 configuration file at <KEA_CONF>:
{
"Dhcp4": {
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{ "output": "<KEA_LOG>" }
],
"severity": "DEBUG",
"debuglevel": 55
}
],

"interfaces-config": {
"interfaces": ["<IFACE>"]
},

"lease-database": {
"type": "memfile",
"persist": true,
"name": "/var/lib/kea/kea-leases4.csv"
},

"valid-lifetime": 600,
"renew-timer": 300,
"rebind-timer": 525,

"subnet4": [
{
"id": 1,
"subnet": "<PXE_SUBNET>",
"pools": [
{ "pool": "<POOL_START> - <POOL_END>" }
],
"option-data": [
{ "name": "routers", "data": "<GATEWAY>" },
{ "name": "domain-name-servers", "data": "<DNS_SERVER>" },
{ "name": "domain-name", "data": "<DOMAIN>" }
],
"reservations": [
{
"hw-address": "<DEVICE_MAC>",
"ip-address": "<DEVICE_IP>",
"next-server": "<SERVER_IP>",
"boot-file-name": "boot/grub/i386-pc/core.0"
}
]
}
]
}
}

Key fields in the reservation:

FieldBIOS ValueUEFI ValuePurpose
hw-addressDevice's MAC address(same)Identifies the device
ip-addressStatic IP for this device(same)Assigned via DHCP reservation
next-serverIP of the TFTP server(same)Tells PXE ROM where to download the bootloader
boot-file-nameboot/grub/i386-pc/core.0boot/grub/x86_64-efi/core.efiPath to the bootloader on TFTP

Note: boot-file-name is relative to the TFTP root. Do not include a leading /. The TFTP server's chroot makes the path resolve to a file under <TFTP_ROOT>. The boot mode (BIOS vs UEFI) is determined entirely by which bootloader path is specified here.

3.6 Verify the File Layout

Before starting services, verify the complete file layout:
echo "=== TFTP Root ==="
ls -la <TFTP_ROOT>/
ls -la <TFTP_ROOT>/boot/grub/
ls -la <TFTP_ROOT>/boot/grub/i386-pc/core.0
ls -la <TFTP_ROOT>/boot/grub/x86_64-efi/core.efi
ls -la <TFTP_ROOT>/nodegrid-<VERSION>/

echo "=== HTTP Docroot ==="
ls -la <HTTP_DOCROOT>/
# Should show symlinks pointing to <TFTP_ROOT>

echo "=== Kea Config ==="
cat <KEA_CONF> | python3 -m json.tool
# Should parse without errors (valid JSON)

Expected results:
  1. core.0 exists and is ~200KB (BIOS bootloader)
  2. core.efi exists and is ~800-900KB (UEFI bootloader)
  3. grub.cfg exists under boot/grub/
  4. vmlinuz, initrd, rootfs_main.img.gz exist under nodegrid-<VERSION>/
  5. HTTP docroot symlinks resolve correctly
  6. Kea config is valid JSON

3.7 Start Services and Test

Start all three services:

Alpine Linux (OpenRC):
rc-service in.tftpd start
rc-service apache2 start
rc-service kea-dhcp4 start

Ubuntu / Debian / RHEL (systemd):
systemctl start tftpd-hpa    # or tftp.socket on RHEL
systemctl start apache2 # or httpd on RHEL
systemctl start kea-dhcp4-server

Verify all services are running:
# Check TFTP
echo "get boot/grub/i386-pc/core.0 /dev/null" | tftp localhost
# Expected: no error, or "Received <N> bytes"

# Check HTTP
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/nodegrid-<VERSION>/version
# Expected: 200

# Check Kea
# Alpine: rc-service kea-dhcp4 status
# systemd: systemctl status kea-dhcp4-server

Power cycle the Nodegrid device. Ensure the device's BIOS boot order has PXE/network boot enabled and set as the first boot option.

Monitor the device's serial console. A successful PXE boot sequence shows:
PXE-E53: No boot filename received           <-- FAIL: Kea not responding
-- or --
Loading kernel: (http)/nodegrid-<VERSION>/vmlinuz <-- SUCCESS
Loading initrd: (http)/nodegrid-<VERSION>/initrd <-- SUCCESS

Tip: If you have access to the server console, watch the Kea log in real time during the boot attempt:
tail -f <KEA_LOG>
You should see DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, and DHCPACK messages for the device's MAC address.

3.8 Troubleshooting -- Lab

Device does not send DHCP requests

  1. Verify PXE/network boot is enabled in the device BIOS
  2. Verify the device and server are on the same Layer 2 network (VLAN)
  3. Check for firewall rules blocking UDP ports 67/68

DHCP response but no TFTP download

  1. Verify next-server in the Kea config matches the server's IP on the PXE subnet
  2. Verify boot-file-name path matches the actual file location under <TFTP_ROOT>
  3. Check firewall rules for UDP port 69
  4. Test TFTP manually: echo "get boot/grub/i386-pc/core.0 /tmp/test.0" | tftp <SERVER_IP>

GRUB loads but cannot find grub.cfg

  1. Verify <TFTP_ROOT>/boot/grub/grub.cfg exists
  2. GRUB's hardcoded prefix is /boot/grub/ -- the file must be at exactly this path relative to the TFTP root
  3. Check TFTP server logs for file-not-found errors

GRUB loads grub.cfg but HTTP downloads fail

  1. Verify Apache is running and listening on port 80
  2. Verify symlinks in <HTTP_DOCROOT> resolve correctly: ls -la <HTTP_DOCROOT>/
  3. Verify FollowSymLinks is enabled in the Apache config
  4. Test HTTP manually: curl -I http://<SERVER_IP>/nodegrid-<VERSION>/version
  5. Check firewall rules for TCP port 80

Version comparison skips install (boots from disk)

  1. The grub.cfg compares the network version against the version already installed on the device's disk
  2. If the versions match, GRUB boots from disk and skips the PXE install
  3. To install a different version, change the version being served (see Part 2)
  4. To force reinstall of the same version, the disk's version file must be modified (out of scope for server-side configuration)

4. Part 2: Multi-Version Scalable Setup

This section extends the lab setup to support multiple Nodegrid OS versions and multiple devices, each independently targeted to a specific version.

4.1 Architecture Overview

The multi-version architecture is built on three principles:
  1. TFTP is the single source of truth. Real files live only under <TFTP_ROOT>. The HTTP docroot contains only symlinks. No file duplication.

  2. boot-file-name is stable -- written once per device at provisioning time and never modified again. Each device's reservation uses a per-MAC TFTP symlink path (boot/grub/i386-pc/core.0-<mac-dashes>) that always resolves to the currently active version's core.0. Version switching retargets the symlink server-side -- no Kea config edit, no Kea restart required.

  3. No defaults. A device without an explicit reservation and per-MAC TFTP symlinks cannot PXE boot. This is a deliberate safety measure -- it prevents devices from accidentally booting the wrong version.

4.2 Directory Naming Convention

Each OS version is stored in its own directory under <TFTP_ROOT> using the naming convention ng<MAJOR-MINOR-PATCH>:

OS VersionDirectory Name
6.0.13ng6-0-13
6.0.36ng6-0-36
5.10.4ng5-10-4

The conversion rule: replace dots with dashes and prefix with ng.

Each version directory is self-contained with its own GRUB binaries, GRUB config, kernel, initrd, and rootfs:
<TFTP_ROOT>/ng<X-Y-Z>/
├── boot/
│ └── grub/
│ ├── grub.cfg Signed, hardcodes "nodegrid-<VERSION>"
│ ├── grub.cfg.sig Signature file (required for UEFI GRUB)
│ ├── i386-pc/
│ │ └── core.0 GRUB bootloader binary — Legacy BIOS
│ └── x86_64-efi/
│ └── core.efi GRUB bootloader binary — UEFI
└── nodegrid-<VERSION>/
├── version Contains the version string (e.g., "6.0.36")
├── vmlinuz Linux kernel
├── initrd Initial ramdisk
├── rootfs_main.img.gz Root filesystem (~800MB)
└── reformat Clean install trigger (0-byte file, optional)

4.3 Extract Additional Version Tarballs

For each additional version, extract the NetBoot tarball and move it to the TFTP root with the correct directory name:
# Upload and extract to staging
mkdir -p /var/staging
tar xzf Nodegrid_NetBoot_v<VERSION>_*.tar.gz -C /var/staging/

# Move to TFTP root with the ng<X-Y-Z> naming convention
mv /var/staging/<extracted-dir> <TFTP_ROOT>/ng<X-Y-Z>/
rm -rf /var/staging

If migrating from the lab setup (Part 1), rename your existing version directory to follow the convention:
# Example: rename the lab layout to multi-version layout
# First, move the existing files into a version directory
mkdir -p <TFTP_ROOT>/ng<X-Y-Z>
mv <TFTP_ROOT>/boot <TFTP_ROOT>/ng<X-Y-Z>/boot
mv <TFTP_ROOT>/nodegrid-<VERSION> <TFTP_ROOT>/ng<X-Y-Z>/nodegrid-<VERSION>

Verify each version directory has the required files:
for VER_DIR in <TFTP_ROOT>/ng*; do
echo "=== $(basename ${VER_DIR}) ==="
NG_SUB=$(ls -d ${VER_DIR}/nodegrid-* 2>/dev/null | head -1)
test -f "${VER_DIR}/boot/grub/i386-pc/core.0" && echo " OK: core.0 (BIOS)" || echo " MISSING: core.0"
test -f "${VER_DIR}/boot/grub/x86_64-efi/core.efi" && echo " OK: core.efi (UEFI)" || echo " MISSING: core.efi"
test -f "${VER_DIR}/boot/grub/grub.cfg" && echo " OK: grub.cfg" || echo " MISSING: grub.cfg"
test -f "${VER_DIR}/boot/grub/grub.cfg.sig" && echo " OK: grub.cfg.sig" || echo " MISSING: grub.cfg.sig"
test -f "${NG_SUB}/version" && echo " OK: version" || echo " MISSING: version"
test -f "${NG_SUB}/vmlinuz" && echo " OK: vmlinuz" || echo " MISSING: vmlinuz"
test -f "${NG_SUB}/initrd" && echo " OK: initrd" || echo " MISSING: initrd"
test -f "${NG_SUB}/rootfs_main.img.gz" && echo " OK: rootfs" || echo " MISSING: rootfs"
done

Two sets of symlinks are required for each version:

The grub.cfg probes for files at (tftp)/nodegrid-<VERSION>/version (a flat path, not inside the ng<X-Y-Z> directory). Create symlinks at the TFTP root to resolve these probes:
ln -s ng<X-Y-Z>/nodegrid-<VERSION> <TFTP_ROOT>/nodegrid-<VERSION>

Example:
ln -s ng6-0-13/nodegrid-6.0.13 <TFTP_ROOT>/nodegrid-6.0.13
ln -s ng6-0-36/nodegrid-6.0.36 <TFTP_ROOT>/nodegrid-6.0.36

Create (or update) symlinks in the HTTP docroot for each version. These use absolute paths because Apache resolves them outside the TFTP chroot:
ln -sf <TFTP_ROOT>/ng<X-Y-Z>/nodegrid-<VERSION> <HTTP_DOCROOT>/nodegrid-<VERSION>

Example:
ln -sf /var/tftpboot/ng6-0-13/nodegrid-6.0.13 <HTTP_DOCROOT>/nodegrid-6.0.13
ln -sf /var/tftpboot/ng6-0-36/nodegrid-6.0.36 <HTTP_DOCROOT>/nodegrid-6.0.36

Note on the boot symlink in HTTP docroot: The grub.cfg also probes HTTP for (http)/boot/grub/... paths. You may keep a single boot symlink in the HTTP docroot pointing to any one version's boot/ directory. This symlink is used only as a fallback and does not affect per-device version targeting (the per-MAC symlink handles that via TFTP).
ln -sf <TFTP_ROOT>/ng<X-Y-Z>/boot <HTTP_DOCROOT>/boot

Remove the lab-style flat symlinks if they exist (from Part 1):
# Only if migrating from lab setup
rm -f <HTTP_DOCROOT>/boot
rm -f <HTTP_DOCROOT>/nodegrid-<VERSION> # remove old flat symlink

Then create the new versioned symlinks as shown above.

Per-MAC TFTP symlinks steer each device to the correct version's bootloader and configuration. The number of symlinks depends on the boot mode.

SymlinkLocationTarget
core.0-<mac-dashes><TFTP_ROOT>/boot/grub/i386-pc/../../../ng<X-Y-Z>/boot/grub/i386-pc/core.0
grub.cfg-01-<mac-dashes><TFTP_ROOT>/boot/grub/../../ng<X-Y-Z>/boot/grub/grub.cfg

UEFI GRUB requires a signature file (.sig) alongside each config file. Without it, GRUB rejects the config and falls to the grub> prompt.

SymlinkLocationTarget
core.efi-<mac-dashes><TFTP_ROOT>/boot/grub/x86_64-efi/../../../ng<X-Y-Z>/boot/grub/x86_64-efi/core.efi
grub.cfg-01-<mac-dashes><TFTP_ROOT>/boot/grub/../../ng<X-Y-Z>/boot/grub/grub.cfg
grub.cfg-01-<mac-dashes>.sig<TFTP_ROOT>/boot/grub/../../ng<X-Y-Z>/boot/grub/grub.cfg.sig

  1. core.0-<mac> / core.efi-<mac> -- The TFTP path referenced in the Kea boot-file-name. The PXE ROM downloads this file to obtain the GRUB binary. The symlink target is retargeted on version switch; the Kea reservation never changes.
  2. grub.cfg-01-<mac> -- GRUB built-in behavior: the grub.cfg-01-<mac> config search is compiled into the GRUB binary (grub_net_search_config_file). This runs before any grub.cfg is loaded -- it is how GRUB finds its config in the first place. The 01- prefix indicates Ethernet hardware type and the naming convention is built into GRUB, not configured on the server.
  3. grub.cfg-01-<mac>.sig -- (UEFI only) GRUB's grub_net_search_config_file requests <config>.sig immediately after <config>. If the .sig file is missing, GRUB rejects the config and falls through all candidates to the grub> prompt.
Where <mac-dashes> is the device's MAC in lowercase with colons replaced by dashes.

MAC Address (colon)Symlink suffix
e4:1a:2c:00:35:32e4-1a-2c-00-35-32
AA:BB:CC:DD:EE:FFaa-bb-cc-dd-ee-ff

Create the GRUB Boot and Config Search Directories

mkdir -p <TFTP_ROOT>/boot/grub/i386-pc
mkdir -p <TFTP_ROOT>/boot/grub/x86_64-efi
mkdir -p <TFTP_ROOT>/boot/grub

ImportantImportant: Do NOT place a default grub.cfg in <TFTP_ROOT>/boot/grub/. The absence of a default ensures that devices without an explicit per-MAC symlink cannot PXE boot -- they fail loudly rather than silently booting the wrong version.


BIOS device -- create both per-MAC symlinks:
# core.0 symlink (used by Kea boot-file-name / PXE ROM)
ln -sf ../../../ng<X-Y-Z>/boot/grub/i386-pc/core.0 \
<TFTP_ROOT>/boot/grub/i386-pc/core.0-<mac-dashes>

# grub.cfg symlink (used by GRUB after it starts)
ln -sf ../../ng<X-Y-Z>/boot/grub/grub.cfg \
<TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-dashes>

UEFI device -- create all three per-MAC symlinks:
# core.efi symlink (used by Kea boot-file-name / PXE ROM)
ln -sf ../../../ng<X-Y-Z>/boot/grub/x86_64-efi/core.efi \
<TFTP_ROOT>/boot/grub/x86_64-efi/core.efi-<mac-dashes>

# grub.cfg symlink (used by GRUB after it starts)
ln -sf ../../ng<X-Y-Z>/boot/grub/grub.cfg \
<TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-dashes>

# grub.cfg.sig symlink (required by UEFI GRUB for signature verification)
ln -sf ../../ng<X-Y-Z>/boot/grub/grub.cfg.sig \
<TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-dashes>.sig

Why relative symlinks? The TFTP server uses chroot (-s flag). After chroot, all paths are relative to <TFTP_ROOT>. Relative symlinks that resolve entirely within the chroot work correctly.

Automation note: The management scripts automate symlink creation for both boot modes. The manual steps above are for reference.

Verify all symlinks resolve:
# Boot symlinks (BIOS)
ls -la <TFTP_ROOT>/boot/grub/i386-pc/core.0-*

# Boot symlinks (UEFI)
ls -la <TFTP_ROOT>/boot/grub/x86_64-efi/core.efi-*

# grub.cfg symlinks (both modes)
ls -la <TFTP_ROOT>/boot/grub/grub.cfg-*

# Verify all resolve to real files
for LINK in <TFTP_ROOT>/boot/grub/i386-pc/core.0-* \
<TFTP_ROOT>/boot/grub/x86_64-efi/core.efi-* \
<TFTP_ROOT>/boot/grub/grub.cfg-*; do
test -f "${LINK}" && echo "OK: $(basename ${LINK})" || echo "BROKEN: $(basename ${LINK})"
done

See also: Appendix B for the complete TFTP directory tree showing version directories, per-MAC symlinks, and fallback symlinks after all steps in this section are complete.

4.6 Update Kea Config for Per-Host Reservations

In the multi-version setup, next-server and boot-file-name are specified per host reservation, not at the subnet level. Each device's reservation uses a stable per-MAC boot-file-name -- a TFTP symlink path that never changes after provisioning. Version switching retargets the symlink server-side; the Kea config is never modified again after a device is added.

Update <KEA_CONF>:
{
"Dhcp4": {
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{ "output": "<KEA_LOG>" }
],
"severity": "DEBUG",
"debuglevel": 55
}
],

"interfaces-config": {
"interfaces": ["<IFACE>"]
},

"lease-database": {
"type": "memfile",
"persist": true,
"name": "/var/lib/kea/kea-leases4.csv"
},

"valid-lifetime": 600,
"renew-timer": 300,
"rebind-timer": 525,

"subnet4": [
{
"id": 1,
"subnet": "<PXE_SUBNET>",
"pools": [
{ "pool": "<POOL_START> - <POOL_END>" }
],
"option-data": [
{ "name": "routers", "data": "<GATEWAY>" },
{ "name": "domain-name-servers", "data": "<DNS_SERVER>" },
{ "name": "domain-name", "data": "<DOMAIN>" }
],
"reservations": [
{
"hw-address": "e4:1a:2c:00:35:32",
"ip-address": "172.16.90.50",
"next-server": "<SERVER_IP>",
"boot-file-name": "boot/grub/i386-pc/core.0-e4-1a-2c-00-35-32"
},
{
"hw-address": "e4:1a:2c:01:5c:fb",
"ip-address": "172.16.90.51",
"next-server": "<SERVER_IP>",
"boot-file-name": "boot/grub/x86_64-efi/core.efi-e4-1a-2c-01-5c-fb"
}
]
}
]
}
}

Key points:
  1. boot-file-name encodes both the boot mode and the device identity:
    1. BIOS: boot/grub/i386-pc/core.0-<mac-dashes>
    2. UEFI: boot/grub/x86_64-efi/core.efi-<mac-dashes>
  2. This is a stable path that never changes regardless of which OS version is active -- the TFTP symlink at that path is what gets retargeted during version switches (see Section 4.7)
  3. BIOS and UEFI devices coexist in the same reservations array -- the boot mode is determined entirely by the boot-file-name path
  4. Non-reserved clients get DHCP leases with routing and DNS options but no PXE boot parameters
After creating the initial config, restart Kea:
# Alpine:
rc-service kea-dhcp4 restart

# systemd:
systemctl restart kea-dhcp4-server

Note: This is the only Kea restart needed for the lifetime of the multi-version setup (beyond adding or removing device reservations). Switching versions does not require a Kea restart.

4.7 Version Switching

To switch a device to a different OS version, retarget its per-MAC TFTP symlinks to the new version directory. No Kea config change and no Kea restart are required -- the boot-file-name in the Kea reservation is a stable per-MAC path that never changes.

The firmware type (BIOS or UEFI) is auto-detected from the existing Kea reservation's boot-file-name -- there is no need to specify it during a version switch.

What Happens During a Version Switch

  1. The boot symlink (core.0-<mac> or core.efi-<mac>) is retargeted to the new version's bootloader
  2. The config symlink (grub.cfg-01-<mac>) is retargeted to the new version's grub.cfg
  3. For UEFI devices, the signature symlink (grub.cfg-01-<mac>.sig) is also retargeted
  4. Per-MAC HTTP symlinks are updated to serve the new version's files
  5. The reformat sentinel is cleared (unless a clean install is requested)
These operations are automated by the management script. For manual reference, retarget the symlinks using ln -sf with the new version directory as the target (see the symlink tables in Section 4.5).

The next power cycle (or PXE boot) of the device triggers the install to the new version.

4.8 Adding a New Device

To add a new Nodegrid device to the PXE infrastructure, the following server-side state must be created. The management script automates all of these steps.

Required State

  1. Per-MAC TFTP boot symlink -- core.0-<mac> (BIOS) or core.efi-<mac> (UEFI), pointing to the target version's bootloader.

  2. Per-MAC GRUB config symlink -- grub.cfg-01-<mac>, pointing to the target version's grub.cfg.

  3. Per-MAC GRUB config signature symlink (UEFI only) -- grub.cfg-01-<mac>.sig, pointing to the target version's grub.cfg.sig. Without this, UEFI GRUB rejects the config.

  4. Per-MAC HTTP directory -- <HTTP_DOCROOT>/<mac-dashes>/ containing symlinks to the target version's version, vmlinuz, initrd, and rootfs_main.img.gz files. This directory structure is not a GRUB built-in -- it is logic engineered into ZPE's grub.cfg, which uses ${net_default_mac} to probe per-device HTTP paths.

  5. Colon-format HTTP symlink -- <HTTP_DOCROOT>/<mac-colons><mac-dashes>. GRUB's ${net_default_mac} variable uses colons (e.g., e4:1a:2c:00:35:32), but the per-MAC HTTP directory uses dashes. This symlink bridges the two formats so GRUB's HTTP requests resolve correctly.

  6. Kea DHCP reservation -- with hw-address, next-server, and a stable boot-file-name that encodes both the boot mode and device identity. Optionally includes a fixed ip-address.
See also: Appendix B for the complete HTTP docroot tree showing per-version symlinks, per-MAC directories with internal symlinks, and colon-format symlinks after a device is provisioned.

Boot Mode Selection

The boot mode is chosen at provisioning time and encoded in the Kea reservation's boot-file-name:

Boot Modeboot-file-name
Legacy BIOSboot/grub/i386-pc/core.0-<mac-dashes>
UEFIboot/grub/x86_64-efi/core.efi-<mac-dashes>

To change a device's boot mode after provisioning, remove the device and re-add it with the desired mode.

After provisioning, the Kea config is pushed to the DHCP server and Kea is restarted once. Subsequent version switches do not require a Kea restart.

4.9 Adding a New OS Version

To add a new Nodegrid OS version to the PXE infrastructure, the following server-side state must be created:
  1. Version directory -- The NetBoot tarball contents extracted to <TFTP_ROOT>/ng<X-Y-Z>/ following the standard naming convention. The directory must contain the GRUB binaries (core.0, core.efi), GRUB config (grub.cfg, grub.cfg.sig), and OS files (vmlinuz, initrd, rootfs_main.img.gz, version).

  2. TFTP fallback symlink -- <TFTP_ROOT>/nodegrid-<VERSION>ng<X-Y-Z>/nodegrid-<VERSION>. Resolves the grub.cfg's probe for (tftp)/nodegrid-<VERSION>/version.

  3. HTTP docroot symlink -- <HTTP_DOCROOT>/nodegrid-<VERSION><TFTP_ROOT>/ng<X-Y-Z>/nodegrid-<VERSION>. Resolves the grub.cfg's HTTP probe.
A deployment script automates the full workflow: tarball upload, extraction with correct directory naming, symlink creation, and post-deployment verification. It auto-detects the version from the tarball contents and validates that all critical files are present.

The new version is available immediately after deployment. Assign devices to it using the version switching procedure (Section 4.7).

4.10 Removing an Old Version

Before removing a version, verify no devices are currently targeting it:

Step 1: Check for Active References

# Check both per-MAC TFTP symlinks
ls -la <TFTP_ROOT>/boot/grub/i386-pc/core.0-* | grep ng<X-Y-Z>
ls -la <TFTP_ROOT>/boot/grub/grub.cfg-* | grep ng<X-Y-Z>

If any devices reference this version, switch them to a different version first (Section 4.7).

rm -f <TFTP_ROOT>/nodegrid-<VERSION>
rm -f <HTTP_DOCROOT>/nodegrid-<VERSION>

Step 3: Remove the Version Directory

rm -rf <TFTP_ROOT>/ng<X-Y-Z>

4.11 Controlling Upgrade vs Clean Install

The reformat sentinel file controls the install behavior per device. It is a 0-byte file in the device's per-MAC HTTP directory:
<HTTP_DOCROOT>/<mac-dashes>/reformat

reformat fileGRUB probe resultKernel cmdlineBehavior
PresentHTTP 200LABEL=pxeboot-formatFull disk reformat + clean OS install
AbsentHTTP 404LABEL=pxebootUpgrade only (preserves existing config)

GRUB probes GET /<mac-dashes>/reformat via HTTP during Stage 4 of the boot chain. The presence or absence of this file in the device's per-MAC HTTP directory determines the boot mode for that specific device -- no other devices are affected.

Managing the Reformat Sentinel

The management script handles the sentinel automatically during version switches -- creating it when a clean install is requested, and clearing it by default otherwise. The sentinel persists across version switches until explicitly cleared.

Manual management:
# Enable clean install for device e4:1a:2c:00:35:32
touch <HTTP_DOCROOT>/e4-1a-2c-00-35-32/reformat

# Disable clean install (upgrade only)
rm -f <HTTP_DOCROOT>/e4-1a-2c-00-35-32/reformat

ImportantImportant: The sentinel is not self-clearing. GRUB reads the sentinel at boot time but does not remove it. After a clean install completes and the device reboots, if the reformat file is still present, the next PXE boot will trigger another clean install. Clear the sentinel after the install completes, or use the management script which handles this automatically on the next version switch.


4.12 Troubleshooting -- Multi-Version

All troubleshooting steps from Section 3.8 apply. Additional issues specific to multi-version setups:

GRUB loads but boots the wrong version

  1. Verify both per-MAC TFTP symlinks point to the correct version:
    ls -la <TFTP_ROOT>/boot/grub/i386-pc/core.0-<mac-dashes>
    ls -la <TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-dashes>
  2. Both should target the same ng<X-Y-Z> version directory. A mismatch (e.g., core.0 from v6.0.36 but grub.cfg from v6.0.13) will technically work (cross-version loading has been tested), but the grub.cfg symlink determines which version's kernel and rootfs are loaded.
  3. The Kea reservation boot-file-name is a stable per-MAC path and never encodes a version -- it does not need to be checked for version mismatches.

GRUB cannot find grub.cfg (file not found)

  1. Verify the per-MAC symlink exists:
    ls -la <TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-with-dashes>
  2. Verify the symlink resolves to a real file:
    test -f <TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-with-dashes> && echo "OK" || echo "BROKEN"
  3. Verify the MAC address format: lowercase, dashes not colons, 01- prefix
  4. The boot/grub/ directory must exist at the TFTP root (not inside a version directory) because GRUB's prefix is hardcoded to /boot/grub/

Device gets DHCP lease but no PXE parameters

  1. Verify the device has a reservation in Kea (not just a pool lease)
  2. Verify the reservation includes both next-server and boot-file-name
  3. Verify boot-file-name is the stable per-MAC form: boot/grub/i386-pc/core.0-<mac-dashes> (no version in the path)
  4. Check the Kea log for the device's MAC address -- the DHCPACK should include next-server and boot-file-name fields

New version directory not accessible via HTTP

  1. Verify the HTTP docroot symlink exists and resolves:
    ls -la <HTTP_DOCROOT>/nodegrid-<VERSION>
  2. Verify Apache FollowSymLinks is enabled
  3. If the symlink uses an absolute path to <TFTP_ROOT>, ensure Apache has read permissions on the TFTP root directory
  4. Test: curl -I http://localhost/nodegrid-<VERSION>/version

GRUB shows 404 errors for module lists

During boot, GRUB may log errors fetching command.lst, fs.lst, crypto.lst, and terminal.lst from /boot/grub/i386-pc/ (BIOS) or /boot/grub/x86_64-efi/ (UEFI). These are optional module list files. In the multi-version layout, the architecture-specific directories exist inside each version directory but not at the TFTP root. These errors are non-fatal -- GRUB proceeds normally without them.

UEFI GRUB falls to grub> prompt (.sig file missing)

  1. UEFI GRUB's grub_net_search_config_file requests <config>.sig immediately after each <config> candidate. If the .sig file is missing, the config is rejected and GRUB falls through all candidates to the interactive grub> prompt.
  2. Verify the .sig symlink exists alongside the config symlink:
    ls -la <TFTP_ROOT>/boot/grub/grub.cfg-01-<mac-dashes>.sig
  3. Verify the symlink resolves to the real grub.cfg.sig in the version directory.
  1. GRUB's ${net_default_mac} variable uses colons (e.g., e4:1a:2c:01:5c:fb), but the per-MAC HTTP directory uses dashes (e.g., e4-1a-2c-01-5c-fb).
  2. A colon-format symlink in the HTTP docroot bridges this gap: <HTTP_DOCROOT>/e4:1a:2c:01:5c:fbe4-1a-2c-01-5c-fb
  3. Without this symlink, GRUB's HTTP requests for version probes, reformat checks, and OS file downloads all return 404.
  4. Verify the symlink exists:
    ls -la <HTTP_DOCROOT>/<mac-colons>

5. Appendix A: Technical Details

GRUB Hardcoded Prefix

The core.0 and core.efi binaries both have a compiled-in prefix of /boot/grub/. This is set at GRUB build time and cannot be changed without recompiling. Regardless of the path used to download the bootloader (e.g., ng6-0-36/boot/grub/i386-pc/core.0 or ng6-0-36/boot/grub/x86_64-efi/core.efi), GRUB always searches for its configuration and modules at /boot/grub/ relative to the TFTP root.

This is why:
  1. Per-MAC grub.cfg symlinks live at <TFTP_ROOT>/boot/grub/, not inside version directories
  2. The boot/grub/ directory at the TFTP root is a routing directory for GRUB config lookups, not a place for real boot files

GRUB Config Search Order

This search order is GRUB built-in behavior -- it is compiled into the core.0 and core.efi binaries and executes before any grub.cfg is loaded. When GRUB starts, it searches for its configuration file in this order (via TFTP):
  1. /boot/grub/grub.cfg-01-<mac> (per-device, Ethernet type prefix)
  2. /boot/grub/grub.cfg-<ip-hex> (per-IP, full hex)
  3. /boot/grub/grub.cfg-<ip-hex-prefix> (progressively shorter IP prefixes)
  4. /boot/grub/grub.cfg (default fallback)
In the multi-version architecture, only option 1 is used. Options 2-4 are not configured (no files at those paths), and option 4 (default) is intentionally absent.

UEFI note: For each config candidate, UEFI GRUB also requests the corresponding .sig file (e.g., grub.cfg-01-<mac>.sig). If the .sig file is missing, that config candidate is rejected and GRUB proceeds to the next candidate in the search order. This is why UEFI devices require a grub.cfg-01-<mac>.sig symlink in addition to the grub.cfg-01-<mac> symlink.

tftpd-hpa with the -s flag performs a chroot() to the TFTP root directory. After chroot:
  1. All client-requested paths are relative to <TFTP_ROOT>
  2. Symlinks must resolve entirely within the chroot
  3. Relative symlinks work correctly (e.g., ../../ng6-0-36/boot/grub/grub.cfg)
  4. Absolute symlinks that point outside <TFTP_ROOT> will not resolve
This is why per-MAC grub.cfg symlinks use relative paths.

Apache httpd does not chroot. With Options FollowSymLinks enabled, Apache follows both relative and absolute symlinks from the document root. This allows absolute symlinks in <HTTP_DOCROOT> to point to targets under <TFTP_ROOT>, even if the TFTP root is outside the HTTP document root.

Both <TFTP_ROOT> and <HTTP_DOCROOT> must be on the same filesystem (or Apache must have read access to the symlink targets).

Version Comparison Logic

The grub.cfg includes a version comparison step:
cmp ($net_proto)/${net_dir}/version ($VERS_PART)/version

This compares the version file from the network (HTTP or TFTP) against the version file on the device's local disk partition. The behavior is:

Comparison ResultAction
Versions differProceed with PXE install (kernel load)
Versions matchBoot from disk, skip PXE install
Comparison error (e.g., no disk)Boot from disk (failsafe)

Operational implication: PXE booting a device that already has the target version installed will result in a disk boot, not a reinstall. The reformat file is never checked when versions match.

Signature Enforcement

The grub.cfg includes signature enforcement for kernel and initrd loading. Signature checking is enforced (set check_signatures=enforce) before loading the kernel and initrd. The per-MAC grub.cfg file (or symlink) is loaded before check_signatures=enforce is set, so GRUB does not verify the signature of the grub.cfg file itself. Since the per-MAC symlink points to a real signed grub.cfg, this is a non-issue for the standard architecture.

UEFI Boot Mode Differences

The overall PXE boot architecture is identical for BIOS and UEFI. The following differences apply to UEFI devices:

Bootloader Binary

Boot ModeBinaryLocationTypical Size
Legacy BIOScore.0<TFTP_ROOT>/ng<X-Y-Z>/boot/grub/i386-pc/~200 KB
UEFIcore.efi<TFTP_ROOT>/ng<X-Y-Z>/boot/grub/x86_64-efi/~800-900 KB

Both binaries share the same compiled-in GRUB prefix (/boot/grub/) and use the same grub.cfg, kernel, initrd, and rootfs.

UEFI GRUB's grub_net_search_config_file function requests <config>.sig immediately after <config> for each candidate in the config search order. If the .sig file is missing, the config is rejected and GRUB falls through all candidates to the grub> prompt.

This is why UEFI devices require a third per-MAC symlink: grub.cfg-01-<mac>.sig../../ng<X-Y-Z>/boot/grub/grub.cfg.sig

GRUB's ${net_default_mac} variable uses colons (e.g., e4:1a:2c:01:5c:fb), but the per-MAC HTTP directory uses dashes (e.g., e4-1a-2c-01-5c-fb). A colon-format symlink in the HTTP docroot bridges this gap:
<HTTP_DOCROOT>/e4:1a:2c:01:5c:fb  ->  e4-1a-2c-01-5c-fb

Without this symlink, all of GRUB's HTTP requests (version probe, reformat check, kernel/initrd/rootfs downloads) return 404. This symlink is also created for BIOS devices for consistency, though BIOS GRUB typically uses dashes in its MAC variable.

UEFI GRUB TFTP Request Sequence

The observed TFTP request sequence from a UEFI device (via packet capture):
  1. boot/grub/x86_64-efi/core.efi-<MAC-dashes> (no leading /)
  2. /boot/grub/grub.cfg-01-<MAC-dashes> (leading /, 01- prefix, dashes)
  3. /boot/grub/grub.cfg-01-<MAC-dashes>.sig (signature file)
  4. /boot/grub/grub.cfg-<HEX-IP> (progressively shorter hex IP variants)
  5. /boot/grub/grub.cfg (bare fallback)
  6. command.lst, fs.lst, crypto.lst, terminal.lst (normal mode files)
Steps 4-6 are expected fallthrough behavior when step 2-3 succeed. Steps 4-5 never match (no files at those paths by design), and step 6 files are optional (non-fatal 404s).

Disk Space Planning

Each Nodegrid OS version consumes approximately 1GB of disk space, dominated by rootfs_main.img.gz. Plan storage accordingly:

VersionsApproximate Disk Usage
1~1 GB
2~2 GB
5~5 GB
10~10 GB

The TFTP root filesystem should have sufficient free space for all versions you intend to serve simultaneously.

6. Appendix B: Reference Configurations

Complete Kea Config -- Lab (Single Version)

{
"Dhcp4": {
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{ "output": "/var/log/kea/kea-dhcp4.log" }
],
"severity": "DEBUG",
"debuglevel": 55
}
],

"interfaces-config": {
"interfaces": ["<IFACE>"]
},

"lease-database": {
"type": "memfile",
"persist": true,
"name": "/var/lib/kea/kea-leases4.csv"
},

"valid-lifetime": 600,
"renew-timer": 300,
"rebind-timer": 525,

"subnet4": [
{
"id": 1,
"subnet": "<PXE_SUBNET>",
"pools": [
{ "pool": "<POOL_START> - <POOL_END>" }
],
"option-data": [
{ "name": "routers", "data": "<GATEWAY>" },
{ "name": "domain-name-servers", "data": "<DNS_SERVER>" },
{ "name": "domain-name", "data": "<DOMAIN>" }
],
"reservations": [
{
"hw-address": "<DEVICE_MAC>",
"ip-address": "<DEVICE_IP>",
"next-server": "<SERVER_IP>",
"boot-file-name": "boot/grub/i386-pc/core.0"
}
]
}
]
}
}

Complete Kea Config -- Scalable (Multi-Version, Mixed BIOS + UEFI)

{
"Dhcp4": {
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{ "output": "/var/log/kea/kea-dhcp4.log" }
],
"severity": "DEBUG",
"debuglevel": 55
}
],

"interfaces-config": {
"interfaces": ["<IFACE>"]
},

"lease-database": {
"type": "memfile",
"persist": true,
"name": "/var/lib/kea/kea-leases4.csv"
},

"valid-lifetime": 600,
"renew-timer": 300,
"rebind-timer": 525,

"subnet4": [
{
"id": 1,
"subnet": "<PXE_SUBNET>",
"pools": [
{ "pool": "<POOL_START> - <POOL_END>" }
],
"option-data": [
{ "name": "routers", "data": "<GATEWAY>" },
{ "name": "domain-name-servers", "data": "<DNS_SERVER>" },
{ "name": "domain-name", "data": "<DOMAIN>" }
],
"reservations": [
{
"hw-address": "<BIOS_DEVICE_MAC>",
"ip-address": "<BIOS_DEVICE_IP>",
"next-server": "<SERVER_IP>",
"boot-file-name": "boot/grub/i386-pc/core.0-<bios-device-mac-dashes>"
},
{
"hw-address": "<UEFI_DEVICE_MAC>",
"ip-address": "<UEFI_DEVICE_IP>",
"next-server": "<SERVER_IP>",
"boot-file-name": "boot/grub/x86_64-efi/core.efi-<uefi-device-mac-dashes>"
}
]
}
]
}
}

Note: next-server is the same for all reservations (the TFTP server IP). boot-file-name is a stable per-MAC path that never changes after provisioning -- version switching retargets TFTP symlinks server-side and does not require a Kea config edit or restart. The boot mode (BIOS vs UEFI) is encoded in the boot-file-name path: i386-pc/core.0-<mac> for BIOS, x86_64-efi/core.efi-<mac> for UEFI.

TFTP Directory Tree -- Multi-Version

<TFTP_ROOT>/
├── ng6-0-13/ Real files (version 6.0.13)
│ ├── boot/
│ │ └── grub/
│ │ ├── grub.cfg Signed
│ │ ├── grub.cfg.sig
│ │ ├── fonts/
│ │ ├── i386-pc/
│ │ │ └── core.0 GRUB binary — BIOS (~205KB)
│ │ └── x86_64-efi/
│ │ └── core.efi GRUB binary — UEFI (~813KB)
│ └── nodegrid-6.0.13/
│ ├── version "6.0.13"
│ ├── vmlinuz, vmlinuz.sig
│ ├── initrd, initrd.sig
│ └── rootfs_main.img.gz, rootfs_main.img.gz.sig

├── ng6-0-36/ Real files (version 6.0.36)
│ ├── boot/
│ │ └── grub/
│ │ ├── grub.cfg Signed
│ │ ├── grub.cfg.sig
│ │ ├── fonts/
│ │ ├── i386-pc/
│ │ │ └── core.0 GRUB binary — BIOS (~214KB)
│ │ └── x86_64-efi/
│ │ └── core.efi GRUB binary — UEFI (~911KB)
│ └── nodegrid-6.0.36/
│ ├── version "6.0.36"
│ ├── vmlinuz, vmlinuz.sig
│ ├── initrd, initrd.sig
│ └── rootfs_main.img.gz, rootfs_main.img.gz.sig

├── boot/
│ └── grub/ GRUB routing directory
│ ├── i386-pc/ BIOS per-MAC boot symlinks
│ │ └── core.0-e4-1a-2c-00-35-32 → ../../../ng6-0-36/boot/grub/i386-pc/core.0
│ ├── x86_64-efi/ UEFI per-MAC boot symlinks
│ │ └── core.efi-e4-1a-2c-01-5c-fb → ../../../ng6-0-36/boot/grub/x86_64-efi/core.efi
│ ├── grub.cfg-01-e4-1a-2c-00-35-32 → ../../ng6-0-36/boot/grub/grub.cfg (BIOS device)
│ ├── grub.cfg-01-e4-1a-2c-01-5c-fb → ../../ng6-0-36/boot/grub/grub.cfg (UEFI device)
│ ├── grub.cfg-01-e4-1a-2c-01-5c-fb.sig → ../../ng6-0-36/boot/grub/grub.cfg.sig (UEFI .sig)
│ └── (no default grub.cfg — intentionally absent)

├── nodegrid-6.0.13 → ng6-0-13/nodegrid-6.0.13 (TFTP fallback symlink)
└── nodegrid-6.0.36 → ng6-0-36/nodegrid-6.0.36 (TFTP fallback symlink)

HTTP Docroot Tree -- Multi-Version

<HTTP_DOCROOT>/
├── nodegrid-6.0.13 → <TFTP_ROOT>/ng6-0-13/nodegrid-6.0.13 (absolute symlink)
├── nodegrid-6.0.36 → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36 (absolute symlink)
├── boot → <TFTP_ROOT>/ng6-0-36/boot (absolute symlink, optional fallback)

├── e4-1a-2c-00-35-32/ Per-MAC directory (BIOS device, targeting v6.0.36)
│ ├── version → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/version
│ ├── vmlinuz → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/vmlinuz
│ ├── initrd → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/initrd
│ └── rootfs_main.img.gz → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/rootfs_main.img.gz

├── e4:1a:2c:00:35:32 → e4-1a-2c-00-35-32 Colon-format symlink (GRUB ${net_default_mac})

├── e4-1a-2c-01-5c-fb/ Per-MAC directory (UEFI device, targeting v6.0.36)
│ ├── version → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/version
│ ├── vmlinuz → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/vmlinuz
│ ├── initrd → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/initrd
│ ├── rootfs_main.img.gz → <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/rootfs_main.img.gz
│ └── reformat Optional sentinel (0-byte, triggers clean install)

└── e4:1a:2c:01:5c:fb → e4-1a-2c-01-5c-fb Colon-format symlink (GRUB ${net_default_mac})

7. Appendix C: PXE Boot Chain -- Detailed Walkthrough

This appendix traces the complete PXE boot sequence from power-on to OS install, identifying every network transaction and decision point. The BIOS walkthrough is shown first, followed by UEFI-specific differences.

BIOS Boot Chain

Stage 1: DHCP (UDP 67/68)

Device NIC (Intel PXE ROM):
1. Sends DHCPDISCOVER broadcast (includes MAC address in chaddr field)

Kea DHCP server:
2. Matches MAC to a host reservation
3. Sends DHCPOFFER with:
- ip-address: <DEVICE_IP> (from reservation)
- next-server: <SERVER_IP> (TFTP server, from reservation)
- boot-file-name: boot/grub/i386-pc/core.0-e4-1a-2c-00-35-32
(stable per-MAC symlink path, never changes after provisioning)
- Standard options: router, DNS, domain name (from subnet config)

Device NIC:
4. Sends DHCPREQUEST (accepting the offer)

Kea:
5. Sends DHCPACK (confirming the lease)

Stage 2: TFTP -- Bootloader Download (UDP 69)

Device NIC (PXE ROM):
6. Downloads boot-file-name from next-server via TFTP
GET boot/grub/i386-pc/core.0-e4-1a-2c-00-35-32
-> Resolves to <TFTP_ROOT>/boot/grub/i386-pc/core.0-e4-1a-2c-00-35-32
-> Symlink: ../../../ng6-0-36/boot/grub/i386-pc/core.0
-> Real file: <TFTP_ROOT>/ng6-0-36/boot/grub/i386-pc/core.0
-> ~214KB GRUB binary

7. Transfers control to GRUB

Stage 3: TFTP -- GRUB Config Discovery (UDP 69)

GRUB (running on device):
8. Hardcoded prefix: /boot/grub/
9. Searches for config via TFTP (in order):
GET /boot/grub/grub.cfg-01-e4-1a-2c-00-35-32
-> Resolves to <TFTP_ROOT>/boot/grub/grub.cfg-01-e4-1a-2c-00-35-32
-> Symlink: ../../ng6-0-36/boot/grub/grub.cfg
-> Real file: <TFTP_ROOT>/ng6-0-36/boot/grub/grub.cfg
-> FOUND (stops searching)

10. Optional module list lookups (all return 404, non-fatal):
GET /boot/grub/i386-pc/command.lst -> 404
GET /boot/grub/i386-pc/fs.lst -> 404
GET /boot/grub/i386-pc/crypto.lst -> 404
GET /boot/grub/i386-pc/terminal.lst -> 404

Stage 4: HTTP -- Version Probing and OS File Download (TCP 80)

GRUB (executing grub.cfg):
11. Probes for per-MAC version (HTTP):
GET /e4-1a-2c-00-35-32/version -> 200
-> Resolves to <HTTP_DOCROOT>/e4-1a-2c-00-35-32/version
-> symlink to <TFTP_ROOT>/ng6-0-36/nodegrid-6.0.36/version

12. Version comparison:
Compare network version (6.0.36) vs disk version
-> If different: proceed to install
-> If same: chainload disk GRUB, skip install

13. Check reformat flag (HTTP):
GET /e4-1a-2c-00-35-32/reformat -> 200 (file exists) or 404 (absent)
-> 200: set LABEL=pxeboot-format (clean install)
-> 404: set LABEL=pxeboot (upgrade)

14. Set signature enforcement:
set check_signatures=enforce

15. Load kernel (HTTP):
GET /e4-1a-2c-00-35-32/vmlinuz -> 200

16. Load initrd (HTTP):
GET /e4-1a-2c-00-35-32/initrd -> 200

17. Boot kernel with command line including LABEL=pxeboot or pxeboot-format

Stage 5: OS Installation

Linux kernel (running on device):
18. Boots with initrd
19. Downloads rootfs (HTTP):
wget http://<SERVER_IP>/e4-1a-2c-00-35-32/rootfs_main.img.gz
20. Writes OS to disk
21. Reboots into installed Nodegrid OS

UEFI Boot Chain -- Differences from BIOS

The UEFI boot chain follows the same five stages. The key differences are:

Stage 1: DHCP

  1. boot-file-name uses the UEFI path: boot/grub/x86_64-efi/core.efi-e4-1a-2c-01-5c-fb

Stage 2: TFTP -- Bootloader Download

  1. The PXE ROM downloads core.efi (~800-900KB) instead of core.0 (~200KB)
  2. The TFTP request path has no leading /: boot/grub/x86_64-efi/core.efi-e4-1a-2c-01-5c-fb

Stage 3: TFTP -- GRUB Config Discovery

  1. UEFI GRUB requests each config candidate and its .sig file:
    GET /boot/grub/grub.cfg-01-e4-1a-2c-01-5c-fb       -> 200 (config)
    GET /boot/grub/grub.cfg-01-e4-1a-2c-01-5c-fb.sig -> 200 (signature)
  2. If the .sig file returns 404, the config is rejected and GRUB falls through to the next candidate (hex-IP, then default, then grub> prompt)
  3. Module list lookups use x86_64-efi/ instead of i386-pc/

Stage 4: HTTP -- Version Probing and OS File Download

  1. GRUB's ${net_default_mac} variable uses colons (e.g., e4:1a:2c:01:5c:fb), so HTTP requests use the colon-format path:
    GET /e4:1a:2c:01:5c:fb/version                     -> 200
  2. The colon-format symlink in the HTTP docroot resolves this to the dash-format per-MAC directory: <HTTP_DOCROOT>/e4:1a:2c:01:5c:fbe4-1a-2c-01-5c-fb
  3. All subsequent HTTP requests (reformat check, kernel, initrd, rootfs) also use the colon-format MAC

Network Ports Summary

StageProtocolPort(s)Direction
DHCPUDP67 (server), 68 (client)Broadcast then unicast
TFTPUDP69 (initial), ephemeral (data)Unicast
HTTPTCP80Unicast

Ensure firewall rules allow all three protocols between the device subnet and the PXE server.

    • Related Articles

    • How to enable TFTP services on Nodegrid

      To enable the TFTP server, you need to edit the tftpd configuration file as it is disabled by default. (as root) vi /etc/xinetd.d/tftpd And change the disable attribute to “no”: service tftp { disable = yes <<<==== change this to no socket_type = ...
    • Nodegrid Manager Installation in ESXi 5

      Nodegrid Manager software is installed from an ISO file. The installation procedure is a three-stage process:  Creating a virtual machine; Booting from the ISO file in order to install the software; Restarting and booting from the newly created ...
    • Nodegrid Manager Installation in ESXi 6

      Nodegrid Manager software is installed from an ISO file. The installation procedure is a three-stage process:  Creating a virtual machine; Booting from the ISO file in order to install the software; Restarting and booting from the newly created ...
    • DHCP Server Lease Management

      Nodegrid version 5.10.x provides better visibility into your DHCP network and offers a new feature ease management of your DHCP network. You can find these improvements under Tracking-->Network-->DHCP. The DHCP section here is now split between ...
    • How to Configure Nodegrid Serial Ports

      To configure the serial ports of your Nodegrid Serial Console, follow the guideline steps below.   WebUI Log in as admin to the Nodegrid Serial Console Web interface. Go to Managed Devices page. Select the serial ports you want to configure, or check ...