Skip to content

Commit

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

Signed-off-by: Rui Lopes <rgl@ruilopes.com>
  • Loading branch information
rgl committed Nov 4, 2021
1 parent f42cbcc commit a52ce86
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 67 deletions.
40 changes: 28 additions & 12 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,24 @@ 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 {
if !IsPXE(req) {
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 Down Expand Up @@ -123,14 +134,19 @@ func SetupPXE(ctx context.Context, rep, req *dhcp4.Packet) bool {
return true
}

func SetFilename(rep *dhcp4.Packet, filename string, nextServer net.IP, pxeClient bool) {
func SetFilename(rep *dhcp4.Packet, filename string, nextServer net.IP, httpServerFQDN string, httpClient bool) {
if httpClient {
filename = "http://" + httpServerFQDN + "/" + filename
}
file := rep.File()
if len(filename) > len(file) {
err := errors.New("filename too long, would be truncated")
// req CHaddr and XID == req's
dhcplog.With("mac", rep.GetCHAddr(), "xid", rep.GetXID(), "filename", filename).Fatal(err)
}
if pxeClient {
if httpClient {
rep.SetString(dhcp4.OptionClassID, "HTTPClient")
} else {
rep.SetString(dhcp4.OptionClassID, "PXEClient")
}
rep.SetSIAddr(nextServer) // next-server: IP address of the TFTP/HTTP Server.
Expand Down
1 change: 1 addition & 0 deletions ipxe/dhcp_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func IsPacketIPXE(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 "Packet 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, packetVersion)
Expand Down
11 changes: 11 additions & 0 deletions ipxe/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ipxe

import (
"embed"
)

//go:embed ipxe/undionly.kpxe
//go:embed ipxe/ipxe.efi
//go:embed ipxe/snp-nolacp.efi
//go:embed ipxe/snp-hua.efi
var Files embed.FS
12 changes: 5 additions & 7 deletions job/dhcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (j Job) configureDHCP(ctx context.Context, rep, req *dhcp4.Packet) bool {
ipxe.Setup(rep)
}

j.setPXEFilename(rep, isPacket, isARM, isUEFI)
j.setPXEFilename(rep, isPacket, isARM, isUEFI, dhcp.IsHTTPClient(req))
} else {
span.AddEvent("did not SetupPXE because packet is not a PXE request")
}
Expand All @@ -113,7 +113,7 @@ func (j Job) areWeProvisioner() bool {
return j.hardware.HardwareProvisioner() == j.ProvisionerEngineName()
}

func (j Job) setPXEFilename(rep *dhcp4.Packet, isPacket, isARM, isUEFI bool) {
func (j Job) setPXEFilename(rep *dhcp4.Packet, isPacket, isARM, isUEFI, isHTTPClient bool) {
if j.HardwareState() == "in_use" {
if j.InstanceID() == "" {
j.Error(errors.New("setPXEFilename called on a job with no instance"))
Expand All @@ -139,7 +139,6 @@ func (j Job) setPXEFilename(rep *dhcp4.Packet, isPacket, isARM, isUEFI bool) {
}

var filename string
var pxeClient bool
if !isPacket {
if j.PArch() == "hua" || j.PArch() == "2a2" {
filename = "snp-hua.efi"
Expand All @@ -162,11 +161,10 @@ func (j Job) setPXEFilename(rep *dhcp4.Packet, isPacket, isARM, isUEFI bool) {

os := j.OperatingSystem()
j.With("instance.state", j.instance.State, "os_slug", os.Slug, "os_distro", os.Distro, "os_version", os.Version).Info()
pxeClient = true
filename = "/nonexistent"
} else {
pxeClient = true
filename = "http://" + conf.PublicFQDN + "/auto.ipxe"
isHTTPClient = true
filename = "auto.ipxe"
}

if filename == "" {
Expand All @@ -176,5 +174,5 @@ func (j Job) setPXEFilename(rep *dhcp4.Packet, isPacket, isARM, isUEFI bool) {
return
}

dhcp.SetFilename(rep, filename, conf.PublicIPv4, pxeClient)
dhcp.SetFilename(rep, filename, conf.PublicIPv4, conf.PublicFQDN, isHTTPClient)
}
28 changes: 16 additions & 12 deletions job/dhcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ func TestSetPXEFilename(t *testing.T) {
conf.PublicFQDN = "boots-testing.packet.net"

var setPXEFilenameTests = []struct {
name string
hState string
id string
iState string
slug string
plan string
allowPXE bool
packet bool
arm bool
uefi bool
filename string
name string
hState string
id string
iState string
slug string
plan string
allowPXE bool
httpClient bool
packet bool
arm bool
uefi bool
filename string
}{
{name: "just in_use",
hState: "in_use"},
Expand All @@ -51,6 +52,9 @@ func TestSetPXEFilename(t *testing.T) {
arm: true, filename: "snp-nolacp.efi"},
{name: "x86 uefi",
uefi: true, filename: "ipxe.efi"},
{name: "x86 uefi http client",
uefi: true, allowPXE: true, httpClient: true,
filename: "http://" + conf.PublicFQDN + "/ipxe.efi"},
{name: "all defaults",
filename: "undionly.kpxe"},
{name: "packet iPXE",
Expand Down Expand Up @@ -87,7 +91,7 @@ func TestSetPXEFilename(t *testing.T) {
instance: instance,
}
rep := dhcp4.NewPacket(42)
j.setPXEFilename(&rep, tt.packet, tt.arm, tt.uefi)
j.setPXEFilename(&rep, tt.packet, tt.arm, tt.uefi, tt.httpClient)
filename := string(bytes.TrimRight(rep.File(), "\x00"))

if tt.filename != filename {
Expand Down
6 changes: 4 additions & 2 deletions job/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ 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.
// NB this must handle HEAD/GET and return Content-Length for odd clients
// like the Seeed Studio Odyssey X86J4105 board.
ipxeFilesHandler.ServeHTTP(w, req)
}

func (j Job) ServePhoneHomeEndpoint(w http.ResponseWriter, req *http.Request) {
Expand Down
12 changes: 12 additions & 0 deletions job/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,36 @@ package job

import (
"context"
"io/fs"
"net"
"net/http"
"os"
"time"

"github.com/packethost/pkg/log"
"github.com/pkg/errors"
"github.com/tinkerbell/boots/conf"
"github.com/tinkerbell/boots/dhcp"
"github.com/tinkerbell/boots/ipxe"
"github.com/tinkerbell/boots/packet"
tw "github.com/tinkerbell/tink/protos/workflow"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)

var joblog log.Logger
var ipxeFilesHandler http.Handler
var client packet.Client
var provisionerEngineName string

func Init(l log.Logger) {
joblog = l.Package("http")
ipxeFilesFS, _ := fs.Sub(ipxe.Files, "ipxe")
ipxeFilesHandler = http.FileServer(http.FS(ipxeFilesFS))
initRSA()
}

// SetClient sets the client used to interact with the api.
func SetClient(c packet.Client) {
client = c
Expand Down
9 changes: 0 additions & 9 deletions job/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,8 @@ package job

import (
"context"

"github.com/packethost/pkg/log"
)

var joblog log.Logger

func Init(l log.Logger) {
joblog = l.Package("http")
initRSA()
}

func (j Job) Fatal(err error, args ...interface{}) {
j.Logger.AddCallerSkip(1).Error(err, args...)
panic(err)
Expand Down
28 changes: 3 additions & 25 deletions tftp/tftp.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,14 @@
package tftp

import (
_ "embed"
"io"
"net"
"os"
"time"

"github.com/packethost/pkg/log"
"github.com/pkg/errors"
"github.com/tinkerbell/boots/ipxe"
)

//go:embed ipxe/ipxe.efi
var ipxeEFI []byte

//go:embed ipxe/undionly.kpxe
var undionly []byte

//go:embed ipxe/snp-nolacp.efi
var snpNolacp []byte

//go:embed ipxe/snp-hua.efi
var snpHua []byte

var tftpFiles = map[string][]byte{
"undionly.kpxe": undionly,
"snp-nolacp.efi": snpNolacp,
"ipxe.efi": ipxeEFI,
"snp-hua.efi": snpHua,
}

type tftpTransfer struct {
log.Logger
unread []byte
Expand All @@ -40,9 +19,8 @@ type tftpTransfer struct {
func Open(mac net.HardwareAddr, filename, client string) (*tftpTransfer, error) {
l := tftplog.With("mac", mac, "client", client, "filename", filename)

content, ok := tftpFiles[filename]
if !ok {
err := errors.Wrap(os.ErrNotExist, "unknown file")
content, err := ipxe.Files.ReadFile("ipxe/" + filename)
if err != nil {
l.With("event", "open", "error", err).Info()

return nil, err
Expand Down

0 comments on commit a52ce86

Please sign in to comment.