Files
nfs/internal/nfs4/migration_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

312 lines
9.6 KiB
Go

// 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"
)
// The location attributes round trip: the fs_locations attribute
// decodes back to the servers and root paths, and the info attribute
// carries the flags, the validity window and the replica entries.
func TestFsLocationsShapes(t *testing.T) {
blob := AppendFsLocations(nil, []string{"exports"}, []FsLocation{{
Servers: []string{"a.example", "b.example"},
RootPath: []string{"exports", "data"},
}})
root, locations, err := DecodeFsLocations(blob)
if err != nil {
t.Fatal(err)
}
if len(root) != 1 || root[0] != "exports" {
t.Fatalf("root %v", root)
}
if len(locations) != 1 || len(locations[0].Servers) != 2 ||
locations[0].Servers[1] != "b.example" ||
len(locations[0].RootPath) != 2 || locations[0].RootPath[1] != "data" {
t.Fatalf("locations %+v", locations)
}
// The fs_locations_info attribute: flags, validity, items with the
// opaque info bytes.
info := AppendFsLocationsInfo(nil, Fsli4ifVarSub, 60, []string{"exports"},
[]FsItem{{Entries: []FsServer{{
Currency: 3, Info: []byte{Fsli4gfWritable | Fsli4gfCurReq}, Server: "a.example",
}}, RootPath: []string{"data"}}})
d := xdr.NewDecoder(info)
if flags, err := d.Uint32(); err != nil || flags != Fsli4ifVarSub {
t.Fatalf("flags %d: %v", flags, err)
}
if valid, err := d.Int32(); err != nil || valid != 60 {
t.Fatalf("valid %d: %v", valid, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("root count %d: %v", n, err)
}
if rootComponent, err := d.String(); err != nil || rootComponent != "exports" {
t.Fatalf("root component %q: %v", rootComponent, err)
}
if _, err := d.Uint32(); err != nil { // items count
t.Fatal(err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("entries %d: %v", n, err)
}
if cur, err := d.Int32(); err != nil || cur != 3 {
t.Fatalf("currency %d: %v", cur, err)
}
if info2, err := d.VarOpaque(); err != nil || len(info2) != 1 ||
info2[0]&Fsli4gfWritable == 0 || info2[0]&Fsli4gfCurReq == 0 {
t.Fatalf("info % x: %v", info2, err)
}
if s, err := d.String(); err != nil || s != "a.example" {
t.Fatalf("server %q: %v", s, err)
}
// The item closes with the root path of the replicas.
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("rootpath count %d: %v", n, err)
}
if c, err := d.String(); err != nil || c != "data" {
t.Fatalf("rootpath %q: %v", c, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
}
// The emulated layout family bodies carry the device id first and the
// family payload after; the GETDEVICELIST pair round trips.
func TestLayoutFamilyShapes(t *testing.T) {
var dev [16]byte
copy(dev[:], "0123456789abcdef")
files := AppendFileLayoutBody(nil, dev, 4096, 2, 8,
[][]byte{[]byte("fh1"), []byte("fh2")})
d := xdr.NewDecoder(files)
if got, err := d.Raw(16); err != nil || !bytes.Equal(got, dev[:]) {
t.Fatalf("device % x: %v", got, err)
}
if u, err := d.Uint32(); err != nil || u != 4096 {
t.Fatalf("util %d: %v", u, err)
}
if fs, err := d.Uint32(); err != nil || fs != 2 {
t.Fatalf("first stripe %d: %v", fs, err)
}
if po, err := d.Uint64(); err != nil || po != 8 {
t.Fatalf("pattern %d: %v", po, err)
}
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("fh count %d: %v", n, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh1" {
t.Fatalf("fh %q: %v", fh, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh2" {
t.Fatalf("fh2 %q: %v", fh, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
// The device address: stripe indices and the multipath list.
addr := AppendFileDeviceAddr(nil, []uint32{0, 1},
[]NetAddr{{Netid: "tcp", Uaddr: "10.0.0.1.8.1"}})
d = xdr.NewDecoder(addr)
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("indices %d: %v", n, err)
}
if _, err := d.Raw(8); err != nil {
t.Fatal(err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("addrs %d: %v", n, err)
}
if id, err := d.String(); err != nil || id != "tcp" {
t.Fatalf("netid %q: %v", id, err)
}
if ua, err := d.String(); err != nil || ua != "10.0.0.1.8.1" {
t.Fatalf("uaddr %q: %v", ua, err)
}
// The block volume, the SCSI extent and the OSD component bodies
// carry their shapes.
block := AppendBlockDeviceAddr(nil, BlockVolume{DeviceID: dev, BaseOffset: 8, BlockCount: 64})
d = xdr.NewDecoder(block)
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("volumes %d: %v", n, err)
}
if typ, err := d.Uint32(); err != nil || typ != 1 {
t.Fatalf("volume type %d: %v", typ, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, dev[:]) {
t.Fatalf("volume device % x: %v", raw, err)
}
if bo, err := d.Uint64(); err != nil || bo != 8 {
t.Fatalf("base offset %d: %v", bo, err)
}
if bc, err := d.Uint64(); err != nil || bc != 64 {
t.Fatalf("block count %d: %v", bc, err)
}
scsi := AppendScsiLayoutBody(nil, dev, 1, 2, 3)
d = xdr.NewDecoder(scsi)
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("extents %d: %v", n, err)
}
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, dev[:]) {
t.Fatalf("extent device % x: %v", raw, err)
}
for _, want := range []uint64{1, 2, 3} {
if v, err := d.Uint64(); err != nil || v != want {
t.Fatalf("extent field %d: %v", v, err)
}
}
obj := AppendObjectLayoutBody(nil, dev, ObjectLayout{NumComponents: 1,
StripeUnit: 4096, GroupWidth: 1, GroupDepth: 1, RefTagSize: 4})
d = xdr.NewDecoder(obj)
if alg, err := d.Uint32(); err != nil || alg != 0 {
t.Fatalf("raid algorithm %d: %v", alg, err)
}
if su, err := d.Uint64(); err != nil || su != 4096 {
t.Fatalf("stripe unit %d: %v", su, err)
}
// The GETDEVICELIST pair round trips.
list := AppendGetDeviceListArgs(nil, LayoutTypeFlexfiles, 8, 5, [8]byte{9})
d = xdr.NewDecoder(list[4:])
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("list type %d: %v", typ, err)
}
if md, err := d.Uint32(); err != nil || md != 8 {
t.Fatalf("max devices %d: %v", md, err)
}
if c, err := d.Uint64(); err != nil || c != 5 {
t.Fatalf("cookie %d: %v", c, err)
}
raw, err = d.Raw(8)
if err != nil || raw[0] != 9 {
t.Fatalf("verifier % x: %v", raw, err)
}
res := AppendGetDeviceListRes(nil, 5, [8]byte{9}, [][16]byte{dev, {1}}, true)
d = xdr.NewDecoder(res)
if c, err := d.Uint64(); err != nil || c != 5 {
t.Fatalf("res cookie %d: %v", c, err)
}
if _, err := d.Raw(8); err != nil {
t.Fatal(err)
}
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("devices %d: %v", n, err)
}
if _, err := d.Raw(32); err != nil {
t.Fatal(err)
}
if eof, err := d.Bool(); err != nil || !eof {
t.Fatalf("eof %v: %v", eof, err)
}
}
// The OPENATTR and READ argop encoders carry their shapes.
func TestOpenattrReadArgs(t *testing.T) {
oa := AppendOpenattrArgs(nil, true)
d := xdr.NewDecoder(oa[4:])
if created, err := d.Bool(); err != nil || !created {
t.Fatalf("created %v: %v", created, err)
}
ra := AppendReadArgs(nil, Stateid{7}, 99, 1024)
d = xdr.NewDecoder(ra[4:])
raw, err := d.Raw(16)
if err != nil || raw[0] != 7 {
t.Fatalf("stateid % x: %v", raw, err)
}
if off, err := d.Uint64(); err != nil || off != 99 {
t.Fatalf("offset %d: %v", off, err)
}
if cnt, err := d.Uint32(); err != nil || cnt != 1024 {
t.Fatalf("count %d: %v", cnt, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
}
// The flexfiles version 2 layout body of
// draft-haynes-nfsv4-flex-filesv2-00 round trips: the data server
// carries the stateid array, the file handle list and the RPC
// credential the draft adds.
func TestFlexFileLayoutV2Shape(t *testing.T) {
var dev [16]byte
copy(dev[:], "v2flexdeviceid!!")
st := Stateid{2, 'F', 'F', 'V', '2'}
body := AppendFlexFileLayoutBodyV2(nil, 3, 0, []FlexMirrorV2{{
DataServers: []FlexDataServerV2{{
DeviceID: dev,
Efficiency: 7,
Stateids: []Stateid{st},
FHs: [][]byte{[]byte("fh-v2")},
User: "petr",
Group: "nfs",
AuthFlavor: 0,
AuthBody: nil,
}},
}})
d := xdr.NewDecoder(body)
if su, err := d.Uint64(); err != nil || su != 0 {
t.Fatalf("stripe unit %d: %v", su, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("mirrors %d: %v", n, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("servers %d: %v", n, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, dev[:]) {
t.Fatalf("device % x: %v", raw, err)
}
if eff, err := d.Uint32(); err != nil || eff != 7 {
t.Fatalf("efficiency %d: %v", eff, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("stateids %d: %v", n, err)
}
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("fhs %d: %v", n, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh-v2" {
t.Fatalf("fh %q: %v", fh, err)
}
if u, err := d.String(); err != nil || u != "petr" {
t.Fatalf("user %q: %v", u, err)
}
if g, err := d.String(); err != nil || g != "nfs" {
t.Fatalf("group %q: %v", g, err)
}
if flavor, err := d.Uint32(); err != nil || flavor != 0 {
t.Fatalf("auth flavor %d: %v", flavor, err)
}
if authBody, err := d.VarOpaque(); err != nil || len(authBody) != 0 {
t.Fatalf("auth body % x: %v", authBody, err)
}
if flags, err := d.Uint32(); err != nil || flags != 3 {
t.Fatalf("flags %d: %v", flags, err)
}
if hint, err := d.Uint32(); err != nil || hint != 0 {
t.Fatalf("stats hint %d: %v", hint, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
}