Files
warewulf/internal/pkg/warewulfd/provision.go
Timothy Middelkoop 4f58845a9e Add IPv6 support for Dnsmasq and NetworkManager
Signed-off-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
2025-11-27 01:56:18 +00:00

382 lines
11 KiB
Go

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"
)
type templateVars struct {
Message string
WaitTime string
Hostname string
Fqdn string
Id string
Cluster string
ImageName string
Ipxe string
Hwaddr string
Ipaddr string
Ipaddr6 string
Port string
Authority string
KernelArgs string
KernelVersion string
Root string
Tags map[string]string
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)
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),
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),
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)
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)
}
}