From 2bfdf0215410640519a2d3e00d2c1d1c6f38badf Mon Sep 17 00:00:00 2001 From: Rui Lopes Date: Sat, 9 Oct 2021 22:38:05 +0100 Subject: [PATCH] add support for UEFI HTTP Boot this closes https://github.com/tinkerbell/boots/issues/210 Signed-off-by: Rui Lopes --- dhcp/pxe.go | 40 ++++++++++++++++++++++++++++------------ ipxe/dhcp_options.go | 1 + job/dhcp.go | 11 ++++------- job/dhcp_test.go | 28 ++++++++++++++++------------ job/http.go | 17 +++++++++++++++-- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/dhcp/pxe.go b/dhcp/pxe.go index 6ce6048a7..9e379cc64 100644 --- a/dhcp/pxe.go +++ b/dhcp/pxe.go @@ -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", @@ -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", @@ -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 @@ -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") } @@ -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. diff --git a/ipxe/dhcp_options.go b/ipxe/dhcp_options.go index 04ccbb7c5..977061c4a 100644 --- a/ipxe/dhcp_options.go +++ b/ipxe/dhcp_options.go @@ -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) diff --git a/job/dhcp.go b/job/dhcp.go index 980815a46..a43d659b7 100644 --- a/job/dhcp.go +++ b/job/dhcp.go @@ -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") } @@ -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")) @@ -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" @@ -162,11 +161,9 @@ 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" + filename = "auto.ipxe" } if filename == "" { @@ -176,5 +173,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) } diff --git a/job/dhcp_test.go b/job/dhcp_test.go index e00a3acae..2b8568e0e 100644 --- a/job/dhcp_test.go +++ b/job/dhcp_test.go @@ -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"}, @@ -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", @@ -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 { diff --git a/job/http.go b/job/http.go index 12b4ee8f4..fa63a62af 100644 --- a/job/http.go +++ b/job/http.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "net/http" + "os" "path" "strings" @@ -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) {