Files
nfs/internal/nfs4server/pnfs_test.go
T
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

328 lines
9.9 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4server
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// layoutStateidOf pulls the layout stateid out of a LAYOUTGET response
// body and checks the response shape.
func layoutStateidOf(t *testing.T, body []byte) nfs4.Stateid {
t.Helper()
d := xdr.NewDecoder(body)
var st nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
t.Fatalf("layout stateid: %v", err)
}
copy(st[:], raw)
roc, err := d.Bool()
if err != nil || roc {
t.Fatalf("return on close %v: %v", roc, err)
}
n, err := d.Uint32()
if err != nil || n != 1 {
t.Fatalf("layout count %d: %v", n, err)
}
if _, err = d.Uint64(); err != nil { // offset
t.Fatal(err)
}
if _, err = d.Uint64(); err != nil { // length
t.Fatal(err)
}
if mode, err := d.Uint32(); err != nil || mode != nfs4.IoModeRW {
t.Fatalf("iomode %d: %v", mode, err)
}
if typ, err := d.Uint32(); err != nil || typ != nfs4.LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
flex, err := d.VarOpaque()
if err != nil {
t.Fatal(err)
}
fd := xdr.NewDecoder(flex)
if stripe, err := fd.Uint64(); err != nil || stripe != 0 {
t.Fatalf("stripe unit: %v", err)
}
if mirrors, err := fd.Uint32(); err != nil || mirrors != 1 {
t.Fatalf("mirrors %d: %v", mirrors, err)
}
if servers, err := fd.Uint32(); err != nil || servers != 1 {
t.Fatalf("data servers %d: %v", servers, err)
}
var device [16]byte
raw, rerr := fd.Raw(16)
if rerr != nil {
t.Fatal(rerr)
}
copy(device[:], raw)
if device != layoutDeviceID {
t.Fatalf("device id % x", device[:])
}
if _, err = fd.Uint32(); err != nil { // efficiency
t.Fatal(err)
}
raw, rerr = fd.Raw(16)
if rerr != nil {
t.Fatal(rerr)
}
if !bytes.Equal(raw, st[:]) {
t.Fatalf("data server stateid % x, want % x", raw, st[:])
}
fhCount, ferr := fd.Uint32()
if ferr != nil || fhCount != 1 {
t.Fatalf("file handle count %d: %v", fhCount, ferr)
}
if _, err = fd.VarOpaque(); err != nil { // the data server file handle
t.Fatal(err)
}
return st
}
func TestPnfsLayoutLifecycle(t *testing.T) {
h := testTree(t)
sid, seq := newSession(t, h)
run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
t.Helper()
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
payload := nfs4.AppendCompoundArgs(nil, "pnfs", nfs4.MinorVersion, all)
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
if !ok {
t.Fatal("garbage")
}
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
if derr != nil {
t.Fatalf("decode: %v", derr)
}
if len(r.Ops) != len(all) {
t.Fatalf("ops %d, want %d", len(r.Ops), len(all))
}
return r, bodies[1:]
}
// Open (create) the file the layout will hang from.
res, bodies := run(seq, [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendOpenArgs(nil, 0x2222, []byte("pnfs-owner"),
nfs4.ShareAccessBoth, 0, true, 0o644, "lay.txt"),
nfs4.AppendGetfh(nil),
})
if res.Status != nfs4.ErrOK {
t.Fatalf("open: status %d", res.Status)
}
var openSt nfs4.Stateid
copy(openSt[:], bodies[1])
fh, err := xdr.NewDecoder(bodies[2]).VarOpaque()
if err != nil || len(fh) == 0 {
t.Fatalf("file handle: %v", err)
}
// LAYOUTGET grants one flexfiles segment naming this server as the
// data server.
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexfiles,
nfs4.IoModeRW, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "layout get", res.Status, nfs4.ErrOK)
layoutSt := layoutStateidOf(t, bodies[1])
if !bytes.HasPrefix(layoutSt[4:], []byte("LAYOUT")) {
t.Fatalf("layout stateid other % x", layoutSt[4:])
}
if h.layouts().count() != 1 {
t.Fatalf("layouts %d, want 1", h.layouts().count())
}
// GETDEVICEINFO refuses an unknown device and answers the address of
// the one device this server carries.
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendGetDeviceInfoArgs(nil, [16]byte{7}, nfs4.LayoutTypeFlexfiles, 1<<16, nfs4.Bitmap{}),
})
wantStatus(t, "unknown device", res.Status, nfs4.ErrNoEnt)
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendGetDeviceInfoArgs(nil, layoutDeviceID, nfs4.LayoutTypeFlexfiles, 1<<16, nfs4.Bitmap{}),
})
wantStatus(t, "device info", res.Status, nfs4.ErrOK)
d := xdr.NewDecoder(bodies[1])
if typ, derr := d.Uint32(); derr != nil || typ != nfs4.LayoutTypeFlexfiles {
t.Fatalf("address type %d: %v", typ, derr)
}
blob, derr := d.VarOpaque()
if derr != nil {
t.Fatal(derr)
}
addr, derr := nfs4.DecodeFlexDeviceAddr(blob)
if derr != nil {
t.Fatalf("decode address: %v", derr)
}
if len(addr.NetAddrs) != 1 || addr.NetAddrs[0].Netid != "tcp" || addr.NetAddrs[0].Uaddr == "" {
t.Fatalf("net addresses %+v", addr.NetAddrs)
}
if len(addr.Versions) != 1 || addr.Versions[0].Version != 4 || addr.Versions[0].MinorVersion != nfs4.MinorVersion {
t.Fatalf("versions %+v", addr.Versions)
}
// LAYOUTCOMMIT with a last write beyond the size grows the file.
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutCommitArgs(nil, 0, ^uint64(0), false, layoutSt, true, 100, false, 0, 0, 0, nil),
})
wantStatus(t, "layout commit", res.Status, nfs4.ErrOK)
cd := xdr.NewDecoder(bodies[1])
if changed, cerr := cd.Bool(); cerr != nil || !changed {
t.Fatalf("size changed %v: %v", changed, cerr)
}
if size, cerr := cd.Uint64(); cerr != nil || size != 101 {
t.Fatalf("new size %d: %v", size, cerr)
}
info, gerr := h.FS.Getattr(fh)
if gerr != nil || info.Size != 101 {
t.Fatalf("backend size %d: %v", info.Size, gerr)
}
// A layout get with an anonymous stateid has no open behind it.
allOnes := nfs4.Stateid{}
for i := range allOnes {
allOnes[i] = 0xff
}
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexfiles,
nfs4.IoModeRW, 0, ^uint64(0), 0, allOnes, 1<<16),
})
wantStatus(t, "layout get without open", res.Status, nfs4.ErrBadStateid)
// An unknown layout type and a bogus IO mode are refused before any
// state is touched. The block layout is one of the emulated families,
// so the probe uses a type nothing supports.
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, 99,
nfs4.IoModeRW, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "unknown layout type", res.Status, nfs4.ErrUnknownLayoutType)
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexfiles,
0, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "bad iomode", res.Status, nfs4.ErrBadIOMode)
// LAYOUTRETURN of the file drops the layout; the commit under the
// returned stateid is refused afterwards.
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutReturnFileArgs(nil, false, nfs4.LayoutTypeFlexfiles,
nfs4.IoModeRW, 0, ^uint64(0), layoutSt),
})
wantStatus(t, "layout return", res.Status, nfs4.ErrOK)
rd := xdr.NewDecoder(bodies[1])
if returned, rerr := rd.Bool(); rerr != nil || !returned {
t.Fatalf("returned %v: %v", returned, rerr)
}
if h.layouts().count() != 0 {
t.Fatalf("layouts %d after return, want 0", h.layouts().count())
}
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutCommitArgs(nil, 0, ^uint64(0), false, layoutSt, false, 0, false, 0, 0, 0, nil),
})
wantStatus(t, "layout commit after return", res.Status, nfs4.ErrBadStateid)
// A whole client return drops every layout of the session.
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexfiles,
nfs4.IoModeRW, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "layout get again", res.Status, nfs4.ErrOK)
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutReturnArgs(nil, false, nfs4.LayoutTypeFlexfiles,
nfs4.IoModeRW, nfs4.ReturnAll),
})
wantStatus(t, "layout return all", res.Status, nfs4.ErrOK)
if h.layouts().count() != 0 {
t.Fatalf("layouts %d after whole client return, want 0", h.layouts().count())
}
// DESTROY_SESSION drops the layouts the session still holds.
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexfiles,
nfs4.IoModeRW, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "layout get before destroy", res.Status, nfs4.ErrOK)
if h.layouts().count() != 1 {
t.Fatalf("layouts %d, want 1", h.layouts().count())
}
payload := nfs4.AppendCompoundArgs(nil, "pnfs", nfs4.MinorVersion,
[][]byte{nfs4.AppendDestroySessionArgs(nil, sid)})
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
if !ok {
t.Fatal("garbage destroy")
}
dres, _, derr := nfs4.DecodeCompoundResBodies(body)
if derr != nil || dres.Status != nfs4.ErrOK {
t.Fatalf("destroy session: status %d %v", dres.Status, derr)
}
if h.layouts().count() != 0 {
t.Fatalf("layouts %d after destroy, want 0", h.layouts().count())
}
}
func TestUaddrOf(t *testing.T) {
t.Parallel()
cases := []struct {
in string
want string
}{
{"127.0.0.1:2049", "127.0.0.1.8.1"},
{"192.168.1.5:65535", "192.168.1.5.255.255"},
{"0.0.0.0:1", "0.0.0.0.0.1"},
{"[::1]:2049", "00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.01.8.1"},
{"no-host", ""},
{"host:notaport", ""},
{"host.example.com:80", ""},
{"1.2.3.4:99999", ""},
{"1.2.3.4:", ""},
}
for _, c := range cases {
if got := uaddrOf(c.in); got != c.want {
t.Errorf("uaddrOf(%q) = %q, want %q", c.in, got, c.want)
}
}
}
func TestDeviceAddrPreference(t *testing.T) {
t.Parallel()
h := &Handler{DeviceAddr: "10.0.0.1.8.1"}
if got := h.deviceAddr(nil); got != "10.0.0.1.8.1" {
t.Fatalf("configured address ignored: %q", got)
}
h2 := &Handler{}
if got := h2.deviceAddr(nil); got != "127.0.0.1.8.1" {
t.Fatalf("loopback default: %q", got)
}
}