// Copyright (c) 2026 Petr BalvĂ­n (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) } }