Skip to content

Commit

Permalink
Merge pull request #216 from rgl/rgl-http-boot
Browse files Browse the repository at this point in the history
add support for UEFI HTTP Boot
  • Loading branch information
tstromberg committed Nov 16, 2021
2 parents f42cbcc + ceb470a commit a580a44
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 92 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
/cmd/boots/boots
/cmd/boots/boots-*-*
coverage.txt
tftp/ipxe/
ipxe/ipxe/*.efi
ipxe/ipxe/*.kpxe
!deploy/stack/.env
.vagrant
deploy/stack/state/webroot/misc/osie/current/*
Expand Down
5 changes: 4 additions & 1 deletion cmd/boots/tftp.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ func (t tftpHandler) ReadFile(c tftp.Conn, filename string) (tftp.ReadCloser, er
defer metrics.JobsInProgress.With(labels).Dec()

ip := tftpClientIP(c.RemoteAddr())
filename = path.Base(filename)
filename = path.Clean(filename)
if path.IsAbs(filename) {
filename = filename[1:]
}
l := mainlog.With("client", ip.String(), "event", "open", "filename", filename)

// clients can send traceparent over TFTP by appending the traceparent string
Expand Down
48 changes: 32 additions & 16 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,18 +134,23 @@ 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) {
rep.SetSIAddr(nextServer) // next-server: IP address of the TFTP Server.

if httpClient {
filename = "http://" + httpServerFQDN + "/" + filename
rep.SetString(dhcp4.OptionClassID, "HTTPClient")
} else {
rep.SetString(dhcp4.OptionClassID, "PXEClient")
}

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 {
rep.SetString(dhcp4.OptionClassID, "PXEClient")
}
rep.SetSIAddr(nextServer) // next-server: IP address of the TFTP/HTTP Server.
copy(file, filename) // filename: Executable (or iPXE script) to boot from.
copy(file, filename) // filename: Executable (or iPXE script) to boot from.
}

func copyGUID(rep, req *dhcp4.Packet) bool {
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
9 changes: 9 additions & 0 deletions ipxe/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ipxe

import "embed"

//go:embed ipxe/ipxe.efi
//go:embed ipxe/snp-hua.efi
//go:embed ipxe/snp-nolacp.efi
//go:embed ipxe/undionly.kpxe
var Files embed.FS
20 changes: 9 additions & 11 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,16 +139,15 @@ 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"
filename = "ipxe/snp-hua.efi"
} else if isARM {
filename = "snp-nolacp.efi"
filename = "ipxe/snp-nolacp.efi"
} else if isUEFI {
filename = "ipxe.efi"
filename = "ipxe/ipxe.efi"
} else {
filename = "undionly.kpxe"
filename = "ipxe/undionly.kpxe"
}
} else if !j.isPXEAllowed() {
// Always honor allow_pxe.
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)
}
42 changes: 23 additions & 19 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 @@ -39,20 +40,23 @@ func TestSetPXEFilename(t *testing.T) {
hState: "in_use", id: "$instance_id", iState: "active", slug: "not_custom_ipxe"},
{name: "active custom ipxe",
hState: "in_use", id: "$instance_id", iState: "active", slug: "custom_ipxe",
filename: "undionly.kpxe"},
filename: "ipxe/undionly.kpxe"},
{name: "active custom ipxe with allow pxe",
hState: "in_use", id: "$instance_id", iState: "active", allowPXE: true,
filename: "undionly.kpxe"},
filename: "ipxe/undionly.kpxe"},
{name: "hua",
plan: "hua", filename: "snp-hua.efi"},
plan: "hua", filename: "ipxe/snp-hua.efi"},
{name: "2a2",
plan: "2a2", filename: "snp-hua.efi"},
plan: "2a2", filename: "ipxe/snp-hua.efi"},
{name: "arm",
arm: true, filename: "snp-nolacp.efi"},
arm: true, filename: "ipxe/snp-nolacp.efi"},
{name: "x86 uefi",
uefi: true, filename: "ipxe.efi"},
uefi: true, filename: "ipxe/ipxe.efi"},
{name: "x86 uefi http client",
uefi: true, allowPXE: true, httpClient: true,
filename: "http://" + conf.PublicFQDN + "/ipxe/ipxe.efi"},
{name: "all defaults",
filename: "undionly.kpxe"},
filename: "ipxe/undionly.kpxe"},
{name: "packet iPXE",
packet: true, filename: "/nonexistent"},
{name: "packet iPXE PXE allowed",
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
10 changes: 10 additions & 0 deletions job/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,33 @@ package job
import (
"context"
"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")
ipxeFilesHandler = http.FileServer(http.FS(ipxe.Files))
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
16 changes: 8 additions & 8 deletions rules.mk
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ generated_go_files := \
.PHONY: $(generated_go_files)

# build all the ipxe binaries
generated_ipxe_files := tftp/ipxe/ipxe.efi tftp/ipxe/snp-hua.efi tftp/ipxe/snp-nolacp.efi tftp/ipxe/undionly.kpxe tftp/ipxe/snp-hua.efi
generated_ipxe_files := ipxe/ipxe/ipxe.efi ipxe/ipxe/snp-hua.efi ipxe/ipxe/snp-nolacp.efi ipxe/ipxe/undionly.kpxe ipxe/ipxe/snp-hua.efi

# go generate
go_generate:
Expand All @@ -71,15 +71,15 @@ include ipxev.mk
ipxeconfigs := $(wildcard ipxe/ipxe/*.h)

# copy ipxe binaries into location available for go embed
tftp/ipxe/ipxe.efi: ipxe/ipxe/build/bin-x86_64-efi/ipxe.efi
tftp/ipxe/snp-nolacp.efi: ipxe/ipxe/build/bin-arm64-efi/snp.efi
tftp/ipxe/undionly.kpxe: ipxe/ipxe/build/bin/undionly.kpxe
tftp/ipxe/ipxe.efi tftp/ipxe/snp-nolacp.efi tftp/ipxe/undionly.kpxe:
mkdir -p tftp/ipxe
ipxe/ipxe/ipxe.efi: ipxe/ipxe/build/bin-x86_64-efi/ipxe.efi
ipxe/ipxe/snp-nolacp.efi: ipxe/ipxe/build/bin-arm64-efi/snp.efi
ipxe/ipxe/undionly.kpxe: ipxe/ipxe/build/bin/undionly.kpxe
ipxe/ipxe/ipxe.efi ipxe/ipxe/snp-nolacp.efi ipxe/ipxe/undionly.kpxe:
mkdir -p ipxe/ipxe
cp $^ $@

tftp/ipxe/snp-hua.efi:
mkdir -p tftp/ipxe
ipxe/ipxe/snp-hua.efi:
mkdir -p ipxe/ipxe
# we dont build the snp-hua.efi binary. It's checked into git, so here we just copy it over
cp ipxe/bin/snp-hua.efi $@

Expand Down
Loading

0 comments on commit a580a44

Please sign in to comment.