Refactor server to separate handlers
Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
60
internal/pkg/warewulfd/efi.go
Normal file
60
internal/pkg/warewulfd/efi.go
Normal 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)
|
||||||
|
}
|
||||||
33
internal/pkg/warewulfd/grub.go
Normal file
33
internal/pkg/warewulfd/grub.go
Normal 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)
|
||||||
|
}
|
||||||
155
internal/pkg/warewulfd/helpers.go
Normal file
155
internal/pkg/warewulfd/helpers.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
internal/pkg/warewulfd/image.go
Normal file
30
internal/pkg/warewulfd/image.go
Normal 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)
|
||||||
|
}
|
||||||
39
internal/pkg/warewulfd/initramfs.go
Normal file
39
internal/pkg/warewulfd/initramfs.go
Normal 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)
|
||||||
|
}
|
||||||
35
internal/pkg/warewulfd/ipxe.go
Normal file
35
internal/pkg/warewulfd/ipxe.go
Normal 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)
|
||||||
|
}
|
||||||
34
internal/pkg/warewulfd/kernel.go
Normal file
34
internal/pkg/warewulfd/kernel.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
34
internal/pkg/warewulfd/shim.go
Normal file
34
internal/pkg/warewulfd/shim.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user