Merge pull request #1958 from middelkoopt/tm-ipv6-pxe

Add IPv6 support for iPXE booting
This commit is contained in:
Jonathon Anderson
2025-08-01 14:56:29 -06:00
committed by GitHub
8 changed files with 117 additions and 12 deletions

View File

@@ -14,12 +14,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added `DELETE /api/overlays/{name}/file?path={path}` - Added `DELETE /api/overlays/{name}/file?path={path}`
- Added `wwctl configure warewulfd` - Added `wwctl configure warewulfd`
- Added troubleshooting documentation regarding NUMA and tmpfs - Added troubleshooting documentation regarding NUMA and tmpfs
- Added support for IPv6 when booting with iPXE. #1852
### Changed ### Changed
- Restore default idempotency of `PUT /api/nodes/{id}` - Restore default idempotency of `PUT /api/nodes/{id}`
- `DELETE /api/overlays/{name}?force=true` can delete overlays that are in use - `DELETE /api/overlays/{name}?force=true` can delete overlays that are in use
- `warewulfd` overlay autobuild rebuilds overlays after node discovery. #1468 - `warewulfd` overlay autobuild rebuilds overlays after node discovery. #1468
### Fixed
- Improved netplan support. #1873 - Improved netplan support. #1873
### Fixed ### Fixed

View File

@@ -47,3 +47,4 @@
* Stephen Simpson [@ssimpson89](https://github.com/ssimpson89) * Stephen Simpson [@ssimpson89](https://github.com/ssimpson89)
* Rafael Lopez <raflopez1@gmail.com> @rafalop * Rafael Lopez <raflopez1@gmail.com> @rafalop
* Arian Cabrera [@acabrera86](https://github.com/acabrera86) * Arian Cabrera [@acabrera86](https://github.com/acabrera86)
* Dacian Reece-Stremtan <dacianstremtan@gmail.com> [@dacianstremtan](https://github.com/dacianstremtan)

View File

@@ -4,6 +4,7 @@ echo
echo "Warewulf Server:" echo "Warewulf Server:"
echo "* Ipaddr: {{.Ipaddr}}" echo "* Ipaddr: {{.Ipaddr}}"
echo "* Port: {{.Port}}" echo "* Port: {{.Port}}"
echo "* Authority: {{.Authority}}"
echo echo
echo "This node:" echo "This node:"
echo "* Fqdn: {{.Fqdn}}" echo "* Fqdn: {{.Fqdn}}"
@@ -28,7 +29,7 @@ reboot
echo "Reading asset key..." echo "Reading asset key..."
smbios --type 3 --get-string 8 --set assetkey smbios --type 3 --get-string 8 --set assetkey
uri="(http,{{.Ipaddr}}:{{.Port}})/provision/${net_default_mac}?assetkey=${assetkey}" uri="(http,{{.Authority}})/provision/${net_default_mac}?assetkey=${assetkey}"
kernel="${uri}&stage=kernel" kernel="${uri}&stage=kernel"
set default={{ or .Tags.GrubMenuEntry "single-stage" }} set default={{ or .Tags.GrubMenuEntry "single-stage" }}

View File

@@ -15,7 +15,7 @@ sleep 30
reboot reboot
{{- end }} {{- end }}
set baseuri http://{{.Ipaddr}}:{{.Port}}/provision/{{.Hwaddr}} set baseuri http://{{.Authority}}/provision/{{.Hwaddr}}
set uri ${baseuri}?assetkey=${asset}&uuid=${uuid} set uri ${baseuri}?assetkey=${asset}&uuid=${uuid}
echo Downloading kernel image... echo Downloading kernel image...
@@ -115,6 +115,7 @@ goto menu
echo Warewulf Server: echo Warewulf Server:
echo * Ipaddr: {{.Ipaddr}} echo * Ipaddr: {{.Ipaddr}}
echo * Port: {{.Port}} echo * Port: {{.Port}}
echo * Authority: {{.Authority}}
echo echo
echo This node: echo This node:
echo * Fqdn: {{.Fqdn}} echo * Fqdn: {{.Fqdn}}

View File

@@ -2,7 +2,7 @@ package warewulfd
import ( import (
"net/http" "net/http"
"strconv" "net/netip"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -44,9 +44,12 @@ func parseReq(req *http.Request) (parserInfo, error) {
ret.efifile = path_parts[2] ret.efifile = path_parts[2]
} }
ret.hwaddr = hwaddr ret.hwaddr = hwaddr
ret.ipaddr = strings.Split(req.RemoteAddr, ":")[0] remoteAddrPort, err := netip.ParseAddrPort(req.RemoteAddr)
ret.remoteport, _ = strconv.Atoi(strings.Split(req.RemoteAddr, ":")[1]) if err != nil {
return ret, errors.New("could not parse remote address")
}
ret.ipaddr = remoteAddrPort.Addr().String()
ret.remoteport = int(remoteAddrPort.Port())
if len(req.URL.Query()["assetkey"]) > 0 { if len(req.URL.Query()["assetkey"]) > 0 {
ret.assetkey = req.URL.Query()["assetkey"][0] ret.assetkey = req.URL.Query()["assetkey"][0]
} }

View File

@@ -0,0 +1,53 @@
package warewulfd
import (
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
var parseReqTests = []struct {
description string
url string
remoteAddr string
result parserInfo
}{
{
description: "basic ipv4 request",
url: "/provision/00:00:00:ff:ff:ff",
remoteAddr: "10.5.1.1:9873",
result: parserInfo{
hwaddr: "00:00:00:ff:ff:ff",
ipaddr: "10.5.1.1",
remoteport: 9873,
stage: "ipxe",
},
},
{
description: "basic ipv6 request",
url: "/provision/00:00:00:ff:ff:ff",
remoteAddr: "[fd00:5::1:1]:9873",
result: parserInfo{
hwaddr: "00:00:00:ff:ff:ff",
ipaddr: "fd00:5::1:1",
remoteport: 9873,
stage: "ipxe",
},
},
}
func Test_ParseReq(t *testing.T) {
for _, tt := range parseReqTests {
t.Run(tt.description, func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Path: tt.url},
RemoteAddr: tt.remoteAddr,
}
result, err := parseReq(req)
assert.NoError(t, err)
assert.Equal(t, tt.result, result)
})
}
}

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/netip"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -32,7 +33,9 @@ type templateVars struct {
Ipxe string Ipxe string
Hwaddr string Hwaddr string
Ipaddr string Ipaddr string
Ipaddr6 string
Port string Port string
Authority string
KernelArgs string KernelArgs string
KernelVersion string KernelVersion string
Root string Root string
@@ -115,12 +118,30 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
kernelVersion = kernel_.Version() kernelVersion = kernel_.Version()
} }
} }
authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port)
ipaddr6 := ""
if confIpaddr6, err := netip.ParsePrefix(conf.Ipaddr6); err == nil {
ipaddr6 = confIpaddr6.Addr().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{ tmpl_data = &templateVars{
Id: remoteNode.Id(), Id: remoteNode.Id(),
Cluster: remoteNode.ClusterName, Cluster: remoteNode.ClusterName,
Fqdn: remoteNode.Id(), Fqdn: remoteNode.Id(),
Ipaddr: conf.Ipaddr, Ipaddr: conf.Ipaddr,
Ipaddr6: ipaddr6,
Port: strconv.Itoa(conf.Warewulf.Port), Port: strconv.Itoa(conf.Warewulf.Port),
Authority: authority,
Hostname: remoteNode.Id(), Hostname: remoteNode.Id(),
Hwaddr: rinfo.hwaddr, Hwaddr: rinfo.hwaddr,
ImageName: remoteNode.ImageName, ImageName: remoteNode.ImageName,
@@ -203,12 +224,30 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
kernelVersion = kernel_.Version() kernelVersion = kernel_.Version()
} }
} }
authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port)
ipaddr6 := ""
if confIpaddr6, err := netip.ParsePrefix(conf.Ipaddr6); err == nil {
ipaddr6 = confIpaddr6.Addr().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{ tmpl_data = &templateVars{
Id: remoteNode.Id(), Id: remoteNode.Id(),
Cluster: remoteNode.ClusterName, Cluster: remoteNode.ClusterName,
Fqdn: remoteNode.Id(), Fqdn: remoteNode.Id(),
Ipaddr: conf.Ipaddr, Ipaddr: conf.Ipaddr,
Ipaddr6: ipaddr6,
Port: strconv.Itoa(conf.Warewulf.Port), Port: strconv.Itoa(conf.Warewulf.Port),
Authority: authority,
Hostname: remoteNode.Id(), Hostname: remoteNode.Id(),
Hwaddr: rinfo.hwaddr, Hwaddr: rinfo.hwaddr,
ImageName: remoteNode.ImageName, ImageName: remoteNode.ImageName,

View File

@@ -23,22 +23,23 @@ var provisionSendTests = []struct {
}{ }{
{"system overlay", "/overlay-system/00:00:00:ff:ff:ff", "system overlay", 200, "10.10.10.10:9873"}, {"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"}, {"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: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"}, {"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", "", 200, "10.10.10.10:9873"},
{"find shim", "/efiboot/shim.efi", "", 404, "10.10.10.11: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", "", 200, "10.10.10.10:9873"},
{"find grub", "/efiboot/grub.efi", "", 404, "10.10.10.11:9873"}, {"find grub", "/efiboot/grub.efi", "", 404, "10.10.10.11:9873"},
{"find initramfs", "/provision/00:00:00:ff:ff:ff?stage=initramfs", "", 200, "10.10.10.10:9873"}, {"find initramfs", "/provision/00:00:00:ff:ff:ff?stage=initramfs", "", 200, "10.10.10.10:9873"},
{"ipxe test with NetDevs and KernelVersion", "/provision/00:00:00:00:00:ff?stage=ipxe", "1.1.1 ifname=net:00:00:00:00:00:ff ", 200, "10.10.10.12: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"},
{"find grub.cfg", "/efiboot/grub.cfg", "dracut", 200, "10.10.10.11: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) { func Test_ProvisionSend(t *testing.T) {
env := testenv.New(t) env := testenv.New(t)
defer env.RemoveAll() defer env.RemoveAll()
env.WriteFile("etc/warewulf/nodes.conf", `nodeprofiles: env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles:
default: default:
image name: suse image name: suse
nodes: nodes:
@@ -78,8 +79,8 @@ nodes:
env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/usr/lib64/efi/shim.efi") 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.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/usr/share/efi/x86_64/grub.efi")
env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/boot/initramfs-1.1.0.img") env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/boot/initramfs-1.1.0.img")
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}}") 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}}")
env.WriteFile("/etc/warewulf/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }}") env.WriteFile("/etc/warewulf/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }} {{ .Authority }}")
dbErr := LoadNodeDB() dbErr := LoadNodeDB()
assert.NoError(t, dbErr) assert.NoError(t, dbErr)
@@ -87,6 +88,8 @@ nodes:
conf := warewulfconf.Get() conf := warewulfconf.Get()
secureFalse := false secureFalse := false
conf.Warewulf.SecureP = &secureFalse conf.Warewulf.SecureP = &secureFalse
conf.Ipaddr = "10.10.0.1"
conf.Ipaddr6 = "fd00:10::1/64"
assert.NoError(t, os.MkdirAll(path.Join(conf.Paths.OverlayProvisiondir(), "n1"), 0700)) 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", "__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", "__RUNTIME__.img"), []byte("runtime overlay"), 0600))