Files
nfs/internal/nfs4/pnfs_test.go
T

547 lines
16 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// layoutOf decodes the single segment a test layout response carries.
func layoutOf(t *testing.T, body []byte) (Layout4, []byte) {
t.Helper()
d := xdr.NewDecoder(body)
var st 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)
}
var l Layout4
if l.Offset, err = d.Uint64(); err != nil {
t.Fatal(err)
}
if l.Length, err = d.Uint64(); err != nil {
t.Fatal(err)
}
if l.IoMode, err = d.Uint32(); err != nil {
t.Fatal(err)
}
if l.Type, err = d.Uint32(); err != nil {
t.Fatal(err)
}
if l.Body, err = d.VarOpaque(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the layout response", d.Remaining())
}
return l, st[:]
}
func flexBodyOf(t *testing.T, body []byte) (FlexDataServer, uint64) {
t.Helper()
d := xdr.NewDecoder(body)
stripeUnit, err := d.Uint64()
if err != nil {
t.Fatal(err)
}
mirrors, err := d.Uint32()
if err != nil || mirrors != 1 {
t.Fatalf("mirror count %d: %v", mirrors, err)
}
servers, err := d.Uint32()
if err != nil || servers != 1 {
t.Fatalf("data server count %d: %v", servers, err)
}
var ds FlexDataServer
raw, rerr := d.Raw(16)
if rerr != nil {
t.Fatalf("device id: %v", rerr)
}
copy(ds.DeviceID[:], raw)
if ds.Efficiency, err = d.Uint32(); err != nil {
t.Fatal(err)
}
raw, rerr = d.Raw(16)
if rerr != nil {
t.Fatalf("data server stateid: %v", rerr)
}
copy(ds.Stateid[:], raw)
fhCount, ferr := d.Uint32()
if ferr != nil || fhCount != 1 {
t.Fatalf("file handle count %d: %v", fhCount, ferr)
}
var fhBody []byte
if fhBody, err = d.VarOpaque(); err != nil {
t.Fatal(err)
}
ds.FHs = [][]byte{fhBody}
if ds.User, err = d.String(); err != nil {
t.Fatal(err)
}
if ds.Group, err = d.String(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 8 {
t.Fatalf("%d bytes left in the flexfiles body", d.Remaining())
}
return ds, stripeUnit
}
func TestLayoutGetResBodyLen(t *testing.T) {
t.Parallel()
st := Stateid{1, 'L', 'A', 'Y', 1, 2, 3}
fh := []byte("filehandle")
body := AppendFlexFileLayoutBody(nil, 0, 4096, []FlexMirror{{
DataServers: []FlexDataServer{{DeviceID: [16]byte{9}, Stateid: st, FHs: [][]byte{fh}}},
}})
res := AppendLayoutGetRes(nil, st, false, []Layout4{{
Offset: 0, Length: ^uint64(0), IoMode: IoModeRW, Type: LayoutTypeFlexfiles, Body: body,
}})
n, err := resBodyLen(OpLayoutGet, ErrOK, res)
if err != nil {
t.Fatalf("resBodyLen: %v", err)
}
if n != len(res) {
t.Fatalf("measured %d, body is %d", n, len(res))
}
l, _ := layoutOf(t, res)
ds, stripe := flexBodyOf(t, l.Body)
if l.Type != LayoutTypeFlexfiles || l.IoMode != IoModeRW || l.Offset != 0 || l.Length != ^uint64(0) {
t.Fatalf("segment %+v", l)
}
if stripe != 4096 {
t.Fatalf("stripe unit %d", stripe)
}
if ds.DeviceID != [16]byte{9} || len(ds.FHs) != 1 || !bytes.Equal(ds.FHs[0], fh) || ds.Stateid != st {
t.Fatalf("data server %+v", ds)
}
}
func TestLayoutCommitReturnDeviceInfoResBodyLen(t *testing.T) {
t.Parallel()
st := Stateid{2, 'L', 'A', 'Y', 4, 5, 6}
commit := AppendLayoutCommitRes(nil, 101)
if n, err := resBodyLen(OpLayoutCommit, ErrOK, commit); err != nil || n != len(commit) {
t.Fatalf("commit res: %d %v", n, err)
}
ret := AppendLayoutReturnRes(nil, st)
if n, err := resBodyLen(OpLayoutReturn, ErrOK, ret); err != nil || n != len(ret) {
t.Fatalf("return res: %d %v", n, err)
}
addr := AppendFlexDeviceAddr(nil, FlexDeviceAddr{
NetAddrs: []NetAddr{{Netid: "tcp", Uaddr: "127.0.0.1.8.1"}},
Versions: []FlexVersion{{Version: 4, MinorVersion: 2, RSize: 1 << 20, WSize: 1 << 20}},
})
dev := AppendGetDeviceInfoRes(nil, addr)
n, err := resBodyLen(OpGetDeviceInfo, ErrOK, dev)
if err != nil {
t.Fatalf("device info res: %v", err)
}
if n != len(dev) {
t.Fatalf("measured %d, body is %d", n, len(dev))
}
d := xdr.NewDecoder(dev)
if typ, derr := d.Uint32(); derr != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("address type %d: %v", typ, derr)
}
blob, derr := d.VarOpaque()
if derr != nil {
t.Fatal(derr)
}
got, derr := DecodeFlexDeviceAddr(blob)
if derr != nil {
t.Fatalf("decode device address: %v", derr)
}
if len(got.NetAddrs) != 1 || got.NetAddrs[0].Netid != "tcp" || got.NetAddrs[0].Uaddr != "127.0.0.1.8.1" {
t.Fatalf("net addresses %+v", got.NetAddrs)
}
if len(got.Versions) != 1 || got.Versions[0] != (FlexVersion{Version: 4, MinorVersion: 2, RSize: 1 << 20, WSize: 1 << 20}) {
t.Fatalf("versions %+v", got.Versions)
}
if _, derr = ReadBitmap(d); derr != nil {
t.Fatal(derr)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the device info response", d.Remaining())
}
}
func TestLayoutArgShapes(t *testing.T) {
t.Parallel()
st := Stateid{7, 'L', 'A', 'Y', 8, 8, 8}
// LAYOUTGET args carry the op code and the full field set.
args := AppendLayoutGetArgs(nil, false, LayoutTypeFlexfiles, IoModeRW, 0, 100, 100, st, 1<<16)
if got := args[0:4]; !bytes.Equal(got, []byte{0, 0, 0, OpLayoutGet & 0xff}) {
t.Fatalf("layout get args open with % x", got)
}
d := xdr.NewDecoder(args[4:])
if _, err := d.Bool(); err != nil {
t.Fatal(err)
}
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
if mode, err := d.Uint32(); err != nil || mode != IoModeRW {
t.Fatalf("iomode %d: %v", mode, err)
}
for range 3 {
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if mc, err := d.Uint32(); err != nil || mc != 1<<16 {
t.Fatalf("maxcount %d: %v", mc, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the layout get args", d.Remaining())
}
// The file return carries the range, the stateid and the empty ffsid
// body; the whole client return carries nothing after the header.
fileArgs := AppendLayoutReturnFileArgs(nil, false, LayoutTypeFlexfiles, IoModeRW, 10, 90, st)
d = xdr.NewDecoder(fileArgs[4:])
reclaim, err := d.Bool()
if err != nil || reclaim {
t.Fatalf("reclaim %v: %v", reclaim, err)
}
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
if mode, err := d.Uint32(); err != nil || mode != IoModeRW {
t.Fatalf("iomode %d: %v", mode, err)
}
if kind, err := d.Uint32(); err != nil || kind != ReturnFile {
t.Fatalf("return kind %d: %v", kind, err)
}
if off, err := d.Uint64(); err != nil || off != 10 {
t.Fatalf("offset %d: %v", off, err)
}
if length, err := d.Uint64(); err != nil || length != 90 {
t.Fatalf("length %d: %v", length, err)
}
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if ffsid, err := d.Uint32(); err != nil || ffsid != 0 {
t.Fatalf("ffsid type %d: %v", ffsid, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the file return", d.Remaining())
}
allArgs := AppendLayoutReturnArgs(nil, true, LayoutTypeFlexfiles, IoModeRW, ReturnAll)
d = xdr.NewDecoder(allArgs[4:])
if reclaim, err := d.Bool(); err != nil || !reclaim {
t.Fatalf("reclaim %v: %v", reclaim, err)
}
for range 3 {
if _, err := d.Uint32(); err != nil {
t.Fatal(err)
}
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the whole client return", d.Remaining())
}
}
func TestLayoutCommitArgsShape(t *testing.T) {
t.Parallel()
st := Stateid{3, 'L', 'A', 'Y', 9, 9, 9}
full := LayoutCommitArgs{
Offset: 8, Length: 92, Reclaim: true, LayoutSt: st,
LastWriteSet: true, LastWrite: 99,
TimeSet: true, Seconds: 1758000000, Nseconds: 500,
UpdateType: 1, UpdateBody: []byte("ff"),
}.Encode()
d := xdr.NewDecoder(full[4:])
if off, err := d.Uint64(); err != nil || off != 8 {
t.Fatalf("offset %d: %v", off, err)
}
if length, err := d.Uint64(); err != nil || length != 92 {
t.Fatalf("length %d: %v", length, err)
}
if reclaim, err := d.Bool(); err != nil || !reclaim {
t.Fatalf("reclaim %v: %v", reclaim, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if set, err := d.Bool(); err != nil || !set {
t.Fatalf("last write set %v: %v", set, err)
}
if lw, err := d.Uint64(); err != nil || lw != 99 {
t.Fatalf("last write %d: %v", lw, err)
}
if ts, err := d.Bool(); err != nil || !ts {
t.Fatalf("time set %v: %v", ts, err)
}
if sec, err := d.Int64(); err != nil || sec != 1758000000 {
t.Fatalf("seconds %d: %v", sec, err)
}
if ns, err := d.Uint32(); err != nil || ns != 500 {
t.Fatalf("nseconds %d: %v", ns, err)
}
if ut, err := d.Uint32(); err != nil || ut != 1 {
t.Fatalf("update type %d: %v", ut, err)
}
if body, err := d.VarOpaque(); err != nil || !bytes.Equal(body, []byte("ff")) {
t.Fatalf("update body %q: %v", body, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the commit args", d.Remaining())
}
// The empty form: no last write, no time, no update body.
empty := LayoutCommitArgs{LayoutSt: st}.Encode()
d = xdr.NewDecoder(empty[4:])
for range 2 {
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
}
if _, err := d.Bool(); err != nil {
t.Fatal(err)
}
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
if set, err := d.Bool(); err != nil || set {
t.Fatalf("empty last write %v: %v", set, err)
}
if ts, err := d.Bool(); err != nil || ts {
t.Fatalf("empty time %v: %v", ts, err)
}
if _, err := d.Uint32(); err != nil {
t.Fatal(err)
}
if body, err := d.VarOpaque(); err != nil || len(body) != 0 {
t.Fatalf("empty update body %q: %v", body, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the empty commit args", d.Remaining())
}
}
func TestGetDeviceInfoArgsShape(t *testing.T) {
t.Parallel()
var device [16]byte
copy(device[:], "0123456789abcdef")
args := AppendGetDeviceInfoArgs(nil, device, LayoutTypeFlexfiles, 1<<16, Bitmap{}.With(0))
d := xdr.NewDecoder(args[4:])
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, device[:]) {
t.Fatalf("device id % x: %v", raw, err)
}
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
if mc, err := d.Uint32(); err != nil || mc != 1<<16 {
t.Fatalf("maxcount %d: %v", mc, err)
}
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the device info args", d.Remaining())
}
}
func TestOps41Shapes(t *testing.T) {
t.Parallel()
// VERIFY args are a bare fattr4 with the op in front.
args := AppendVerifyArgs(nil, OfBits(AttrMode), Attrs{Mode: 0o644})
d := xdr.NewDecoder(args[4:])
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if _, err := d.VarOpaque(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the verify args", d.Remaining())
}
// BIND_CONN_TO_SESSION echoes the session and the direction; its
// result body measures to exactly 20 bytes.
sid := SessionID{1, 2, 3}
res := AppendBindConnToSessionRes(nil, sid, Cdfs4Both)
if n, err := resBodyLen(OpBindConnToSession, ErrOK, res); err != nil || n != len(res) || n != 20 {
t.Fatalf("bind res measured %d of %d: %v", n, len(res), err)
}
d = xdr.NewDecoder(res)
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, sid[:]) {
t.Fatalf("session % x: %v", raw, err)
}
if dir, err := d.Uint32(); err != nil || dir != Cdfs4Both {
t.Fatalf("dir %d: %v", dir, err)
}
// TEST_STATEID measures to the status array.
statuses := AppendTestStateidRes(nil, []uint32{ErrOK, ErrBadStateid})
if n, err := resBodyLen(OpTestStateid, ErrOK, statuses); err != nil || n != len(statuses) {
t.Fatalf("test stateid res measured %d of %d: %v", n, len(statuses), err)
}
// RELEASE_LOCKOWNER and DELEGRETURN carry their shapes.
own := AppendReleaseLockOwnerArgs(nil, 0x99, []byte("owner"))
d = xdr.NewDecoder(own[4:])
if cid, err := d.Uint64(); err != nil || cid != 0x99 {
t.Fatalf("clientid %d: %v", cid, err)
}
if o, err := d.VarOpaque(); err != nil || string(o) != "owner" {
t.Fatalf("owner %q: %v", o, err)
}
st := Stateid{1, 'D', 'E', 'L', 7}
dr := AppendDelegReturnArgs(nil, st)
d = xdr.NewDecoder(dr[4:])
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("deleg return stateid % x: %v", raw, err)
}
fs := AppendFreeStateidArgs(nil, st)
d = xdr.NewDecoder(fs[4:])
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("free stateid % x: %v", raw, err)
}
bc := AppendBackchannelCtlArgs(nil, 0x40000000)
d = xdr.NewDecoder(bc[4:])
if p, err := d.Uint32(); err != nil || p != 0x40000000 {
t.Fatalf("program %d: %v", p, err)
}
ts := AppendTestStateidArgs(nil, []Stateid{st, {}})
d = xdr.NewDecoder(ts[4:])
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("count %d: %v", n, err)
}
for range 2 {
raw, err = d.Raw(16)
if err != nil {
t.Fatal(err)
}
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the test stateid args", d.Remaining())
}
}
func TestXattrShapes(t *testing.T) {
t.Parallel()
// GETXATTR args carry the name alone, RFC 8276: the operation
// number, the length word and the twelve byte name.
args := AppendGetXattrArgs(nil, "user.comment")
if len(args) != 20 {
t.Fatalf("getxattr args of %d bytes, want 20", len(args))
}
d := xdr.NewDecoder(args[4:])
if name, err := d.String(); err != nil || name != "user.comment" {
t.Fatalf("name %q: %v", name, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the getxattr args", d.Remaining())
}
res := AppendGetXattrRes(nil, []byte("hello"))
n, err := resBodyLen(OpGetxattr, ErrOK, res)
if err != nil || n != len(res) {
t.Fatalf("getxattr res measured %d of %d: %v", n, len(res), err)
}
if v, err := DecodeGetXattrRes(res); err != nil || string(v) != "hello" {
t.Fatalf("getxattr value %q: %v", v, err)
}
// LISTXATTR res: the cookie, the names and the eof flag.
list := AppendListXattrRes(nil, 7, []string{"user.a", "user.b"}, false)
n, err = resBodyLen(OpListxattr, ErrOK, list)
if err != nil || n != len(list) {
t.Fatalf("listxattr res measured %d of %d: %v", n, len(list), err)
}
// SETXATTR args: the mode first, then the key and the value.
sx := AppendSetXattrArgs(nil, SetXattrCreate, "user.a", []byte("v"))
d = xdr.NewDecoder(sx[4:])
if mode, err := d.Uint32(); err != nil || mode != SetXattrCreate {
t.Fatalf("mode %d: %v", mode, err)
}
if name, err := d.String(); err != nil || name != "user.a" {
t.Fatalf("set name %q: %v", name, err)
}
if v, err := d.VarOpaque(); err != nil || string(v) != "v" {
t.Fatalf("value %q: %v", v, err)
}
rx := AppendRemoveXattrArgs(nil, "user.a")
d = xdr.NewDecoder(rx[4:])
if name, err := d.String(); err != nil || name != "user.a" {
t.Fatalf("remove name %q: %v", name, err)
}
}
func TestOps41RemainingShapes(t *testing.T) {
t.Parallel()
// The argument-less ops carry exactly the op code.
if got := AppendLookupp(nil); !bytes.Equal(got, []byte{0, 0, 0, OpLookupp & 0xff}) {
t.Fatalf("lookupp % x", got)
}
if got := AppendPutPubfh(nil); !bytes.Equal(got, []byte{0, 0, 0, OpPutPubfh & 0xff}) {
t.Fatalf("putpubfh % x", got)
}
// NVERIFY shares the VERIFY shape.
nv := AppendNverifyArgs(nil, OfBits(AttrSize), Attrs{Size: 9})
d := xdr.NewDecoder(nv[4:])
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if _, err := d.VarOpaque(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the nverify args", d.Remaining())
}
// BIND_CONN_TO_SESSION args: the session, the direction and the
// RDMA mode flag.
bind := AppendBindConnToSessionArgs(nil, SessionID{9}, Cdfc4Back, true)
d = xdr.NewDecoder(bind[4:])
raw, err := d.Raw(16)
if err != nil || raw[0] != 9 {
t.Fatalf("session % x: %v", raw, err)
}
if dir, err := d.Uint32(); err != nil || dir != Cdfc4Back {
t.Fatalf("dir %d: %v", dir, err)
}
if rdma, err := d.Bool(); err != nil || !rdma {
t.Fatalf("rdma %v: %v", rdma, err)
}
// LISTXATTR args carry the cookie and the budget.
la := AppendListXattrArgs(nil, 3, 4096)
d = xdr.NewDecoder(la[4:])
if ck, err := d.Uint64(); err != nil || ck != 3 {
t.Fatalf("list cookie %d: %v", ck, err)
}
if mc, err := d.Uint32(); err != nil || mc != 4096 {
t.Fatalf("list maxcount %d: %v", mc, err)
}
}