Files
nfs/internal/nfs4server/layouts_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

275 lines
8.2 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"
)
// GETDEVICELIST answers the one device of this build for any layout
// type; LAYOUTGET accepts the emulated families and returns their wire
// bodies.
func TestEmulatedLayoutFamilies(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, "fam", 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)
}
return r, bodies[1:]
}
seq++
res, bodies := run(seq, [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)),
nfs4.AppendGetDeviceListArgs(nil, nfs4.LayoutTypeFlexfiles, 8, 0, [8]byte{}),
})
wantStatus(t, "open", res.Status, nfs4.ErrOK)
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendGetDeviceListArgs(nil, nfs4.LayoutTypeFlexfiles, 8, 0, [8]byte{}),
})
wantStatus(t, "device list", res.Status, nfs4.ErrOK)
gd := xdr.NewDecoder(bodies[0])
if cookie, cerr := gd.Uint64(); cerr != nil || cookie != 0 {
t.Fatalf("cookie %d", cookie)
}
if _, cerr := gd.Raw(8); cerr != nil { // verifier
t.Fatal(cerr)
}
n, cerr := gd.Uint32()
if cerr != nil || n != 1 {
t.Fatalf("devices %d: %v", n, cerr)
}
raw, _ := gd.Raw(16)
if !bytes.Equal(raw, layoutDeviceID[:]) {
t.Fatalf("device % x", raw)
}
if eof, _ := gd.Bool(); !eof {
t.Fatal("eof false")
}
// The files layout body decodes with its device and fh list.
openSt := nfs4.Stateid{}
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendOpenArgs(nil, 0x8888, []byte("fam"), nfs4.ShareAccessRead, 0, false, 0, "a.txt"),
nfs4.AppendGetfh(nil),
})
wantStatus(t, "open", res.Status, nfs4.ErrOK)
copy(openSt[:], bodies[1])
fh, _ := xdr.NewDecoder(bodies[2]).VarOpaque()
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFiles, nfs4.IoModeRead, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "files layoutget", res.Status, nfs4.ErrOK)
ld := xdr.NewDecoder(bodies[1])
var st nfs4.Stateid
raw, _ = ld.Raw(16)
copy(st[:], raw)
if !bytes.HasPrefix(st[4:], []byte("LAYOUT")) {
t.Fatalf("stateid % x", st[4:])
}
if _, _ = ld.Bool(); ld.Remaining() == 0 {
t.Fatal("no layout segments")
}
if n, _ := ld.Uint32(); n != 1 { // the segment count
t.Fatalf("segment count %d", n)
}
if _, _ = ld.Uint64(); ld.Remaining() == 0 { // offset
t.Fatal("short segment")
}
if _, _ = ld.Uint64(); ld.Remaining() == 0 { // length
t.Fatal("short segment")
}
if _, _ = ld.Uint32(); ld.Remaining() == 0 { // iomode
t.Fatal("short segment")
}
if _, _ = ld.Uint32(); ld.Remaining() == 0 { // type
t.Fatal("short segment")
}
flex, ferr := ld.VarOpaque()
if ferr != nil {
t.Fatal(ferr)
}
// The files layout body: device, util, first stripe, pattern offset,
// the fh list with the file's own handle.
fd := xdr.NewDecoder(flex)
dev, _ := fd.Raw(16)
if !bytes.Equal(dev, layoutDeviceID[:]) {
t.Fatalf("body device % x len %d", dev, len(flex))
}
if _, _ = fd.Uint32(); fd.Remaining() == 0 { // util
t.Fatal("short files body")
}
if _, _ = fd.Uint32(); fd.Remaining() == 0 { // first stripe
t.Fatal("short files body")
}
if _, _ = fd.Uint64(); fd.Remaining() == 0 { // pattern offset
t.Fatal("short files body")
}
fn, _ := fd.Uint32()
if fn != 1 {
t.Fatalf("fh list %d", fn)
}
if got, _ := fd.VarOpaque(); !bytes.Equal(got, fh) {
t.Fatalf("fh % x", got)
}
// The block, objects and SCSI families answer the same way with
// their own bodies; the emulated device is always this server.
for _, typ := range []uint32{nfs4.LayoutTypeFlexFilesV2, nfs4.LayoutTypeBlock, nfs4.LayoutTypeObjects, nfs4.LayoutTypeScsi} {
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, typ, nfs4.IoModeRead, 0, ^uint64(0), 0, openSt, 1<<16),
})
if res.Status != nfs4.ErrOK {
t.Fatalf("family %d: status %d", typ, res.Status)
}
}
}
// LAYOUTGET of the flexfiles version 2 type answers the version two
// body: the data server with its stateid array, the file handle and the
// AUTH_NONE credential of tight coupling.
func TestFlexFilesV2LayoutGet(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, "ffv2", 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)
}
return r, bodies[1:]
}
seq++
res, bodies := run(seq, [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendOpenArgs(nil, 0x9999, []byte("ffv2"), nfs4.ShareAccessRead, 0, false, 0, "a.txt"),
nfs4.AppendGetfh(nil),
})
wantStatus(t, "open", res.Status, nfs4.ErrOK)
var openSt nfs4.Stateid
copy(openSt[:], bodies[1])
fh, _ := xdr.NewDecoder(bodies[2]).VarOpaque()
seq++
res, bodies = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexFilesV2,
nfs4.IoModeRead, 0, ^uint64(0), 0, openSt, 1<<16),
})
wantStatus(t, "ffv2 layoutget", res.Status, nfs4.ErrOK)
ld := xdr.NewDecoder(bodies[1])
var st nfs4.Stateid
raw, _ := ld.Raw(16)
copy(st[:], raw)
if !bytes.HasPrefix(st[4:], []byte("LAYOUT")) {
t.Fatalf("stateid % x", st[4:])
}
if _, _ = ld.Bool(); ld.Remaining() == 0 {
t.Fatal("no segments")
}
if n, _ := ld.Uint32(); n != 1 {
t.Fatalf("segments %d", n)
}
if _, _ = ld.Uint64(); ld.Remaining() == 0 { // offset
t.Fatal("short segment")
}
if _, _ = ld.Uint64(); ld.Remaining() == 0 { // length
t.Fatal("short segment")
}
if _, _ = ld.Uint32(); ld.Remaining() == 0 { // iomode
t.Fatal("short segment")
}
if typ, _ := ld.Uint32(); typ != nfs4.LayoutTypeFlexFilesV2 {
t.Fatalf("segment type %d", typ)
}
v2, verr := ld.VarOpaque()
if verr != nil {
t.Fatal(verr)
}
fd := xdr.NewDecoder(v2)
if su, err := fd.Uint64(); err != nil || su != 0 {
t.Fatalf("stripe unit %d: %v", su, err)
}
if n, err := fd.Uint32(); err != nil || n != 1 {
t.Fatalf("mirrors %d: %v", n, err)
}
if n, err := fd.Uint32(); err != nil || n != 1 {
t.Fatalf("servers %d: %v", n, err)
}
raw, _ = fd.Raw(16)
if !bytes.Equal(raw, layoutDeviceID[:]) {
t.Fatalf("device % x", raw)
}
if eff, err := fd.Uint32(); err != nil || eff != 0 {
t.Fatalf("efficiency %d: %v", eff, err)
}
if n, err := fd.Uint32(); err != nil || n != 1 {
t.Fatalf("stateids %d: %v", n, err)
}
raw, _ = fd.Raw(16)
if !bytes.Equal(raw, st[:]) {
t.Fatalf("v2 stateid % x want % x", raw, st[:])
}
if n, err := fd.Uint32(); err != nil || n != 1 {
t.Fatalf("fhs %d: %v", n, err)
}
if v2fh, err := fd.VarOpaque(); err != nil || !bytes.Equal(v2fh, fh) {
t.Fatalf("v2 fh % x", v2fh)
}
if _, _ = fd.String(); fd.Remaining() == 0 { // user
t.Fatal("short v2 body")
}
if _, _ = fd.String(); fd.Remaining() == 0 { // group
t.Fatal("short v2 body")
}
if flavor, err := fd.Uint32(); err != nil || flavor != 0 {
t.Fatalf("auth flavor %d: %v", flavor, err)
}
if authBody, err := fd.VarOpaque(); err != nil || len(authBody) != 0 {
t.Fatalf("auth body % x", authBody)
}
// GETDEVICEINFO and GETDEVICELIST accept the version two type.
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendGetDeviceInfoArgs(nil, layoutDeviceID,
nfs4.LayoutTypeFlexFilesV2, 1<<16, nfs4.Bitmap{}),
})
wantStatus(t, "ffv2 deviceinfo", res.Status, nfs4.ErrOK)
seq++
res, _ = run(seq, [][]byte{
nfs4.AppendGetDeviceListArgs(nil, nfs4.LayoutTypeFlexFilesV2, 8, 0, [8]byte{}),
})
wantStatus(t, "ffv2 devicelist", res.Status, nfs4.ErrOK)
}