Refactor server to separate handlers

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2026-02-23 11:40:22 -07:00
parent ced93bcc5a
commit 91723a258a
18 changed files with 620 additions and 422 deletions

View File

@@ -1,12 +1,13 @@
package configure package configure
import ( import (
"fmt"
"os" "os"
"path" "path"
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" 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/util"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog" "github.com/warewulf/warewulf/internal/pkg/wwlog"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -35,7 +36,7 @@ func TFTP() (err error) {
} }
if controller.Warewulf.GrubBoot() { if controller.Warewulf.GrubBoot() {
err := warewulfd.CopyShimGrub() err := CopyShimGrub()
if err != nil { if err != nil {
wwlog.Warn("error when copying shim/grub binaries: %s", err) wwlog.Warn("error when copying shim/grub binaries: %s", err)
} }
@@ -70,3 +71,30 @@ func TFTP() (err error) {
return nil 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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -1,6 +1,7 @@
package warewulfd package warewulfd
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -16,7 +17,49 @@ import (
"github.com/warewulf/warewulf/internal/pkg/wwlog" "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) rinfo, err := parseReqRender(req)
if err != nil { if err != nil {
message := "error parsing request: %s" message := "error parsing request: %s"

View File

@@ -95,7 +95,7 @@ nodes:
t.Run(description, func(t *testing.T) { t.Run(description, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.url, nil) req := httptest.NewRequest(http.MethodGet, tt.url, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
OverlaySend(w, req) HandleOverlayFile(w, req)
res := w.Result() res := w.Result()
defer res.Body.Close() defer res.Body.Close()

View File

@@ -1,15 +1,83 @@
package warewulfd package warewulfd
import ( import (
"fmt"
"net/http" "net/http"
"net/netip" "net/netip"
"strings" "strings"
"github.com/pkg/errors" "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" "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 hwaddr string
ipaddr string ipaddr string
remoteport int remoteport int
@@ -21,8 +89,8 @@ type parserInfo struct {
compress string compress string
} }
func parseReq(req *http.Request) (parserInfo, error) { func parseRequest(req *http.Request) (parsedRequest, error) {
var ret parserInfo var ret parsedRequest
url := strings.Split(req.URL.Path, "?")[0] url := strings.Split(req.URL.Path, "?")[0]
path_parts := strings.Split(url, "/") path_parts := strings.Split(url, "/")
@@ -62,19 +130,20 @@ func parseReq(req *http.Request) (parserInfo, error) {
ret.stage = req.URL.Query()["stage"][0] ret.stage = req.URL.Query()["stage"][0]
} else { } else {
if stage == "ipxe" || stage == "provision" { switch stage {
case "ipxe", "provision":
ret.stage = "ipxe" ret.stage = "ipxe"
} else if stage == "kernel" { case "kernel":
ret.stage = "kernel" ret.stage = "kernel"
} else if stage == "image" { case "image", "container":
ret.stage = "image" ret.stage = "image"
} else if stage == "overlay-system" { case "overlay-system":
ret.stage = "system" ret.stage = "system"
} else if stage == "overlay-runtime" { case "overlay-runtime":
ret.stage = "runtime" ret.stage = "runtime"
} else if stage == "efiboot" { case "efiboot":
ret.stage = "efiboot" ret.stage = "efiboot"
} else if stage == "initramfs" { case "initramfs":
ret.stage = "initramfs" ret.stage = "initramfs"
} }
} }

View File

@@ -12,13 +12,13 @@ var parseReqTests = []struct {
description string description string
url string url string
remoteAddr string remoteAddr string
result parserInfo result parsedRequest
}{ }{
{ {
description: "basic ipv4 request", description: "basic ipv4 request",
url: "/provision/00:00:00:ff:ff:ff", url: "/provision/00:00:00:ff:ff:ff",
remoteAddr: "10.5.1.1:9873", remoteAddr: "10.5.1.1:9873",
result: parserInfo{ result: parsedRequest{
hwaddr: "00:00:00:ff:ff:ff", hwaddr: "00:00:00:ff:ff:ff",
ipaddr: "10.5.1.1", ipaddr: "10.5.1.1",
remoteport: 9873, remoteport: 9873,
@@ -29,7 +29,7 @@ var parseReqTests = []struct {
description: "basic ipv6 request", description: "basic ipv6 request",
url: "/provision/00:00:00:ff:ff:ff", url: "/provision/00:00:00:ff:ff:ff",
remoteAddr: "[fd00:5::1:1]:9873", remoteAddr: "[fd00:5::1:1]:9873",
result: parserInfo{ result: parsedRequest{
hwaddr: "00:00:00:ff:ff:ff", hwaddr: "00:00:00:ff:ff:ff",
ipaddr: "fd00:5::1:1", ipaddr: "fd00:5::1:1",
remoteport: 9873, 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 { for _, tt := range parseReqTests {
t.Run(tt.description, func(t *testing.T) { t.Run(tt.description, func(t *testing.T) {
req := &http.Request{ req := &http.Request{
URL: &url.URL{Path: tt.url}, URL: &url.URL{Path: tt.url},
RemoteAddr: tt.remoteAddr, RemoteAddr: tt.remoteAddr,
} }
result, err := parseReq(req) result, err := parseRequest(req)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.result, result) assert.Equal(t, tt.result, result)
}) })

View File

@@ -1,24 +1,9 @@
package warewulfd package warewulfd
import ( import (
"bytes"
"errors"
"fmt"
"net/http" "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/node"
"github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog" "github.com/warewulf/warewulf/internal/pkg/wwlog"
) )
@@ -44,341 +29,38 @@ type templateVars struct {
NetDevs map[string]*node.NetDev NetDevs map[string]*node.NetDev
} }
func ProvisionSend(w http.ResponseWriter, req *http.Request) { func HandleProvision(w http.ResponseWriter, req *http.Request) {
wwlog.Debug("Requested URL: %s", req.URL.String()) // Parse just enough to determine the stage
conf := warewulfconf.Get() rinfo, err := parseRequest(req)
rinfo, err := parseReq(req)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
wwlog.ErrorExc(err, "Bad status") wwlog.ErrorExc(err, "Bad status")
return return
} }
wwlog.Debug("stage: %s", rinfo.stage) // Dispatch to the appropriate stage handler
var handler http.HandlerFunc
wwlog.Info("request from hwaddr:%s ipaddr:%s | stage:%s", rinfo.hwaddr, req.RemoteAddr, rinfo.stage) switch rinfo.stage {
case "ipxe":
if (rinfo.stage == "runtime" || len(rinfo.overlay) > 0) && conf.Warewulf.Secure() { handler = HandleIpxe
if rinfo.remoteport >= 1024 { case "kernel":
wwlog.Denied("Non-privileged port: %s", req.RemoteAddr) handler = HandleKernel
w.WriteHeader(http.StatusUnauthorized) case "image":
return handler = HandleImage
} case "system", "runtime":
} handler = HandleOverlay
case "efiboot":
status_stages := map[string]string{ handler = HandleEfiBoot
"efiboot": "EFI", case "shim":
"ipxe": "IPXE", handler = HandleShim
"kernel": "KERNEL", case "grub":
"system": "SYSTEM_OVERLAY", handler = HandleGrub
"runtime": "RUNTIME_OVERLAY", case "initramfs":
"initramfs": "INITRAMFS"} handler = HandleInitramfs
default:
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 == "" {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
wwlog.Error("No resource selected") wwlog.Error("Unknown stage: %s", rinfo.stage)
updateStatus(remoteNode.Id(), status_stage, "BAD_REQUEST", rinfo.ipaddr) return
} else {
w.WriteHeader(http.StatusNotFound)
wwlog.Error("Not found: %s", stage_file)
updateStatus(remoteNode.Id(), status_stage, "NOT_FOUND", rinfo.ipaddr)
} }
handler(w, req)
} }

View File

@@ -100,7 +100,7 @@ nodes:
req := httptest.NewRequest(http.MethodGet, tt.url, nil) req := httptest.NewRequest(http.MethodGet, tt.url, nil)
req.RemoteAddr = tt.ip req.RemoteAddr = tt.ip
w := httptest.NewRecorder() w := httptest.NewRecorder()
ProvisionSend(w, req) HandleProvision(w, req)
res := w.Result() res := w.Result()
defer res.Body.Close() defer res.Body.Close()

View File

@@ -37,19 +37,20 @@ func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r) 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 var wwHandler http.ServeMux
wwHandler.HandleFunc("/provision/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/provision/", warewulfd.HandleProvision)
wwHandler.HandleFunc("/ipxe/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/ipxe/", warewulfd.HandleIpxe)
wwHandler.HandleFunc("/efiboot/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/efiboot/", warewulfd.HandleEfiBoot)
wwHandler.HandleFunc("/kernel/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/kernel/", warewulfd.HandleKernel)
wwHandler.HandleFunc("/container/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/image/", warewulfd.HandleImage)
wwHandler.HandleFunc("/overlay-system/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/container/", warewulfd.HandleImage)
wwHandler.HandleFunc("/overlay-system/", warewulfd.HandleOverlay)
if includeRuntime { if includeRuntime {
wwHandler.HandleFunc("/overlay-runtime/", warewulfd.ProvisionSend) wwHandler.HandleFunc("/overlay-runtime/", warewulfd.HandleOverlay)
} }
wwHandler.HandleFunc("/overlay-file/", warewulfd.OverlaySend) wwHandler.HandleFunc("/overlay-file/", warewulfd.HandleOverlayFile)
wwHandler.HandleFunc("/status", warewulfd.StatusSend) wwHandler.HandleFunc("/status", warewulfd.HandleStatus)
if apiHandler != nil { if apiHandler != nil {
wwHandler.Handle("/api/", apiHandler) wwHandler.Handle("/api/", apiHandler)
@@ -86,7 +87,7 @@ func RunServer() error {
apiHandler = api.Handler(auth, conf.API.AllowedIPNets()) apiHandler = api.Handler(auth, conf.API.AllowedIPNets())
} }
httpHandler := configureHandler(!conf.Warewulf.EnableTLS(), apiHandler) httpHandler := configureRootHandler(apiHandler, !conf.Warewulf.EnableTLS())
errChan := make(chan error, 2) errChan := make(chan error, 2)
@@ -97,7 +98,7 @@ func RunServer() error {
if !util.IsFile(key) || !util.IsFile(crt) { 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")) 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() { go func() {
wwlog.Info("Starting HTTPS service on port %d", conf.Warewulf.SecurePort) 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 { if err := http.ListenAndServeTLS(":"+strconv.Itoa(conf.Warewulf.SecurePort), crt, key, httpsHandler); err != nil {

View File

@@ -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)
}

View File

@@ -92,7 +92,7 @@ func statusJSON() ([]byte, error) {
return ret, nil return ret, nil
} }
func StatusSend(w http.ResponseWriter, req *http.Request) { func HandleStatus(w http.ResponseWriter, req *http.Request) {
status, err := statusJSON() status, err := statusJSON()
if err != nil { if err != nil {