IPv6 iPXE support

* Parse address and port with netip w/ tests
* Add Authority to provisioning templates based on remote IP family
  (name based on RFC 3986)
* Add IPv6 support to the default iPXE (tested)
* Update `grub.cfg.ww` to use Authority (untested)

Co-authored-by: Dacian Reece-Stremtan <dacianstremtan@gmail.com>
Co-authored-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
Signed-off-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
This commit is contained in:
Dacian Reece-Stremtan
2025-08-01 13:43:57 -05:00
committed by Jonathon Anderson
parent beb77278c6
commit efd63aecd4
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 `wwctl configure warewulfd`
- Added troubleshooting documentation regarding NUMA and tmpfs
- Added support for IPv6 when booting with iPXE. #1852
### Changed
- Restore default idempotency of `PUT /api/nodes/{id}`
- `DELETE /api/overlays/{name}?force=true` can delete overlays that are in use
- `warewulfd` overlay autobuild rebuilds overlays after node discovery. #1468
### Fixed
- Improved netplan support. #1873
### Fixed

View File

@@ -47,3 +47,4 @@
* Stephen Simpson [@ssimpson89](https://github.com/ssimpson89)
* Rafael Lopez <raflopez1@gmail.com> @rafalop
* 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 "* Ipaddr: {{.Ipaddr}}"
echo "* Port: {{.Port}}"
echo "* Authority: {{.Authority}}"
echo
echo "This node:"
echo "* Fqdn: {{.Fqdn}}"
@@ -28,7 +29,7 @@ reboot
echo "Reading asset key..."
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"
set default={{ or .Tags.GrubMenuEntry "single-stage" }}

View File

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

View File

@@ -2,7 +2,7 @@ package warewulfd
import (
"net/http"
"strconv"
"net/netip"
"strings"
"github.com/pkg/errors"
@@ -44,9 +44,12 @@ func parseReq(req *http.Request) (parserInfo, error) {
ret.efifile = path_parts[2]
}
ret.hwaddr = hwaddr
ret.ipaddr = strings.Split(req.RemoteAddr, ":")[0]
ret.remoteport, _ = strconv.Atoi(strings.Split(req.RemoteAddr, ":")[1])
remoteAddrPort, err := netip.ParseAddrPort(req.RemoteAddr)
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 {
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"
"fmt"
"net/http"
"net/netip"
"path"
"path/filepath"
"strconv"
@@ -32,7 +33,9 @@ type templateVars struct {
Ipxe string
Hwaddr string
Ipaddr string
Ipaddr6 string
Port string
Authority string
KernelArgs string
KernelVersion string
Root string
@@ -115,12 +118,30 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
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{
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,
@@ -203,12 +224,30 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
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{
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,

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"},
{"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"},
{"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"},
{"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"},
{"find grub.cfg", "/efiboot/grub.cfg", "dracut", 200, "10.10.10.11: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) {
env := testenv.New(t)
defer env.RemoveAll()
env.WriteFile("etc/warewulf/nodes.conf", `nodeprofiles:
env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles:
default:
image name: suse
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/share/efi/x86_64/grub.efi")
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/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }}")
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 }} {{ .Authority }}")
dbErr := LoadNodeDB()
assert.NoError(t, dbErr)
@@ -87,6 +88,8 @@ nodes:
conf := warewulfconf.Get()
secureFalse := false
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.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))