Skip to content

Commit

Permalink
add support for UEFI HTTP Boot
Browse files Browse the repository at this point in the history
this closes tinkerbell#210

Signed-off-by: Rui Lopes <rgl@ruilopes.com>
  • Loading branch information
rgl committed Oct 10, 2021
1 parent be5a67e commit 6ae0a7e
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 23 deletions.
39 changes: 28 additions & 11 deletions dhcp/pxe.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ import (

dhcp4 "github.com/packethost/dhcp4-go"
"github.com/pkg/errors"
"github.com/tinkerbell/boots/ipxe"
"go.opentelemetry.io/otel/trace"
)

// from https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml
var procArchTypes = []string{
"x86 BIOS",
"x86 BIOS", // #0 x86_64
"NEC/PC98 (DEPRECATED)",
"Itanium",
"DEC Alpha (DEPRECATED)",
"Arc x86 (DEPRECATED)",
"Intel Lean Client (DEPRECATED)",
"x86 UEFI",
"x64 UEFI",
"x64 UEFI", // #7 x86_64
"EFI Xscale (DEPRECATED)",
"EBC",
"ARM 32-bit UEFI",
Expand All @@ -28,10 +29,10 @@ var procArchTypes = []string{
"PowerPC ePAPR",
"POWER OPAL v3",
"x86 uefi boot from http",
"x64 uefi boot from http",
"x64 uefi boot from http", // #16 x86_64
"ebc boot from http",
"arm uefi 32 boot from http",
"arm uefi 64 boot from http",
"arm uefi 64 boot from http", // #19 aarch64
"pc/at bios boot from http",
"arm 32 uboot",
"arm 64 uboot",
Expand Down Expand Up @@ -59,9 +60,9 @@ func ProcessorArchType(req *dhcp4.Packet) string {
func Arch(req *dhcp4.Packet) string {
arch := ProcessorArchType(req)
switch arch {
case "x86 BIOS", "x64 UEFI":
case "x86 BIOS", "x64 UEFI", "x64 uefi boot from http":
return "x86_64"
case "ARM 64-bit UEFI":
case "ARM 64-bit UEFI", "arm uefi 64 boot from http":
return "aarch64"
default:
return arch
Expand All @@ -83,14 +84,25 @@ func IsPXE(req *dhcp4.Packet) bool {
}
class, ok := req.GetString(dhcp4.OptionClassID)

return ok && strings.HasPrefix(class, "PXEClient")
return ok && (strings.HasPrefix(class, "PXEClient") || strings.HasPrefix(class, "HTTPClient"))
}

func IsHTTPClient(req *dhcp4.Packet) bool {
if ipxe.IsIPXE(req) {
return true
}

classID, ok := req.GetString(dhcp4.OptionClassID)

return ok && strings.HasPrefix(classID, "HTTPClient")
}

func SetupPXE(ctx context.Context, rep, req *dhcp4.Packet) bool {
classID, hasClassID := req.GetString(dhcp4.OptionClassID)
if !hasClassID || !(strings.HasPrefix(classID, "PXEClient") || strings.HasPrefix(classID, "HTTPClient")) {
return false // not a PXE client
}
if !copyGUID(rep, req) {
if class, ok := req.GetString(dhcp4.OptionClassID); !ok || !strings.HasPrefix(class, "PXEClient") {
return false // not a PXE client
}
dhcplog.With("mac", req.GetCHAddr(), "xid", req.GetXID()).Info("no client GUID provided")
}

Expand All @@ -100,7 +112,12 @@ func SetupPXE(ctx context.Context, rep, req *dhcp4.Packet) bool {
// - Section 2.2.2 paragraph 2
// - Table 2-3
// and surroundings
rep.SetString(dhcp4.OptionClassID, "PXEClient")
isPXEClient := strings.HasPrefix(classID, "PXEClient")
if isPXEClient {
rep.SetString(dhcp4.OptionClassID, "PXEClient")
} else {
rep.SetString(dhcp4.OptionClassID, "HTTPClient")
}

/*
Intel's Preboot Execution Environment (PXE) Specification (1999):
Expand Down
1 change: 1 addition & 0 deletions ipxe/dhcp_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func IsOuriPXE(req *dhcp4.Packet) bool {
// TODO: make this actually check for iPXE and use ipxe' build system's ability to set name.
// This way we could set to something like "Boots iPXE" and then just look for that in the identifier sent in dhcp.
// This also means we won't lose ipxe's version number for logging and such.
// see https://ipxe.org/appnote/userclass
if om := GetEncapsulatedOptions(req); om != nil {
if ov, ok := om.GetOption(OptionVersion); ok {
return ok && bytes.Equal(ov, ouriPXEVersion)
Expand Down
9 changes: 7 additions & 2 deletions job/dhcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ func (j Job) configureDHCP(ctx context.Context, rep, req *dhcp4.Packet) bool {
ipxe.Setup(rep)
}

filename := j.getPXEFilename(arch, firmware, isOuriPXE)
isHTTPClient := dhcp.IsHTTPClient(req)

filename := j.getPXEFilename(arch, firmware, isHTTPClient, isOuriPXE)
if filename == "" {
err := errors.New("no filename is set")
j.Error(err)
Expand Down Expand Up @@ -133,7 +135,7 @@ func (j Job) areWeProvisioner() bool {
return j.hardware.HardwareProvisioner() == j.ProvisionerEngineName()
}

func (j Job) getPXEFilename(arch, firmware string, isOuriPXE bool) string {
func (j Job) getPXEFilename(arch, firmware string, isHTTPClient bool, isOuriPXE bool) string {
if !j.isPXEAllowed() {
if j.instance != nil && j.instance.State == "active" {
// We set a filename because if a machine is actually trying to PXE and nothing is sent it may hang for
Expand All @@ -157,6 +159,9 @@ func (j Job) getPXEFilename(arch, firmware string, isOuriPXE bool) string {
case arch == "x86" && firmware == "bios":
filename = "undionly.kpxe"
}
if isHTTPClient {
filename = "http://" + conf.PublicFQDN + "/" + filename
}
} else {
filename = "http://" + conf.PublicFQDN + "/auto.ipxe"
}
Expand Down
20 changes: 12 additions & 8 deletions job/dhcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ func TestGetPXEFilename(t *testing.T) {
conf.PublicFQDN = "boots-testing.packet.net"

var getPXEFilenameTests = []struct {
name string
iState string
allowPXE bool
ouriPXE bool
arch string
firmware string
filename string
name string
iState string
allowPXE bool
httpClient bool
ouriPXE bool
arch string
firmware string
filename string
}{
{name: "inactive instance",
iState: "not_active"},
Expand Down Expand Up @@ -48,6 +49,9 @@ func TestGetPXEFilename(t *testing.T) {
{name: "x86 uefi",
arch: "x86", firmware: "uefi", allowPXE: true,
filename: "ipxe.efi"},
{name: "x86 uefi http client",
arch: "x86", firmware: "uefi", allowPXE: true, httpClient: true,
filename: "http://" + conf.PublicFQDN + "/ipxe.efi"},
{name: "unknown arch",
arch: "riscv", allowPXE: true},
{name: "unknown firmware",
Expand All @@ -72,7 +76,7 @@ func TestGetPXEFilename(t *testing.T) {
},
instance: instance,
}
filename := j.getPXEFilename(tt.arch, tt.firmware, tt.ouriPXE)
filename := j.getPXEFilename(tt.arch, tt.firmware, tt.httpClient, tt.ouriPXE)
if tt.filename != filename {
t.Fatalf("unexpected filename want:%q, got:%q", tt.filename, filename)
}
Expand Down
17 changes: 15 additions & 2 deletions job/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strings"

Expand All @@ -21,8 +22,20 @@ func (j Job) ServeFile(w http.ResponseWriter, req *http.Request, i Installers) {
return
}

w.WriteHeader(http.StatusNotFound)
j.With("file", base).Info("file not found")
// serve iPXE to HTTP clients.
f, err := j.ServeTFTP(base, req.RemoteAddr)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
j.With("file", base).Info("file not found")
} else {
w.WriteHeader(http.StatusInternalServerError)
}

return
}
defer f.Close()
_, _ = io.Copy(w, f)
}

func (j Job) ServePhoneHomeEndpoint(w http.ResponseWriter, req *http.Request) {
Expand Down

0 comments on commit 6ae0a7e

Please sign in to comment.