Add GRUB config route, tests, and server route documentation
- Add /grub/{hwaddr} route for serving GRUB configuration files,
replacing the shim handler with a config-based approach
- Add short URL aliases: 'system' for overlay-system, 'runtime' for
overlay-runtime, and 'grub' stage in parser
- Add 'grub' to status stage tracking in request handling
- Add comprehensive test coverage for EFI, GRUB, initramfs, iPXE,
overlay, and provision handlers (efi_test.go, grub_test.go,
initramfs_test.go, ipxe_test.go, overlay_test.go)
- Expand existing parser and provision tests
- Remove shim.go (functionality folded into grub.go)
- Add userdocs/server/routes.rst documenting all warewulfd HTTP routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
90
internal/pkg/warewulfd/efi_test.go
Normal file
90
internal/pkg/warewulfd/efi_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package warewulfd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
var efiBootTests = []struct {
|
||||
description string
|
||||
url string
|
||||
body string
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{"find shim", "/efiboot/shim.efi", "", 200, "10.10.10.10:9873"},
|
||||
{"find shim: node with missing image returns 404", "/efiboot/shim.efi", "", 404, "10.10.10.11:9873"},
|
||||
{"find grub", "/efiboot/grub.efi", "", 200, "10.10.10.10:9873"},
|
||||
{"find grub: node with missing image returns 404", "/efiboot/grub.efi", "", 404, "10.10.10.11:9873"},
|
||||
{"find grub.cfg", "/efiboot/grub.cfg", "dracut 10.10.0.1:9873", 200, "10.10.10.11:9873"},
|
||||
}
|
||||
|
||||
func Test_HandleEfiBoot(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
|
||||
env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles:
|
||||
default:
|
||||
image name: suse
|
||||
nodes:
|
||||
n1:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:ff:ff:ff
|
||||
profiles:
|
||||
- default
|
||||
n2:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:00:ff:ff
|
||||
image name: none
|
||||
tags:
|
||||
GrubMenuEntry: dracut`)
|
||||
|
||||
env.WriteFile("/var/tmp/arpcache", `IP address HW type Flags HW address Mask Device
|
||||
10.10.10.10 0x1 0x2 00:00:00:ff:ff:ff * dummy
|
||||
10.10.10.11 0x1 0x2 00:00:00:00:ff:ff * dummy`)
|
||||
prevArpFile := arpFile
|
||||
arpFile = env.GetPath("/var/tmp/arpcache")
|
||||
defer func() {
|
||||
arpFile = prevArpFile
|
||||
}()
|
||||
|
||||
env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/usr/lib64/efi/shim.efi")
|
||||
env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/usr/share/efi/x86_64/grub.efi")
|
||||
env.WriteFile("/etc/warewulf/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }} {{ .Authority }}")
|
||||
|
||||
dbErr := LoadNodeDB()
|
||||
assert.NoError(t, dbErr)
|
||||
|
||||
conf := warewulfconf.Get()
|
||||
secureFalse := false
|
||||
conf.Warewulf.SecureP = &secureFalse
|
||||
conf.Ipaddr = "10.10.0.1"
|
||||
conf.Ipaddr6 = "fd00:10::1"
|
||||
|
||||
for _, tt := range efiBootTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||
req.RemoteAddr = tt.ip
|
||||
w := httptest.NewRecorder()
|
||||
HandleEfiBoot(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, readErr := io.ReadAll(res.Body)
|
||||
assert.NoError(t, readErr)
|
||||
if tt.body != "" {
|
||||
assert.Equal(t, tt.body, string(data))
|
||||
}
|
||||
assert.Equal(t, tt.status, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,32 +2,25 @@ package warewulfd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/image"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
// HandleGrub handles direct GRUB binary requests
|
||||
// HandleGrub handles GRUB configuration 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())
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
sendResponse(w, req, stageFile, nil, ctx)
|
||||
stageFile := path.Join(ctx.conf.Paths.Sysconfdir, "warewulf/grub/grub.cfg.ww")
|
||||
tmplData := buildTemplateVars(ctx.conf, ctx.rinfo, ctx.remoteNode)
|
||||
sendResponse(w, req, stageFile, tmplData, ctx)
|
||||
}
|
||||
|
||||
76
internal/pkg/warewulfd/grub_test.go
Normal file
76
internal/pkg/warewulfd/grub_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package warewulfd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
var grubHandlerTests = []struct {
|
||||
description string
|
||||
url string
|
||||
body string
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{"grub config for node with image", "/grub/00:00:00:ff:ff:ff", "", 200, "10.10.10.10:9873"},
|
||||
{"grub config rendered with tag", "/grub/00:00:00:00:ff:ff", "dracut 10.10.0.1:9873", 200, "10.10.10.11:9873"},
|
||||
}
|
||||
|
||||
func Test_HandleGrub(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
|
||||
env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles:
|
||||
default:
|
||||
image name: suse
|
||||
nodes:
|
||||
n1:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:ff:ff:ff
|
||||
profiles:
|
||||
- default
|
||||
n2:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:00:ff:ff
|
||||
image name: none
|
||||
tags:
|
||||
GrubMenuEntry: dracut`)
|
||||
|
||||
env.WriteFile("/etc/warewulf/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }} {{ .Authority }}")
|
||||
|
||||
dbErr := LoadNodeDB()
|
||||
assert.NoError(t, dbErr)
|
||||
|
||||
conf := warewulfconf.Get()
|
||||
secureFalse := false
|
||||
conf.Warewulf.SecureP = &secureFalse
|
||||
conf.Ipaddr = "10.10.0.1"
|
||||
conf.Ipaddr6 = "fd00:10::1"
|
||||
|
||||
for _, tt := range grubHandlerTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||
req.RemoteAddr = tt.ip
|
||||
w := httptest.NewRecorder()
|
||||
HandleGrub(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, readErr := io.ReadAll(res.Body)
|
||||
assert.NoError(t, readErr)
|
||||
if tt.body != "" {
|
||||
assert.Equal(t, tt.body, string(data))
|
||||
}
|
||||
assert.Equal(t, tt.status, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
60
internal/pkg/warewulfd/initramfs_test.go
Normal file
60
internal/pkg/warewulfd/initramfs_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package warewulfd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
var initramfsHandlerTests = []struct {
|
||||
description string
|
||||
url string
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{"find initramfs", "/initramfs/00:00:00:ff:ff:ff", 200, "10.10.10.10:9873"},
|
||||
}
|
||||
|
||||
func Test_HandleInitramfs(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
|
||||
env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles:
|
||||
default:
|
||||
image name: suse
|
||||
nodes:
|
||||
n1:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:ff:ff:ff
|
||||
profiles:
|
||||
- default`)
|
||||
|
||||
env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/boot/vmlinuz-1.1.0")
|
||||
env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/boot/initramfs-1.1.0.img")
|
||||
|
||||
dbErr := LoadNodeDB()
|
||||
assert.NoError(t, dbErr)
|
||||
|
||||
conf := warewulfconf.Get()
|
||||
secureFalse := false
|
||||
conf.Warewulf.SecureP = &secureFalse
|
||||
|
||||
for _, tt := range initramfsHandlerTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||
req.RemoteAddr = tt.ip
|
||||
w := httptest.NewRecorder()
|
||||
HandleInitramfs(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tt.status, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
80
internal/pkg/warewulfd/ipxe_test.go
Normal file
80
internal/pkg/warewulfd/ipxe_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package warewulfd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
var ipxeHandlerTests = []struct {
|
||||
description string
|
||||
url string
|
||||
body string
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{
|
||||
"ipxe with NetDevs, KernelVersion, and Authority",
|
||||
"/ipxe/00:00:00:00:00:ff",
|
||||
"1.1.1 ifname=net:00:00:00:00:00:ff 10.10.0.1 fd00:10::1 10.10.0.1:9873",
|
||||
200,
|
||||
"10.10.10.12:9873",
|
||||
},
|
||||
{
|
||||
"ipxe over ipv6",
|
||||
"/ipxe/00:00:00:00:00:ff",
|
||||
"1.1.1 ifname=net:00:00:00:00:00:ff 10.10.0.1 fd00:10::1 [fd00:10::1]:9873",
|
||||
200,
|
||||
"[fd00:10::10:12]:9873",
|
||||
},
|
||||
}
|
||||
|
||||
func Test_HandleIpxe(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
|
||||
env.WriteFile("/etc/warewulf/nodes.conf", `nodes:
|
||||
n3:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:00:00:ff
|
||||
device: net
|
||||
ipxe template: test
|
||||
kernel:
|
||||
version: 1.1.1`)
|
||||
|
||||
env.WriteFile("/etc/warewulf/ipxe/test.ipxe", "{{.KernelVersion}}{{range $devname, $netdev := .NetDevs}}{{if and $netdev.Hwaddr $netdev.Device}} ifname={{$netdev.Device}}:{{$netdev.Hwaddr}} {{end}}{{end}} {{.Ipaddr}} {{.Ipaddr6}} {{.Authority}}")
|
||||
|
||||
dbErr := LoadNodeDB()
|
||||
assert.NoError(t, dbErr)
|
||||
|
||||
conf := warewulfconf.Get()
|
||||
secureFalse := false
|
||||
conf.Warewulf.SecureP = &secureFalse
|
||||
conf.Ipaddr = "10.10.0.1"
|
||||
conf.Ipaddr6 = "fd00:10::1"
|
||||
|
||||
for _, tt := range ipxeHandlerTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||
req.RemoteAddr = tt.ip
|
||||
w := httptest.NewRecorder()
|
||||
HandleIpxe(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, readErr := io.ReadAll(res.Body)
|
||||
assert.NoError(t, readErr)
|
||||
if tt.body != "" {
|
||||
assert.Equal(t, tt.body, string(data))
|
||||
}
|
||||
assert.Equal(t, tt.status, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
@@ -108,3 +111,89 @@ nodes:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var systemOverlayTests = []struct {
|
||||
description string
|
||||
url string
|
||||
body string
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{"system overlay", "/system/00:00:00:ff:ff:ff", "system overlay", 200, "10.10.10.10:9873"},
|
||||
{"fake overlay returns 404", "/system/00:00:00:ff:ff:ff?overlay=fake", "", 404, "10.10.10.10:9873"},
|
||||
{"specific overlay", "/system/00:00:00:ff:ff:ff?overlay=o1", "specific overlay", 200, "10.10.10.10:9873"},
|
||||
}
|
||||
|
||||
var runtimeOverlayTests = []struct {
|
||||
description string
|
||||
url string
|
||||
body string
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{"runtime overlay", "/runtime/00:00:00:ff:ff:ff", "runtime overlay", 200, "10.10.10.10:9873"},
|
||||
}
|
||||
|
||||
func Test_HandleSystemRuntimeOverlay(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
|
||||
env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles:
|
||||
default:
|
||||
image name: suse
|
||||
nodes:
|
||||
n1:
|
||||
network devices:
|
||||
default:
|
||||
hwaddr: 00:00:00:ff:ff:ff
|
||||
profiles:
|
||||
- default`)
|
||||
|
||||
dbErr := LoadNodeDB()
|
||||
assert.NoError(t, dbErr)
|
||||
|
||||
conf := warewulfconf.Get()
|
||||
secureFalse := false
|
||||
conf.Warewulf.SecureP = &secureFalse
|
||||
|
||||
assert.NoError(t, os.MkdirAll(path.Join(conf.Paths.OverlayProvisiondir(), "n1"), 0700))
|
||||
assert.NoError(t, os.WriteFile(path.Join(conf.Paths.OverlayProvisiondir(), "n1", "__SYSTEM__.img"), []byte("system overlay"), 0600))
|
||||
assert.NoError(t, os.WriteFile(path.Join(conf.Paths.OverlayProvisiondir(), "n1", "__RUNTIME__.img"), []byte("runtime overlay"), 0600))
|
||||
assert.NoError(t, os.WriteFile(path.Join(conf.Paths.OverlayProvisiondir(), "n1", "o1.img"), []byte("specific overlay"), 0600))
|
||||
|
||||
for _, tt := range systemOverlayTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||
req.RemoteAddr = tt.ip
|
||||
w := httptest.NewRecorder()
|
||||
HandleSystemOverlay(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, readErr := io.ReadAll(res.Body)
|
||||
assert.NoError(t, readErr)
|
||||
if tt.body != "" {
|
||||
assert.Equal(t, tt.body, string(data))
|
||||
}
|
||||
assert.Equal(t, tt.status, res.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range runtimeOverlayTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||
req.RemoteAddr = tt.ip
|
||||
w := httptest.NewRecorder()
|
||||
HandleRuntimeOverlay(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, readErr := io.ReadAll(res.Body)
|
||||
assert.NoError(t, readErr)
|
||||
if tt.body != "" {
|
||||
assert.Equal(t, tt.body, string(data))
|
||||
}
|
||||
assert.Equal(t, tt.status, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func initHandleRequest(w http.ResponseWriter, req *http.Request) (*requestContex
|
||||
|
||||
status_stages := map[string]string{
|
||||
"efiboot": "EFI",
|
||||
"grub": "GRUB",
|
||||
"ipxe": "IPXE",
|
||||
"kernel": "KERNEL",
|
||||
"system": "SYSTEM_OVERLAY",
|
||||
@@ -102,14 +103,23 @@ func parseRequest(req *http.Request) (parsedRequest, error) {
|
||||
// handle when stage was passed in the url path /[stage]/hwaddr
|
||||
stage := path_parts[1]
|
||||
hwaddr := ""
|
||||
if stage != "efiboot" {
|
||||
switch stage {
|
||||
case "efiboot":
|
||||
// /efiboot/{file}: no hwaddr in path; identified via ARP
|
||||
if len(path_parts) > 3 {
|
||||
ret.efifile = strings.Join(path_parts[2:], "/")
|
||||
} else {
|
||||
ret.efifile = path_parts[2]
|
||||
}
|
||||
case "grub":
|
||||
// /grub/{hwaddr}: hwaddr explicit in path
|
||||
hwaddr = path_parts[2]
|
||||
hwaddr = strings.ReplaceAll(hwaddr, "-", ":")
|
||||
hwaddr = strings.ToLower(hwaddr)
|
||||
default:
|
||||
hwaddr = path_parts[2]
|
||||
hwaddr = strings.ReplaceAll(hwaddr, "-", ":")
|
||||
hwaddr = strings.ToLower(hwaddr)
|
||||
} else if len(path_parts) > 3 {
|
||||
ret.efifile = strings.Join(path_parts[2:], "/")
|
||||
} else {
|
||||
ret.efifile = path_parts[2]
|
||||
}
|
||||
ret.hwaddr = hwaddr
|
||||
remoteAddrPort, err := netip.ParseAddrPort(req.RemoteAddr)
|
||||
@@ -137,12 +147,14 @@ func parseRequest(req *http.Request) (parsedRequest, error) {
|
||||
ret.stage = "kernel"
|
||||
case "image", "container":
|
||||
ret.stage = "image"
|
||||
case "overlay-system":
|
||||
case "overlay-system", "system":
|
||||
ret.stage = "system"
|
||||
case "overlay-runtime":
|
||||
case "overlay-runtime", "runtime":
|
||||
ret.stage = "runtime"
|
||||
case "efiboot":
|
||||
ret.stage = "efiboot"
|
||||
case "grub":
|
||||
ret.stage = "grub"
|
||||
case "initramfs":
|
||||
ret.stage = "initramfs"
|
||||
}
|
||||
@@ -154,14 +166,23 @@ func parseRequest(req *http.Request) (parsedRequest, error) {
|
||||
if len(req.URL.Query()["compress"]) > 0 {
|
||||
ret.compress = req.URL.Query()["compress"][0]
|
||||
}
|
||||
if ret.efifile == "" && len(req.URL.Query()["file"]) > 0 {
|
||||
ret.efifile = req.URL.Query()["file"][0]
|
||||
}
|
||||
if ret.stage == "" {
|
||||
return ret, errors.New("no stage encoded in GET")
|
||||
}
|
||||
if ret.hwaddr == "" {
|
||||
ret.hwaddr = ArpFind(ret.ipaddr)
|
||||
wwlog.Verbose("node mac not encoded, arp cache got %s for %s", ret.hwaddr, ret.ipaddr)
|
||||
if ret.hwaddr == "" {
|
||||
return ret, errors.New("no hwaddr encoded in GET")
|
||||
if len(req.URL.Query()["wwid"]) > 0 {
|
||||
ret.hwaddr = req.URL.Query()["wwid"][0]
|
||||
ret.hwaddr = strings.ReplaceAll(ret.hwaddr, "-", ":")
|
||||
ret.hwaddr = strings.ToLower(ret.hwaddr)
|
||||
} else {
|
||||
ret.hwaddr = ArpFind(ret.ipaddr)
|
||||
wwlog.Verbose("node mac not encoded, arp cache got %s for %s", ret.hwaddr, ret.ipaddr)
|
||||
if ret.hwaddr == "" {
|
||||
return ret, errors.New("no hwaddr encoded in GET")
|
||||
}
|
||||
}
|
||||
}
|
||||
if ret.ipaddr == "" {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
var parseReqTests = []struct {
|
||||
description string
|
||||
url string
|
||||
rawQuery string
|
||||
remoteAddr string
|
||||
result parsedRequest
|
||||
}{
|
||||
@@ -36,13 +37,143 @@ var parseReqTests = []struct {
|
||||
stage: "ipxe",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "initramfs dedicated route",
|
||||
url: "/initramfs/00:00:00:ff:ff:ff",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "initramfs",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "grub route with hwaddr in path",
|
||||
url: "/grub/00:00:00:ff:ff:ff",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "grub",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "wwid query param on dedicated route",
|
||||
url: "/kernel/",
|
||||
rawQuery: "wwid=00%3A00%3A00%3Aff%3Aff%3Aff",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "kernel",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "wwid and stage query params on provision route",
|
||||
url: "/provision/",
|
||||
rawQuery: "wwid=00%3A00%3A00%3Aff%3Aff%3Aff&stage=kernel",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "kernel",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "efiboot with wwid and file query params",
|
||||
url: "/efiboot/",
|
||||
rawQuery: "wwid=00%3A00%3A00%3Aff%3Aff%3Aff&file=shim.efi",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "efiboot",
|
||||
efifile: "shim.efi",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "grub with wwid query param",
|
||||
url: "/grub/",
|
||||
rawQuery: "wwid=00%3A00%3A00%3Aff%3Aff%3Aff",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "grub",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "path hwaddr takes priority over wwid query param",
|
||||
url: "/kernel/00:00:00:ff:ff:ff",
|
||||
rawQuery: "wwid=11%3A11%3A11%3A11%3A11%3A11",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "kernel",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "system overlay dedicated route",
|
||||
url: "/system/00:00:00:ff:ff:ff",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "system",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "runtime overlay dedicated route",
|
||||
url: "/runtime/00:00:00:ff:ff:ff",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "runtime",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "stage=grub via provision route serves config (new semantics)",
|
||||
url: "/provision/00:00:00:ff:ff:ff",
|
||||
rawQuery: "stage=grub",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "grub",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "path efifile takes priority over file query param",
|
||||
url: "/efiboot/shim.efi",
|
||||
rawQuery: "wwid=00%3A00%3A00%3Aff%3Aff%3Aff&file=grub.cfg",
|
||||
remoteAddr: "10.5.1.1:9873",
|
||||
result: parsedRequest{
|
||||
hwaddr: "00:00:00:ff:ff:ff",
|
||||
ipaddr: "10.5.1.1",
|
||||
remoteport: 9873,
|
||||
stage: "efiboot",
|
||||
efifile: "shim.efi",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_ParseRequest(t *testing.T) {
|
||||
for _, tt := range parseReqTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
URL: &url.URL{Path: tt.url},
|
||||
URL: &url.URL{Path: tt.url, RawQuery: tt.rawQuery},
|
||||
RemoteAddr: tt.remoteAddr,
|
||||
}
|
||||
result, err := parseRequest(req)
|
||||
|
||||
@@ -51,10 +51,6 @@ func HandleProvision(w http.ResponseWriter, req *http.Request) {
|
||||
handler = HandleSystemOverlay
|
||||
case "runtime":
|
||||
handler = HandleRuntimeOverlay
|
||||
case "efiboot":
|
||||
handler = HandleEfiBoot
|
||||
case "shim":
|
||||
handler = HandleShim
|
||||
case "grub":
|
||||
handler = HandleGrub
|
||||
case "initramfs":
|
||||
|
||||
@@ -21,18 +21,15 @@ var provisionSendTests = []struct {
|
||||
status int
|
||||
ip string
|
||||
}{
|
||||
{"system overlay", "/overlay-system/00:00:00:ff:ff:ff", "system overlay", 200, "10.10.10.10:9873"},
|
||||
{"runtime overlay", "/overlay-runtime/00:00:00:ff:ff:ff", "runtime overlay", 200, "10.10.10.10:9873"},
|
||||
{"fake overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=fake", "", 404, "10.10.10.10:9873"},
|
||||
{"specific overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=o1", "specific overlay", 200, "10.10.10.10:9873"},
|
||||
{"find shim", "/efiboot/shim.efi", "", 200, "10.10.10.10:9873"},
|
||||
{"find shim", "/efiboot/shim.efi", "", 404, "10.10.10.11:9873"},
|
||||
{"find grub", "/efiboot/grub.efi", "", 200, "10.10.10.10:9873"},
|
||||
{"find grub", "/efiboot/grub.efi", "", 404, "10.10.10.11:9873"},
|
||||
{"system overlay", "/provision/00:00:00:ff:ff:ff?stage=system", "system overlay", 200, "10.10.10.10:9873"},
|
||||
{"runtime overlay", "/provision/00:00:00:ff:ff:ff?stage=runtime", "runtime overlay", 200, "10.10.10.10:9873"},
|
||||
{"fake overlay", "/provision/00:00:00:ff:ff:ff?stage=system&overlay=fake", "", 404, "10.10.10.10:9873"},
|
||||
{"specific overlay", "/provision/00:00:00:ff:ff:ff?stage=system&overlay=o1", "specific overlay", 200, "10.10.10.10:9873"},
|
||||
{"grub config", "/provision/00:00:00:ff:ff:ff?stage=grub", "", 200, "10.10.10.10:9873"},
|
||||
{"grub config rendered", "/provision/00:00:00:00:ff:ff?stage=grub", "dracut 10.10.0.1:9873", 200, "10.10.10.11:9873"},
|
||||
{"find initramfs", "/provision/00:00:00:ff:ff:ff?stage=initramfs", "", 200, "10.10.10.10:9873"},
|
||||
{"ipxe test with NetDevs, KernelVersion, and Authority", "/provision/00:00:00:00:00:ff?stage=ipxe", "1.1.1 ifname=net:00:00:00:00:00:ff 10.10.0.1 fd00:10::1 10.10.0.1:9873", 200, "10.10.10.12:9873"},
|
||||
{"ipxe ipv6", "/provision/00:00:00:00:00:ff?stage=ipxe", "1.1.1 ifname=net:00:00:00:00:00:ff 10.10.0.1 fd00:10::1 [fd00:10::1]:9873", 200, "[fd00:10::10:12]:9873"},
|
||||
{"find grub.cfg", "/efiboot/grub.cfg", "dracut 10.10.0.1:9873", 200, "10.10.10.11:9873"},
|
||||
}
|
||||
|
||||
func Test_ProvisionSend(t *testing.T) {
|
||||
|
||||
@@ -42,13 +42,19 @@ func configureRootHandler(apiHandler http.Handler) *slashFix {
|
||||
wwHandler.HandleFunc("/provision/", warewulfd.HandleProvision)
|
||||
wwHandler.HandleFunc("/ipxe/", warewulfd.HandleIpxe)
|
||||
wwHandler.HandleFunc("/efiboot/", warewulfd.HandleEfiBoot)
|
||||
wwHandler.HandleFunc("/grub/", warewulfd.HandleGrub)
|
||||
wwHandler.HandleFunc("/kernel/", warewulfd.HandleKernel)
|
||||
wwHandler.HandleFunc("/image/", warewulfd.HandleImage)
|
||||
wwHandler.HandleFunc("/initramfs/", warewulfd.HandleInitramfs)
|
||||
wwHandler.HandleFunc("/system/", warewulfd.HandleSystemOverlay)
|
||||
wwHandler.HandleFunc("/runtime/", warewulfd.HandleRuntimeOverlay)
|
||||
wwHandler.HandleFunc("/status", warewulfd.HandleStatus)
|
||||
|
||||
/* Deprecated */
|
||||
wwHandler.HandleFunc("/container/", warewulfd.HandleImage)
|
||||
wwHandler.HandleFunc("/overlay-system/", warewulfd.HandleSystemOverlay)
|
||||
wwHandler.HandleFunc("/overlay-runtime/", warewulfd.HandleRuntimeOverlay)
|
||||
wwHandler.HandleFunc("/overlay-file/", warewulfd.HandleOverlayFile)
|
||||
wwHandler.HandleFunc("/status", warewulfd.HandleStatus)
|
||||
|
||||
if apiHandler != nil {
|
||||
wwHandler.Handle("/api/", apiHandler)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ Welcome to the Warewulf User Guide!
|
||||
Server Installation <server/installation>
|
||||
Controlling Warewulf (wwctl) <server/wwctl>
|
||||
Server Configuration <server/configuration>
|
||||
Server Routes <server/routes>
|
||||
Using dnsmasq <server/dnsmasq>
|
||||
Security <server/security>
|
||||
Bootloaders <server/bootloaders>
|
||||
|
||||
322
userdocs/server/routes.rst
Normal file
322
userdocs/server/routes.rst
Normal file
@@ -0,0 +1,322 @@
|
||||
.. _server-routes:
|
||||
|
||||
=============
|
||||
Server Routes
|
||||
=============
|
||||
|
||||
The Warewulf provisioning daemon, ``warewulfd``, serves all boot and provisioning
|
||||
resources over HTTP. Each resource type has a dedicated route, and nodes are
|
||||
identified by their Warewulf ID (``wwid``).
|
||||
|
||||
``{wwid}`` is typically the node's default MAC address: a colon-separated
|
||||
hexadecimal string, e.g., ``aa:bb:cc:dd:ee:ff``. Dashes are accepted in place
|
||||
of colons and are normalized automatically.
|
||||
|
||||
.. note::
|
||||
|
||||
The port ``warewulfd`` listens on is configured with ``warewulf:port`` in
|
||||
``warewulf.conf`` (default: ``9873``). When TLS is enabled, a second listener
|
||||
is started on the port configured with ``warewulf:tls port`` (default: ``9874``).
|
||||
|
||||
URL Patterns
|
||||
============
|
||||
|
||||
Every provisioning route (except ``/overlay-file/`` and ``/status``) supports
|
||||
six equivalent URL patterns for specifying the node identity:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
/{stage}/{wwid} # wwid in path
|
||||
/{stage}?wwid={wwid} # wwid as query parameter
|
||||
/{stage} # wwid resolved from ARP cache
|
||||
|
||||
/provision/{wwid}?stage={stage}
|
||||
/provision?wwid={wwid}&stage={stage}
|
||||
/provision?stage={stage} # wwid resolved from ARP cache
|
||||
|
||||
The ``/efiboot/`` route is an exception: the path segment contains the boot
|
||||
file name rather than a wwid, and the node is identified via ``?wwid=`` or ARP:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
/efiboot/{file} # file in path, wwid from ARP
|
||||
/efiboot/{file}?wwid={wwid} # file in path, explicit wwid
|
||||
/efiboot?wwid={wwid}&file={file} # both as query parameters
|
||||
|
||||
Common Query Parameters
|
||||
=======================
|
||||
|
||||
Most provisioning routes accept the following query parameters:
|
||||
|
||||
* ``wwid``: Warewulf ID of the node, typically its default MAC address. Used
|
||||
when the node identity is not embedded in the URL path. Takes priority over
|
||||
ARP lookup; the path always takes priority over the ``wwid`` query parameter.
|
||||
|
||||
* ``assetkey``: Hardware asset tag. If the node has an ``AssetKey`` configured
|
||||
in ``nodes.conf``, the server requires this parameter to match before serving
|
||||
any content. See :ref:`Security <server-routes-security>` below.
|
||||
|
||||
* ``uuid``: System UUID of the requesting node. Accepted for logging purposes.
|
||||
|
||||
* ``compress``: Compression format for the response. The only supported value
|
||||
is ``gz``. When ``compress=gz`` is specified, the server serves a pre-built
|
||||
gzip-compressed version of the file. If no compressed version exists, the
|
||||
server returns ``404 Not Found``.
|
||||
|
||||
Provisioning Routes
|
||||
===================
|
||||
|
||||
``/ipxe/{wwid}``
|
||||
----------------
|
||||
|
||||
Serves a rendered iPXE boot script for the node identified by ``{wwid}``.
|
||||
|
||||
The script is rendered as a Go template from a file in
|
||||
``/etc/warewulf/ipxe/``. The specific template used is determined by the
|
||||
node's ``Ipxe`` field (defaulting to ``default``); for example, a node with
|
||||
``Ipxe: dracut`` receives the template from
|
||||
``/etc/warewulf/ipxe/dracut.ipxe``.
|
||||
|
||||
If the requesting node is not known to Warewulf, the server falls back to
|
||||
serving ``/etc/warewulf/ipxe/unconfigured.ipxe``.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``
|
||||
|
||||
``/kernel/{wwid}``
|
||||
------------------
|
||||
|
||||
Serves the raw kernel binary for the node identified by ``{wwid}``. The
|
||||
kernel is taken from the node's assigned image.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``compress``
|
||||
|
||||
``/image/{wwid}``
|
||||
-----------------
|
||||
|
||||
Serves the raw node image file for the node identified by ``{wwid}``.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``compress``
|
||||
|
||||
``/initramfs/{wwid}``
|
||||
---------------------
|
||||
|
||||
Serves the initramfs binary for the node identified by ``{wwid}``. The
|
||||
initramfs is extracted from the node's assigned image based on the node's
|
||||
kernel version. This route is used in two-stage boot configurations. See
|
||||
:ref:`booting with dracut` for details.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``compress``
|
||||
|
||||
``/system/{wwid}``
|
||||
------------------
|
||||
|
||||
Serves the system overlay image for the node identified by ``{wwid}``.
|
||||
The system overlay is rendered at provisioning time and contains
|
||||
configuration files that are static for the lifetime of the boot.
|
||||
|
||||
When ``autobuild overlays`` is enabled in ``warewulf.conf``, the server
|
||||
will automatically rebuild the overlay if it is out of date relative to
|
||||
``nodes.conf`` or the overlay source files.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``compress``, ``overlay``
|
||||
|
||||
* ``overlay``: A comma-separated list of overlay names. When specified, only
|
||||
the named overlays are served (rather than the node's full system overlay
|
||||
set).
|
||||
|
||||
``/runtime/{wwid}``
|
||||
-------------------
|
||||
|
||||
Serves the runtime overlay image for the node identified by ``{wwid}``.
|
||||
The runtime overlay is rendered on demand and may contain node-specific
|
||||
secrets. ``wwclient`` fetches the runtime overlay periodically during normal
|
||||
operation.
|
||||
|
||||
When ``warewulf:secure`` is enabled in ``warewulf.conf``, this route requires
|
||||
that the request originate from a privileged TCP port (port number less than
|
||||
1024). This prevents unprivileged users on a node from retrieving the runtime
|
||||
overlay.
|
||||
|
||||
When TLS is enabled in ``warewulf.conf``, this route requires that the request
|
||||
arrive over HTTPS. Plain-HTTP requests are rejected with ``403 Forbidden``. The
|
||||
HTTPS listener port is configured with ``warewulf:tls port``.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``compress``, ``overlay``
|
||||
|
||||
* ``overlay``: A comma-separated list of overlay names. Same behavior as for
|
||||
``/system/``.
|
||||
|
||||
``/overlay-file/{overlay}/{path}``
|
||||
----------------------------------
|
||||
|
||||
Provides direct access to an individual file within a named overlay. This
|
||||
route uses a different URL structure than the other provisioning routes: the
|
||||
overlay name is in the second path segment, and the file path within the overlay
|
||||
follows.
|
||||
|
||||
If the ``render`` parameter is provided, the file is rendered as a Go template
|
||||
for the specified node and the rendered content is returned. If ``render`` is
|
||||
absent, the raw file bytes are returned without any template processing.
|
||||
|
||||
If the requested path does not end in ``.ww`` but a ``.ww``-suffixed version of
|
||||
the file exists, and a ``render`` node is specified, the server automatically
|
||||
serves the ``.ww`` template.
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
* ``render``: Node ID to render the template for. If not specified, the raw
|
||||
file is returned.
|
||||
|
||||
.. note::
|
||||
|
||||
This route does not require authentication via ``assetkey`` and does not
|
||||
perform node lookup by hardware address.
|
||||
|
||||
``/efiboot/{file}``
|
||||
-------------------
|
||||
|
||||
Serves EFI boot files for GRUB-based booting. The requesting node is
|
||||
identified by ``?wwid=`` (preferred) or, if not supplied, by an ARP lookup
|
||||
of the client's IP address against the kernel's ARP cache (``/proc/net/arp``).
|
||||
This route is intended for EFI HTTP Boot clients, where the firmware fetches
|
||||
a boot URI from DHCP and cannot perform variable substitution.
|
||||
|
||||
The ``{file}`` component determines what is served:
|
||||
|
||||
* ``shim.efi``: Serves the ``shim.efi`` binary extracted from the node's
|
||||
assigned image.
|
||||
* ``grub.efi`` (or ``grubx64.efi``, ``grubaa64.efi``, ``grubia32.efi``,
|
||||
``grubarm.efi``, ``grub-tpm.efi``): Serves the GRUB EFI binary extracted
|
||||
from the node's assigned image.
|
||||
* ``grub.cfg``: Serves a rendered GRUB configuration file from
|
||||
``/etc/warewulf/grub/grub.cfg.ww``. The configuration is rendered as a Go
|
||||
template for the identified node.
|
||||
|
||||
Because ``shim.efi`` resolves subsequent files relative to its own load URL,
|
||||
GRUB and ``grub.cfg`` are also fetched from the ``/efiboot/`` path. The
|
||||
``grub.cfg`` served by this route uses ``${net_default_mac}`` to embed the
|
||||
node's wwid in all further provisioning URLs, directing subsequent requests
|
||||
to the per-node ``/grub/{wwid}`` route.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``wwid``, ``file``
|
||||
|
||||
* ``file``: The EFI file to serve (``shim.efi``, ``grub.efi``, or ``grub.cfg``).
|
||||
Used when the file name is not embedded in the URL path.
|
||||
|
||||
.. note::
|
||||
|
||||
``/efiboot/`` is the recommended route for EFI HTTP Boot clients. For
|
||||
TFTP-booted GRUB clients that know their own wwid, use
|
||||
``/grub/{wwid}`` to fetch the per-node GRUB configuration directly.
|
||||
|
||||
``/grub/{wwid}``
|
||||
----------------
|
||||
|
||||
Serves a rendered GRUB configuration file for the node identified by
|
||||
``{wwid}``. The configuration is rendered from
|
||||
``/etc/warewulf/grub/grub.cfg.ww`` as a Go template. This route is the
|
||||
preferred method for TFTP-booted GRUB clients to fetch their per-node
|
||||
configuration, as the node identity is explicit in the URL rather than
|
||||
resolved via ARP.
|
||||
|
||||
**Query parameters:** ``assetkey``, ``uuid``, ``wwid``
|
||||
|
||||
``/provision/{wwid}``
|
||||
---------------------
|
||||
|
||||
.. deprecated::
|
||||
|
||||
This route is maintained for backwards compatibility. New configurations
|
||||
should use the dedicated routes described above.
|
||||
|
||||
A legacy dispatcher route. The provisioning stage is determined by the
|
||||
``stage`` query parameter, which is dispatched to the appropriate handler:
|
||||
|
||||
* ``stage=ipxe`` → ``/ipxe/``
|
||||
* ``stage=kernel`` → ``/kernel/``
|
||||
* ``stage=image`` → ``/image/``
|
||||
* ``stage=initramfs`` → ``/initramfs/``
|
||||
* ``stage=system`` → ``/system/``
|
||||
* ``stage=runtime`` → ``/runtime/``
|
||||
* ``stage=grub`` → ``/grub/``
|
||||
|
||||
**Query parameters:** ``stage`` (required), ``assetkey``, ``uuid``, ``compress``,
|
||||
``overlay``
|
||||
|
||||
Status Route
|
||||
============
|
||||
|
||||
``/status``
|
||||
-----------
|
||||
|
||||
Returns a JSON object containing the last-known provisioning status for all
|
||||
nodes that have contacted the server. No authentication is required.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
{
|
||||
"nodes": {
|
||||
"node01": {
|
||||
"node name": "node01",
|
||||
"stage": "RUNTIME_OVERLAY",
|
||||
"sent": "2 kB",
|
||||
"ipaddr": "10.0.1.1",
|
||||
"last seen": 1712345678
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
The ``stage`` field reflects the most recent provisioning stage completed for
|
||||
the node. Possible values include ``IPXE``, ``KERNEL``, ``IMAGE``,
|
||||
``INITRAMFS``, ``SYSTEM_OVERLAY``, ``RUNTIME_OVERLAY``, and ``EFI``.
|
||||
|
||||
REST API
|
||||
========
|
||||
|
||||
When enabled in ``warewulf.conf``, ``warewulfd`` exposes a REST API under
|
||||
``/api/``. The API provides programmatic access to nodes, profiles, images,
|
||||
and overlays. Interactive documentation is available at ``/api/docs``.
|
||||
|
||||
See :ref:`rest-api` for full details.
|
||||
|
||||
.. _server-routes-security:
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
Several mechanisms are available to restrict access to provisioning routes.
|
||||
|
||||
Asset key validation
|
||||
--------------------
|
||||
|
||||
If a node is configured with an ``AssetKey`` in ``nodes.conf``, the Warewulf
|
||||
server will only respond to provisioning requests that include a matching
|
||||
``?assetkey=`` query parameter. Requests with a missing or incorrect asset key
|
||||
receive ``401 Unauthorized``. The asset key is typically a hardware-level
|
||||
firmware string (an "asset tag") that is accessible only with root or physical
|
||||
access.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# wwctl node set node01 --assetkey "SYSTEM-ASSET-TAG"
|
||||
|
||||
Secure mode
|
||||
-----------
|
||||
|
||||
When ``warewulf:secure`` is set to ``true`` in ``warewulf.conf``, the
|
||||
``/runtime/`` route requires that requests originate from a privileged
|
||||
TCP source port (port number less than 1024). Because only processes running as
|
||||
``root`` can bind to privileged ports, this prevents unprivileged users on a
|
||||
cluster node from downloading the runtime overlay.
|
||||
|
||||
TLS
|
||||
---
|
||||
|
||||
When TLS is enabled in ``warewulf.conf``, the ``/runtime/`` route
|
||||
rejects plain-HTTP requests with ``403 Forbidden``. Runtime overlays must be
|
||||
fetched over HTTPS. Because iPXE and GRUB cannot handle HTTPS, the kernel,
|
||||
image, and system overlay continue to be served over plain HTTP even when TLS
|
||||
is enabled.
|
||||
|
||||
See :ref:`Security <server/security>` for instructions on enabling TLS and
|
||||
generating certificates.
|
||||
Reference in New Issue
Block a user