diff --git a/internal/pkg/configure/tftp.go b/internal/pkg/configure/tftp.go index 44e88367..0f19b326 100644 --- a/internal/pkg/configure/tftp.go +++ b/internal/pkg/configure/tftp.go @@ -1,12 +1,13 @@ package configure import ( + "fmt" "os" "path" warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/image" "github.com/warewulf/warewulf/internal/pkg/util" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" "github.com/warewulf/warewulf/internal/pkg/wwlog" "golang.org/x/sys/unix" ) @@ -35,7 +36,7 @@ func TFTP() (err error) { } if controller.Warewulf.GrubBoot() { - err := warewulfd.CopyShimGrub() + err := CopyShimGrub() if err != nil { wwlog.Warn("error when copying shim/grub binaries: %s", err) } @@ -70,3 +71,30 @@ func TFTP() (err error) { return nil } + +func CopyShimGrub() (err error) { + conf := warewulfconf.Get() + wwlog.Debug("copy shim and grub binaries from host") + shimPath := image.ShimFind("") + if shimPath == "" { + return fmt.Errorf("no shim found on the host os") + } + err = util.CopyFile(shimPath, path.Join(conf.TFTP.TftpRoot, "warewulf", "shim.efi")) + if err != nil { + return err + } + _ = os.Chmod(path.Join(conf.TFTP.TftpRoot, "warewulf", "shim.efi"), 0o755) + grubPath := image.GrubFind("") + if grubPath == "" { + return fmt.Errorf("no grub found on host os") + } + err = util.CopyFile(grubPath, path.Join(conf.TFTP.TftpRoot, "warewulf", "grub.efi")) + if err != nil { + return err + } + _ = os.Chmod(path.Join(conf.TFTP.TftpRoot, "warewulf", "grub.efi"), 0o755) + err = util.CopyFile(grubPath, path.Join(conf.TFTP.TftpRoot, "warewulf", "grubx64.efi")) + _ = os.Chmod(path.Join(conf.TFTP.TftpRoot, "warewulf", "grubx64.efi"), 0o755) + + return +} diff --git a/internal/pkg/warewulfd/copyshim.go b/internal/pkg/warewulfd/copyshim.go deleted file mode 100644 index 0ba3ea66..00000000 --- a/internal/pkg/warewulfd/copyshim.go +++ /dev/null @@ -1,45 +0,0 @@ -package warewulfd - -import ( - "fmt" - "os" - "path" - - warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" - "github.com/warewulf/warewulf/internal/pkg/wwlog" - - "github.com/warewulf/warewulf/internal/pkg/image" - "github.com/warewulf/warewulf/internal/pkg/util" -) - -/* -Copies the default shim, which is the shim located on host -to the tftp directory -*/ - -func CopyShimGrub() (err error) { - conf := warewulfconf.Get() - wwlog.Debug("copy shim and grub binaries from host") - shimPath := image.ShimFind("") - if shimPath == "" { - return fmt.Errorf("no shim found on the host os") - } - err = util.CopyFile(shimPath, path.Join(conf.TFTP.TftpRoot, "warewulf", "shim.efi")) - if err != nil { - return err - } - _ = os.Chmod(path.Join(conf.TFTP.TftpRoot, "warewulf", "shim.efi"), 0o755) - grubPath := image.GrubFind("") - if grubPath == "" { - return fmt.Errorf("no grub found on host os") - } - err = util.CopyFile(grubPath, path.Join(conf.TFTP.TftpRoot, "warewulf", "grub.efi")) - if err != nil { - return err - } - _ = os.Chmod(path.Join(conf.TFTP.TftpRoot, "warewulf", "grub.efi"), 0o755) - err = util.CopyFile(grubPath, path.Join(conf.TFTP.TftpRoot, "warewulf", "grubx64.efi")) - _ = os.Chmod(path.Join(conf.TFTP.TftpRoot, "warewulf", "grubx64.efi"), 0o755) - - return -} diff --git a/internal/pkg/warewulfd/efi.go b/internal/pkg/warewulfd/efi.go new file mode 100644 index 00000000..b853d890 --- /dev/null +++ b/internal/pkg/warewulfd/efi.go @@ -0,0 +1,60 @@ +package warewulfd + +import ( + "fmt" + "net/http" + "path" + + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleEfiBoot handles EFI boot file requests (shim, grub, grub.cfg) +func HandleEfiBoot(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + sendResponse(w, req, "", nil, ctx) + return + } + + wwlog.Debug("requested method: %s", req.Method) + imageName := ctx.remoteNode.ImageName + var stageFile string + var tmplData *templateVars + + switch ctx.rinfo.efifile { + case "shim.efi": + stageFile = image.ShimFind(imageName) + if stageFile == "" { + wwlog.Error("couldn't find shim.efi for %s", imageName) + w.WriteHeader(http.StatusNotFound) + return + } + case "grub.efi", "grub-tpm.efi", "grubx64.efi", "grubia32.efi", "grubaa64.efi", "grubarm.efi": + stageFile = image.GrubFind(imageName) + if stageFile == "" { + wwlog.Error("could't find grub*.efi for %s", imageName) + w.WriteHeader(http.StatusNotFound) + return + } + case "grub.cfg": + stageFile = path.Join(ctx.conf.Paths.Sysconfdir, "warewulf/grub/grub.cfg.ww") + tmplData = buildTemplateVars(ctx.conf, ctx.rinfo, ctx.remoteNode) + if stageFile == "" { + wwlog.Error("could't find grub.cfg template for %s", imageName) + w.WriteHeader(http.StatusNotFound) + return + } + default: + wwlog.ErrorExc(fmt.Errorf("could't find efiboot file: %s", ctx.rinfo.efifile), "") + w.WriteHeader(http.StatusNotFound) + return + } + + sendResponse(w, req, stageFile, tmplData, ctx) +} diff --git a/internal/pkg/warewulfd/grub.go b/internal/pkg/warewulfd/grub.go new file mode 100644 index 00000000..f01a43d9 --- /dev/null +++ b/internal/pkg/warewulfd/grub.go @@ -0,0 +1,33 @@ +package warewulfd + +import ( + "net/http" + + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleGrub handles direct GRUB binary requests +func HandleGrub(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + var stageFile string + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + } else { + if ctx.remoteNode.ImageName != "" { + stageFile = image.GrubFind(ctx.remoteNode.ImageName) + if stageFile == "" { + wwlog.Error("No grub found for image %s", ctx.remoteNode.ImageName) + } + } else { + wwlog.Warn("No conainer set for node %s", ctx.remoteNode.Id()) + } + } + + sendResponse(w, req, stageFile, nil, ctx) +} diff --git a/internal/pkg/warewulfd/helpers.go b/internal/pkg/warewulfd/helpers.go new file mode 100644 index 00000000..29f2062b --- /dev/null +++ b/internal/pkg/warewulfd/helpers.go @@ -0,0 +1,155 @@ +package warewulfd + +import ( + "bytes" + "fmt" + "net/http" + "net/netip" + "path" + "path/filepath" + "strconv" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/kernel" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// buildTemplateVars constructs the templateVars struct with all necessary +// fields, including handling IPv6 authority formatting and kernel version resolution. +func buildTemplateVars(conf *warewulfconf.WarewulfYaml, rinfo parsedRequest, remoteNode node.Node) *templateVars { + kernelArgs := "" + kernelVersion := "" + if remoteNode.Kernel != nil { + kernelArgs = strings.Join(remoteNode.Kernel.Args, " ") + kernelVersion = remoteNode.Kernel.Version + } + if kernelVersion == "" { + if kernel_ := kernel.FromNode(&remoteNode); kernel_ != nil { + kernelVersion = kernel_.Version() + } + } + + authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port) + ipaddr6 := "" + if confIpaddr6, err := netip.ParseAddr(conf.Ipaddr6); err == nil { + ipaddr6 = confIpaddr6.String() + } + if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil { + if rinfoIpaddr.Is6() { + if ipaddr6 != "" { + authority = fmt.Sprintf("[%s]:%d", ipaddr6, conf.Warewulf.Port) + } else { + wwlog.Error("No valid IPv6 address configured, but request is IPv6") + } + } + } else { + wwlog.Error("Could not parse request IP address: %s", rinfo.ipaddr) + } + + return &templateVars{ + Id: remoteNode.Id(), + Cluster: remoteNode.ClusterName, + Fqdn: remoteNode.Id(), + Ipaddr: conf.Ipaddr, + Ipaddr6: ipaddr6, + Port: strconv.Itoa(conf.Warewulf.Port), + TLS: conf.Warewulf.EnableTLS(), + Authority: authority, + Hostname: remoteNode.Id(), + Hwaddr: rinfo.hwaddr, + ImageName: remoteNode.ImageName, + Ipxe: remoteNode.Ipxe, + KernelArgs: kernelArgs, + KernelVersion: kernelVersion, + Root: remoteNode.Root, + NetDevs: remoteNode.NetDevs, + Tags: remoteNode.Tags} +} + +// sendResponse handles the common response logic for provision handlers. +// If tmplData is non-nil, it renders the stageFile as a template. Otherwise, it +// sends stageFile as a raw file (with optional .gz compression). +func sendResponse(w http.ResponseWriter, req *http.Request, stageFile string, tmplData *templateVars, ctx *requestContext) { + wwlog.Serv("stage_file '%s'", stageFile) + + if util.IsFile(stageFile) { + + if tmplData != nil { + if ctx.rinfo.compress != "" { + wwlog.Error("Unsupported %s compressed version for file: %s", + ctx.rinfo.compress, stageFile) + w.WriteHeader(http.StatusNotFound) + return + } + + // Create a template with the Sprig functions. + tmpl := template.New(filepath.Base(stageFile)).Funcs(sprig.TxtFuncMap()) + + // Parse the template. + parsedTmpl, err := tmpl.ParseFiles(stageFile) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + wwlog.ErrorExc(err, "") + return + } + + // template engine writes file to buffer in case rendering fails + var buf bytes.Buffer + + err = parsedTmpl.Execute(&buf, tmplData) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + wwlog.ErrorExc(err, "") + return + } + + w.Header().Set("Content-Type", "text") + w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) + _, err = buf.WriteTo(w) + if err != nil { + wwlog.ErrorExc(err, "") + } + + wwlog.Info("send %s -> %s", stageFile, ctx.remoteNode.Id()) + + } else { + if ctx.rinfo.compress == "gz" { + stageFile += ".gz" + + if !util.IsFile(stageFile) { + wwlog.Error("unprepared for compressed version of file %s", + stageFile) + w.WriteHeader(http.StatusNotFound) + return + } + } else if ctx.rinfo.compress != "" { + wwlog.Error("unsupported %s compressed version of file %s", + ctx.rinfo.compress, stageFile) + w.WriteHeader(http.StatusNotFound) + } + + err := sendFile(w, req, stageFile, ctx.remoteNode.Id()) + if err != nil { + wwlog.ErrorExc(err, "") + return + } + } + + updateStatus(ctx.remoteNode.Id(), ctx.statusStage, path.Base(stageFile), ctx.rinfo.ipaddr) + + } else if stageFile == "" { + w.WriteHeader(http.StatusBadRequest) + wwlog.Error("No resource selected") + updateStatus(ctx.remoteNode.Id(), ctx.statusStage, "BAD_REQUEST", ctx.rinfo.ipaddr) + + } else { + w.WriteHeader(http.StatusNotFound) + wwlog.Error("Not found: %s", stageFile) + updateStatus(ctx.remoteNode.Id(), ctx.statusStage, "NOT_FOUND", ctx.rinfo.ipaddr) + } +} diff --git a/internal/pkg/warewulfd/image.go b/internal/pkg/warewulfd/image.go new file mode 100644 index 00000000..53b5762d --- /dev/null +++ b/internal/pkg/warewulfd/image.go @@ -0,0 +1,30 @@ +package warewulfd + +import ( + "net/http" + + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleImage handles container/image requests +func HandleImage(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + var stageFile string + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + } else { + if ctx.remoteNode.ImageName != "" { + stageFile = image.ImageFile(ctx.remoteNode.ImageName) + } else { + wwlog.Warn("No image set for node %s", ctx.remoteNode.Id()) + } + } + + sendResponse(w, req, stageFile, nil, ctx) +} diff --git a/internal/pkg/warewulfd/initramfs.go b/internal/pkg/warewulfd/initramfs.go new file mode 100644 index 00000000..c2ce4186 --- /dev/null +++ b/internal/pkg/warewulfd/initramfs.go @@ -0,0 +1,39 @@ +package warewulfd + +import ( + "net/http" + + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/kernel" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleInitramfs handles initramfs requests +func HandleInitramfs(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + var stageFile string + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + } else { + if kernel_ := kernel.FromNode(&ctx.remoteNode); kernel_ != nil { + if kver := kernel_.Version(); kver != "" { + if initramfs := image.FindInitramfs(ctx.remoteNode.ImageName, kver); initramfs != nil { + stageFile = initramfs.FullPath() + } else { + wwlog.Error("No initramfs found for kernel %s in image %s", kver, ctx.remoteNode.ImageName) + } + } else { + wwlog.Error("No initramfs found: unable to determine kernel version for node %s", ctx.remoteNode.Id()) + } + } else { + wwlog.Error("No initramfs found: unable to find kernel for node %s", ctx.remoteNode.Id()) + } + } + + sendResponse(w, req, stageFile, nil, ctx) +} diff --git a/internal/pkg/warewulfd/ipxe.go b/internal/pkg/warewulfd/ipxe.go new file mode 100644 index 00000000..020b4072 --- /dev/null +++ b/internal/pkg/warewulfd/ipxe.go @@ -0,0 +1,35 @@ +package warewulfd + +import ( + "net/http" + "path" + + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleIpxe handles iPXE boot script requests +func HandleIpxe(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + var stageFile string + var tmplData *templateVars + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + stageFile = path.Join(ctx.conf.Paths.Sysconfdir, "/warewulf/ipxe/unconfigured.ipxe") + tmplData = &templateVars{ + Hwaddr: ctx.rinfo.hwaddr} + } else { + template := ctx.remoteNode.Ipxe + if template == "" { + template = "default" + } + stageFile = path.Join(ctx.conf.Paths.Sysconfdir, "warewulf/ipxe", template+".ipxe") + tmplData = buildTemplateVars(ctx.conf, ctx.rinfo, ctx.remoteNode) + } + + sendResponse(w, req, stageFile, tmplData, ctx) +} diff --git a/internal/pkg/warewulfd/kernel.go b/internal/pkg/warewulfd/kernel.go new file mode 100644 index 00000000..c69c8631 --- /dev/null +++ b/internal/pkg/warewulfd/kernel.go @@ -0,0 +1,34 @@ +package warewulfd + +import ( + "net/http" + + "github.com/warewulf/warewulf/internal/pkg/kernel" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleKernel handles kernel binary requests +func HandleKernel(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + var stageFile string + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + } else { + kernel_ := kernel.FromNode(&ctx.remoteNode) + if kernel_ == nil { + wwlog.Error("No kernel found for node %s", ctx.remoteNode.Id()) + } else { + stageFile = kernel_.FullPath() + if stageFile == "" { + wwlog.Error("No kernel path found for node %s", ctx.remoteNode.Id()) + } + } + } + + sendResponse(w, req, stageFile, nil, ctx) +} diff --git a/internal/pkg/warewulfd/overlay.go b/internal/pkg/warewulfd/overlay.go index 4db3f1dc..5f45bb08 100644 --- a/internal/pkg/warewulfd/overlay.go +++ b/internal/pkg/warewulfd/overlay.go @@ -1,6 +1,7 @@ package warewulfd import ( + "errors" "fmt" "net" "net/http" @@ -16,7 +17,49 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func OverlaySend(w http.ResponseWriter, req *http.Request) { +// HandleOverlay handles system and runtime overlay requests +func HandleOverlay(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + sendResponse(w, req, "", nil, ctx) + return + } + + var context string + var request_overlays []string + + if len(ctx.rinfo.overlay) > 0 { + request_overlays = strings.Split(ctx.rinfo.overlay, ",") + } else { + context = ctx.rinfo.stage + } + + stageFile, err := getOverlayFile( + ctx.remoteNode, + context, + request_overlays, + ctx.conf.Warewulf.AutobuildOverlays()) + + if err != nil { + if errors.Is(err, overlay.ErrDoesNotExist) { + w.WriteHeader(http.StatusNotFound) + wwlog.ErrorExc(err, "") + return + } + w.WriteHeader(http.StatusInternalServerError) + wwlog.ErrorExc(err, "") + return + } + + sendResponse(w, req, stageFile, nil, ctx) +} + +func HandleOverlayFile(w http.ResponseWriter, req *http.Request) { rinfo, err := parseReqRender(req) if err != nil { message := "error parsing request: %s" diff --git a/internal/pkg/warewulfd/overlay_test.go b/internal/pkg/warewulfd/overlay_test.go index 0163ff56..f1668993 100644 --- a/internal/pkg/warewulfd/overlay_test.go +++ b/internal/pkg/warewulfd/overlay_test.go @@ -95,7 +95,7 @@ nodes: t.Run(description, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tt.url, nil) w := httptest.NewRecorder() - OverlaySend(w, req) + HandleOverlayFile(w, req) res := w.Result() defer res.Body.Close() diff --git a/internal/pkg/warewulfd/parser.go b/internal/pkg/warewulfd/parser.go index bd29e80e..8347a659 100644 --- a/internal/pkg/warewulfd/parser.go +++ b/internal/pkg/warewulfd/parser.go @@ -1,15 +1,83 @@ package warewulfd import ( + "fmt" "net/http" "net/netip" "strings" "github.com/pkg/errors" + warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -type parserInfo struct { +// requestContext holds the validated results of the parsed request +type requestContext struct { + conf *warewulfconf.WarewulfYaml + rinfo parsedRequest + remoteNode node.Node + statusStage string +} + +// initHandleRequest performs common initial request parsing, security checks, +// node lookup, and asset key validation. On error, it writes the HTTP error +// response and returns a non-nil error so the caller can simply return. +func initHandleRequest(w http.ResponseWriter, req *http.Request) (*requestContext, error) { + wwlog.Debug("Requested URL: %s", req.URL.String()) + conf := warewulfconf.Get() + rinfo, err := parseRequest(req) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + wwlog.ErrorExc(err, "Bad status") + return nil, err + } + + wwlog.Debug("stage: %s", rinfo.stage) + + wwlog.Info("request from hwaddr:%s ipaddr:%s | stage:%s", rinfo.hwaddr, req.RemoteAddr, rinfo.stage) + + if (rinfo.stage == "runtime" || len(rinfo.overlay) > 0) && conf.Warewulf.Secure() { + if rinfo.remoteport >= 1024 { + wwlog.Denied("Non-privileged port: %s", req.RemoteAddr) + w.WriteHeader(http.StatusUnauthorized) + return nil, fmt.Errorf("non-privileged port") + } + } + + status_stages := map[string]string{ + "efiboot": "EFI", + "ipxe": "IPXE", + "kernel": "KERNEL", + "system": "SYSTEM_OVERLAY", + "runtime": "RUNTIME_OVERLAY", + "initramfs": "INITRAMFS"} + + statusStage := status_stages[rinfo.stage] + + remoteNode, err := GetNodeOrSetDiscoverable(rinfo.hwaddr, conf.Warewulf.AutobuildOverlays()) + if err != nil && err != node.ErrNoUnconfigured { + wwlog.ErrorExc(err, "") + w.WriteHeader(http.StatusServiceUnavailable) + return nil, err + } + + if remoteNode.AssetKey != "" && remoteNode.AssetKey != rinfo.assetkey { + w.WriteHeader(http.StatusUnauthorized) + wwlog.Denied("incorrect asset key: node %s: %s", remoteNode.Id(), rinfo.assetkey) + updateStatus(remoteNode.Id(), statusStage, "BAD_ASSET", rinfo.ipaddr) + return nil, fmt.Errorf("incorrect asset key") + } + + return &requestContext{ + conf: conf, + rinfo: rinfo, + remoteNode: remoteNode, + statusStage: statusStage, + }, nil +} + +type parsedRequest struct { hwaddr string ipaddr string remoteport int @@ -21,8 +89,8 @@ type parserInfo struct { compress string } -func parseReq(req *http.Request) (parserInfo, error) { - var ret parserInfo +func parseRequest(req *http.Request) (parsedRequest, error) { + var ret parsedRequest url := strings.Split(req.URL.Path, "?")[0] path_parts := strings.Split(url, "/") @@ -62,19 +130,20 @@ func parseReq(req *http.Request) (parserInfo, error) { ret.stage = req.URL.Query()["stage"][0] } else { - if stage == "ipxe" || stage == "provision" { + switch stage { + case "ipxe", "provision": ret.stage = "ipxe" - } else if stage == "kernel" { + case "kernel": ret.stage = "kernel" - } else if stage == "image" { + case "image", "container": ret.stage = "image" - } else if stage == "overlay-system" { + case "overlay-system": ret.stage = "system" - } else if stage == "overlay-runtime" { + case "overlay-runtime": ret.stage = "runtime" - } else if stage == "efiboot" { + case "efiboot": ret.stage = "efiboot" - } else if stage == "initramfs" { + case "initramfs": ret.stage = "initramfs" } } diff --git a/internal/pkg/warewulfd/parser_test.go b/internal/pkg/warewulfd/parser_test.go index da595575..03efb651 100644 --- a/internal/pkg/warewulfd/parser_test.go +++ b/internal/pkg/warewulfd/parser_test.go @@ -12,13 +12,13 @@ var parseReqTests = []struct { description string url string remoteAddr string - result parserInfo + result parsedRequest }{ { description: "basic ipv4 request", url: "/provision/00:00:00:ff:ff:ff", remoteAddr: "10.5.1.1:9873", - result: parserInfo{ + result: parsedRequest{ hwaddr: "00:00:00:ff:ff:ff", ipaddr: "10.5.1.1", remoteport: 9873, @@ -29,7 +29,7 @@ var parseReqTests = []struct { description: "basic ipv6 request", url: "/provision/00:00:00:ff:ff:ff", remoteAddr: "[fd00:5::1:1]:9873", - result: parserInfo{ + result: parsedRequest{ hwaddr: "00:00:00:ff:ff:ff", ipaddr: "fd00:5::1:1", remoteport: 9873, @@ -38,14 +38,14 @@ var parseReqTests = []struct { }, } -func Test_ParseReq(t *testing.T) { +func Test_ParseRequest(t *testing.T) { for _, tt := range parseReqTests { t.Run(tt.description, func(t *testing.T) { req := &http.Request{ URL: &url.URL{Path: tt.url}, RemoteAddr: tt.remoteAddr, } - result, err := parseReq(req) + result, err := parseRequest(req) assert.NoError(t, err) assert.Equal(t, tt.result, result) }) diff --git a/internal/pkg/warewulfd/provision.go b/internal/pkg/warewulfd/provision.go index e3bd229d..b07603c9 100644 --- a/internal/pkg/warewulfd/provision.go +++ b/internal/pkg/warewulfd/provision.go @@ -1,24 +1,9 @@ package warewulfd import ( - "bytes" - "errors" - "fmt" "net/http" - "net/netip" - "path" - "path/filepath" - "strconv" - "strings" - "text/template" - "github.com/Masterminds/sprig/v3" - warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" - "github.com/warewulf/warewulf/internal/pkg/image" - "github.com/warewulf/warewulf/internal/pkg/kernel" "github.com/warewulf/warewulf/internal/pkg/node" - "github.com/warewulf/warewulf/internal/pkg/overlay" - "github.com/warewulf/warewulf/internal/pkg/util" "github.com/warewulf/warewulf/internal/pkg/wwlog" ) @@ -44,341 +29,38 @@ type templateVars struct { NetDevs map[string]*node.NetDev } -func ProvisionSend(w http.ResponseWriter, req *http.Request) { - wwlog.Debug("Requested URL: %s", req.URL.String()) - conf := warewulfconf.Get() - rinfo, err := parseReq(req) +func HandleProvision(w http.ResponseWriter, req *http.Request) { + // Parse just enough to determine the stage + rinfo, err := parseRequest(req) if err != nil { w.WriteHeader(http.StatusBadRequest) wwlog.ErrorExc(err, "Bad status") return } - wwlog.Debug("stage: %s", rinfo.stage) - - wwlog.Info("request from hwaddr:%s ipaddr:%s | stage:%s", rinfo.hwaddr, req.RemoteAddr, rinfo.stage) - - if (rinfo.stage == "runtime" || len(rinfo.overlay) > 0) && conf.Warewulf.Secure() { - if rinfo.remoteport >= 1024 { - wwlog.Denied("Non-privileged port: %s", req.RemoteAddr) - w.WriteHeader(http.StatusUnauthorized) - return - } - } - - status_stages := map[string]string{ - "efiboot": "EFI", - "ipxe": "IPXE", - "kernel": "KERNEL", - "system": "SYSTEM_OVERLAY", - "runtime": "RUNTIME_OVERLAY", - "initramfs": "INITRAMFS"} - - status_stage := status_stages[rinfo.stage] - var stage_file string - - // TODO: when module version is upgraded to go1.18, should be 'any' type - var tmpl_data *templateVars - - remoteNode, err := GetNodeOrSetDiscoverable(rinfo.hwaddr, conf.Warewulf.AutobuildOverlays()) - if err != nil && err != node.ErrNoUnconfigured { - wwlog.ErrorExc(err, "") - w.WriteHeader(http.StatusServiceUnavailable) - return - } - - if remoteNode.AssetKey != "" && remoteNode.AssetKey != rinfo.assetkey { - w.WriteHeader(http.StatusUnauthorized) - wwlog.Denied("incorrect asset key: node %s: %s", remoteNode.Id(), rinfo.assetkey) - updateStatus(remoteNode.Id(), status_stage, "BAD_ASSET", rinfo.ipaddr) - return - } - - if !remoteNode.Valid() { - wwlog.Error("%s (unknown/unconfigured node)", rinfo.hwaddr) - if rinfo.stage == "ipxe" { - stage_file = path.Join(conf.Paths.Sysconfdir, "/warewulf/ipxe/unconfigured.ipxe") - tmpl_data = &templateVars{ - Hwaddr: rinfo.hwaddr} - } - - } else if rinfo.stage == "ipxe" { - template := remoteNode.Ipxe - if template == "" { - template = "default" - } - stage_file = path.Join(conf.Paths.Sysconfdir, "warewulf/ipxe", template+".ipxe") - kernelArgs := "" - kernelVersion := "" - if remoteNode.Kernel != nil { - kernelArgs = strings.Join(remoteNode.Kernel.Args, " ") - kernelVersion = remoteNode.Kernel.Version - } - if kernelVersion == "" { - if kernel_ := kernel.FromNode(&remoteNode); kernel_ != nil { - kernelVersion = kernel_.Version() - } - } - authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port) - ipaddr6 := "" - if confIpaddr6, err := netip.ParseAddr(conf.Ipaddr6); err == nil { - ipaddr6 = confIpaddr6.String() - } - if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil { - if rinfoIpaddr.Is6() { - if ipaddr6 != "" { - authority = fmt.Sprintf("[%s]:%d", ipaddr6, conf.Warewulf.Port) - } else { - wwlog.Error("No valid IPv6 address configured, but request is IPv6") - } - } - } else { - wwlog.Error("Could not parse request IP address: %s", rinfo.ipaddr) - } - tmpl_data = &templateVars{ - Id: remoteNode.Id(), - Cluster: remoteNode.ClusterName, - Fqdn: remoteNode.Id(), - Ipaddr: conf.Ipaddr, - Ipaddr6: ipaddr6, - Port: strconv.Itoa(conf.Warewulf.Port), - TLS: conf.Warewulf.EnableTLS(), - Authority: authority, - Hostname: remoteNode.Id(), - Hwaddr: rinfo.hwaddr, - ImageName: remoteNode.ImageName, - KernelArgs: kernelArgs, - KernelVersion: kernelVersion, - Root: remoteNode.Root, - NetDevs: remoteNode.NetDevs, - Tags: remoteNode.Tags} - } else if rinfo.stage == "kernel" { - kernel_ := kernel.FromNode(&remoteNode) - if kernel_ == nil { - wwlog.Error("No kernel found for node %s", remoteNode.Id()) - } else { - stage_file = kernel_.FullPath() - if stage_file == "" { - wwlog.Error("No kernel path found for node %s", remoteNode.Id()) - } - } - - } else if rinfo.stage == "image" { - if remoteNode.ImageName != "" { - stage_file = image.ImageFile(remoteNode.ImageName) - } else { - wwlog.Warn("No image set for node %s", remoteNode.Id()) - } - - } else if rinfo.stage == "system" || rinfo.stage == "runtime" { - var context string - var request_overlays []string - - if len(rinfo.overlay) > 0 { - request_overlays = strings.Split(rinfo.overlay, ",") - } else { - context = rinfo.stage - } - stage_file, err = getOverlayFile( - remoteNode, - context, - request_overlays, - conf.Warewulf.AutobuildOverlays()) - - if err != nil { - if errors.Is(err, overlay.ErrDoesNotExist) { - w.WriteHeader(http.StatusNotFound) - wwlog.ErrorExc(err, "") - return - } - w.WriteHeader(http.StatusInternalServerError) - wwlog.ErrorExc(err, "") - return - } - } else if rinfo.stage == "efiboot" { - wwlog.Debug("requested method: %s", req.Method) - imageName := remoteNode.ImageName - switch rinfo.efifile { - case "shim.efi": - stage_file = image.ShimFind(imageName) - if stage_file == "" { - wwlog.Error("couldn't find shim.efi for %s", imageName) - w.WriteHeader(http.StatusNotFound) - return - } - case "grub.efi", "grub-tpm.efi", "grubx64.efi", "grubia32.efi", "grubaa64.efi", "grubarm.efi": - stage_file = image.GrubFind(imageName) - if stage_file == "" { - wwlog.Error("could't find grub*.efi for %s", imageName) - w.WriteHeader(http.StatusNotFound) - return - } - case "grub.cfg": - stage_file = path.Join(conf.Paths.Sysconfdir, "warewulf/grub/grub.cfg.ww") - kernelArgs := "" - kernelVersion := "" - if remoteNode.Kernel != nil { - kernelArgs = strings.Join(remoteNode.Kernel.Args, " ") - kernelVersion = remoteNode.Kernel.Version - } - if kernelVersion == "" { - if kernel_ := kernel.FromNode(&remoteNode); kernel_ != nil { - kernelVersion = kernel_.Version() - } - } - authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port) - ipaddr6 := "" - if confIpaddr6, err := netip.ParseAddr(conf.Ipaddr6); err == nil { - ipaddr6 = confIpaddr6.String() - } - if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil { - if rinfoIpaddr.Is6() { - if ipaddr6 != "" { - authority = fmt.Sprintf("[%s]:%d", ipaddr6, conf.Warewulf.Port) - } else { - wwlog.Error("No valid IPv6 address configured, but request is IPv6") - } - } - } else { - wwlog.Error("Could not parse request IP address: %s", rinfo.ipaddr) - } - tmpl_data = &templateVars{ - Id: remoteNode.Id(), - Cluster: remoteNode.ClusterName, - Fqdn: remoteNode.Id(), - Ipaddr: conf.Ipaddr, - Ipaddr6: ipaddr6, - Port: strconv.Itoa(conf.Warewulf.Port), - TLS: conf.Warewulf.EnableTLS(), - Authority: authority, - Hostname: remoteNode.Id(), - Hwaddr: rinfo.hwaddr, - ImageName: remoteNode.ImageName, - Ipxe: remoteNode.Ipxe, - KernelArgs: kernelArgs, - KernelVersion: kernelVersion, - Root: remoteNode.Root, - NetDevs: remoteNode.NetDevs, - Tags: remoteNode.Tags} - if stage_file == "" { - wwlog.Error("could't find grub.cfg template for %s", imageName) - w.WriteHeader(http.StatusNotFound) - return - } - default: - wwlog.ErrorExc(fmt.Errorf("could't find efiboot file: %s", rinfo.efifile), "") - } - } else if rinfo.stage == "shim" { - if remoteNode.ImageName != "" { - stage_file = image.ShimFind(remoteNode.ImageName) - - if stage_file == "" { - wwlog.Error("No kernel found for image %s", remoteNode.ImageName) - } - } else { - wwlog.Warn("No image set for this %s", remoteNode.Id()) - } - } else if rinfo.stage == "grub" { - if remoteNode.ImageName != "" { - stage_file = image.GrubFind(remoteNode.ImageName) - if stage_file == "" { - wwlog.Error("No grub found for image %s", remoteNode.ImageName) - } - } else { - wwlog.Warn("No conainer set for node %s", remoteNode.Id()) - } - } else if rinfo.stage == "initramfs" { - if kernel_ := kernel.FromNode(&remoteNode); kernel_ != nil { - if kver := kernel_.Version(); kver != "" { - if initramfs := image.FindInitramfs(remoteNode.ImageName, kver); initramfs != nil { - stage_file = initramfs.FullPath() - } else { - wwlog.Error("No initramfs found for kernel %s in image %s", kver, remoteNode.ImageName) - } - } else { - wwlog.Error("No initramfs found: unable to determine kernel version for node %s", remoteNode.Id()) - } - } else { - wwlog.Error("No initramfs found: unable to find kernel for node %s", remoteNode.Id()) - } - } - - wwlog.Serv("stage_file '%s'", stage_file) - - if util.IsFile(stage_file) { - - if tmpl_data != nil { - if rinfo.compress != "" { - wwlog.Error("Unsupported %s compressed version for file: %s", - rinfo.compress, stage_file) - w.WriteHeader(http.StatusNotFound) - return - } - - // Create a template with the Sprig functions. - tmpl := template.New(filepath.Base(stage_file)).Funcs(sprig.TxtFuncMap()) - - // Parse the template. - parsedTmpl, err := tmpl.ParseFiles(stage_file) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - wwlog.ErrorExc(err, "") - return - } - - // template engine writes file to buffer in case rendering fails - var buf bytes.Buffer - - err = parsedTmpl.Execute(&buf, tmpl_data) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - wwlog.ErrorExc(err, "") - return - } - - w.Header().Set("Content-Type", "text") - w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) - _, err = buf.WriteTo(w) - if err != nil { - wwlog.ErrorExc(err, "") - } - - wwlog.Info("send %s -> %s", stage_file, remoteNode.Id()) - - } else { - if rinfo.compress == "gz" { - stage_file += ".gz" - - if !util.IsFile(stage_file) { - wwlog.Error("unprepared for compressed version of file %s", - stage_file) - w.WriteHeader(http.StatusNotFound) - return - } - } else if rinfo.compress != "" { - wwlog.Error("unsupported %s compressed version of file %s", - rinfo.compress, stage_file) - w.WriteHeader(http.StatusNotFound) - } - - err = sendFile(w, req, stage_file, remoteNode.Id()) - if err != nil { - wwlog.ErrorExc(err, "") - return - } - } - - updateStatus(remoteNode.Id(), status_stage, path.Base(stage_file), rinfo.ipaddr) - - } else if stage_file == "" { + // Dispatch to the appropriate stage handler + var handler http.HandlerFunc + switch rinfo.stage { + case "ipxe": + handler = HandleIpxe + case "kernel": + handler = HandleKernel + case "image": + handler = HandleImage + case "system", "runtime": + handler = HandleOverlay + case "efiboot": + handler = HandleEfiBoot + case "shim": + handler = HandleShim + case "grub": + handler = HandleGrub + case "initramfs": + handler = HandleInitramfs + default: w.WriteHeader(http.StatusBadRequest) - wwlog.Error("No resource selected") - updateStatus(remoteNode.Id(), status_stage, "BAD_REQUEST", rinfo.ipaddr) - - } else { - w.WriteHeader(http.StatusNotFound) - wwlog.Error("Not found: %s", stage_file) - updateStatus(remoteNode.Id(), status_stage, "NOT_FOUND", rinfo.ipaddr) + wwlog.Error("Unknown stage: %s", rinfo.stage) + return } - + handler(w, req) } diff --git a/internal/pkg/warewulfd/provision_test.go b/internal/pkg/warewulfd/provision_test.go index 769d5ea7..0f0d043f 100644 --- a/internal/pkg/warewulfd/provision_test.go +++ b/internal/pkg/warewulfd/provision_test.go @@ -100,7 +100,7 @@ nodes: req := httptest.NewRequest(http.MethodGet, tt.url, nil) req.RemoteAddr = tt.ip w := httptest.NewRecorder() - ProvisionSend(w, req) + HandleProvision(w, req) res := w.Result() defer res.Body.Close() diff --git a/internal/pkg/warewulfd/server/server.go b/internal/pkg/warewulfd/server/server.go index 027da7e9..26aa5ca1 100644 --- a/internal/pkg/warewulfd/server/server.go +++ b/internal/pkg/warewulfd/server/server.go @@ -37,19 +37,20 @@ func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } -func configureHandler(includeRuntime bool, apiHandler http.Handler) *slashFix { +func configureRootHandler(apiHandler http.Handler, includeRuntime bool) *slashFix { var wwHandler http.ServeMux - wwHandler.HandleFunc("/provision/", warewulfd.ProvisionSend) - wwHandler.HandleFunc("/ipxe/", warewulfd.ProvisionSend) - wwHandler.HandleFunc("/efiboot/", warewulfd.ProvisionSend) - wwHandler.HandleFunc("/kernel/", warewulfd.ProvisionSend) - wwHandler.HandleFunc("/container/", warewulfd.ProvisionSend) - wwHandler.HandleFunc("/overlay-system/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/provision/", warewulfd.HandleProvision) + wwHandler.HandleFunc("/ipxe/", warewulfd.HandleIpxe) + wwHandler.HandleFunc("/efiboot/", warewulfd.HandleEfiBoot) + wwHandler.HandleFunc("/kernel/", warewulfd.HandleKernel) + wwHandler.HandleFunc("/image/", warewulfd.HandleImage) + wwHandler.HandleFunc("/container/", warewulfd.HandleImage) + wwHandler.HandleFunc("/overlay-system/", warewulfd.HandleOverlay) if includeRuntime { - wwHandler.HandleFunc("/overlay-runtime/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/overlay-runtime/", warewulfd.HandleOverlay) } - wwHandler.HandleFunc("/overlay-file/", warewulfd.OverlaySend) - wwHandler.HandleFunc("/status", warewulfd.StatusSend) + wwHandler.HandleFunc("/overlay-file/", warewulfd.HandleOverlayFile) + wwHandler.HandleFunc("/status", warewulfd.HandleStatus) if apiHandler != nil { wwHandler.Handle("/api/", apiHandler) @@ -86,7 +87,7 @@ func RunServer() error { apiHandler = api.Handler(auth, conf.API.AllowedIPNets()) } - httpHandler := configureHandler(!conf.Warewulf.EnableTLS(), apiHandler) + httpHandler := configureRootHandler(apiHandler, !conf.Warewulf.EnableTLS()) errChan := make(chan error, 2) @@ -97,7 +98,7 @@ func RunServer() error { if !util.IsFile(key) || !util.IsFile(crt) { return fmt.Errorf("TLS enabled but keys not found in %s, run 'wwctl configure tls --create' to generate keys", path.Join(conf.Paths.Sysconfdir, "warewulf", "tls")) } - httpsHandler := configureHandler(true, apiHandler) + httpsHandler := configureRootHandler(apiHandler, true) go func() { wwlog.Info("Starting HTTPS service on port %d", conf.Warewulf.SecurePort) if err := http.ListenAndServeTLS(":"+strconv.Itoa(conf.Warewulf.SecurePort), crt, key, httpsHandler); err != nil { diff --git a/internal/pkg/warewulfd/shim.go b/internal/pkg/warewulfd/shim.go new file mode 100644 index 00000000..9d3c6270 --- /dev/null +++ b/internal/pkg/warewulfd/shim.go @@ -0,0 +1,34 @@ +package warewulfd + +import ( + "net/http" + + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// HandleShim handles direct shim binary requests +func HandleShim(w http.ResponseWriter, req *http.Request) { + ctx, err := initHandleRequest(w, req) + if err != nil { + return // response already written + } + + var stageFile string + + if !ctx.remoteNode.Valid() { + wwlog.Error("%s (unknown/unconfigured node)", ctx.rinfo.hwaddr) + } else { + if ctx.remoteNode.ImageName != "" { + stageFile = image.ShimFind(ctx.remoteNode.ImageName) + + if stageFile == "" { + wwlog.Error("No kernel found for image %s", ctx.remoteNode.ImageName) + } + } else { + wwlog.Warn("No image set for this %s", ctx.remoteNode.Id()) + } + } + + sendResponse(w, req, stageFile, nil, ctx) +} diff --git a/internal/pkg/warewulfd/status.go b/internal/pkg/warewulfd/status.go index 4a66e042..00c500ca 100644 --- a/internal/pkg/warewulfd/status.go +++ b/internal/pkg/warewulfd/status.go @@ -92,7 +92,7 @@ func statusJSON() ([]byte, error) { return ret, nil } -func StatusSend(w http.ResponseWriter, req *http.Request) { +func HandleStatus(w http.ResponseWriter, req *http.Request) { status, err := statusJSON() if err != nil {