Files
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

320 lines
9.8 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The pNFS layout layer: the wire codecs of LAYOUTGET, LAYOUTCOMMIT,
// LAYOUTRETURN and GETDEVICEINFO over the flexfiles layout of RFC 8435.
// The shapes follow the XDR of RFC 8881 sections 18.43 to 18.45 and RFC
// 8435 section 6.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Layout types of the layouttype4 enum, the registry values of RFC 7863
// and RFC 8435: files, objects and block volume are the NFSv4.1 core,
// flexfiles is four, SCSI five.
const (
LayoutTypeFiles = 1
LayoutTypeObjects = 2
LayoutTypeBlock = 3
LayoutTypeFlexfiles = 4
LayoutTypeScsi = 5
)
// Layout IO modes of the layoutiomode4 enum, RFC 7863.
const (
IoModeRead = 1
IoModeRW = 2
IoModeAny = 3
)
// LAYOUTCOMMIT uses the newtime4 union for the modify time a client
// asserts.
type NewTime struct {
Set bool
Seconds int64
Nseconds uint32
}
// AppendLayoutGetArgs encodes the whole LAYOUTGET argop: the signal flag,
// the layout type, the IO mode, the range, the stateid the layout hangs
// from and the response size budget.
func AppendLayoutGetArgs(b []byte, signalAvail bool, layoutType, iomode uint32, offset, length, minlength uint64, st Stateid, maxcount uint32) []byte {
b = xdr.AppendUint32(b, OpLayoutGet)
b = xdr.AppendBool(b, signalAvail)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, iomode)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendUint64(b, minlength)
b = st.Append(b)
return xdr.AppendUint32(b, maxcount)
}
// A Layout4 is one segment of a granted layout: the byte range, the IO
// mode and the type specific body.
type Layout4 struct {
Offset uint64
Length uint64
IoMode uint32
Type uint32
Body []byte
}
// AppendLayout4 encodes one layout4.
func AppendLayout4(b []byte, l Layout4) []byte {
b = xdr.AppendUint64(b, l.Offset)
b = xdr.AppendUint64(b, l.Length)
b = xdr.AppendUint32(b, l.IoMode)
b = xdr.AppendUint32(b, l.Type)
return xdr.AppendVarOpaque(b, l.Body)
}
// AppendLayoutGetRes encodes a successful LAYOUTGET result: the layout
// stateid, the return-on-close flag and the layouts.
func AppendLayoutGetRes(b []byte, layoutSt Stateid, returnOnClose bool, layouts []Layout4) []byte {
b = layoutSt.Append(b)
b = xdr.AppendBool(b, returnOnClose)
b = xdr.AppendUint32(b, uint32(len(layouts)))
for _, l := range layouts {
b = AppendLayout4(b, l)
}
return b
}
// AppendLayoutCommitArgs encodes the LAYOUTCOMMIT argop body: the range,
// the reclaim flag, the layout stateid, the last write offset, the modify
// time and the layout update record.
func AppendLayoutCommitArgs(b []byte, offset, length uint64, reclaim bool, layoutSt Stateid, lastWriteSet bool, lastWrite uint64, timeSet bool, seconds int64, nseconds uint32, updateType uint32, updateBody []byte) []byte {
b = xdr.AppendUint32(b, OpLayoutCommit)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendBool(b, reclaim)
b = layoutSt.Append(b)
if lastWriteSet {
b = xdr.AppendBool(b, true)
b = xdr.AppendUint64(b, lastWrite)
} else {
b = xdr.AppendBool(b, false)
}
if timeSet {
b = xdr.AppendBool(b, true)
b = xdr.AppendInt64(b, seconds)
b = xdr.AppendUint32(b, nseconds)
} else {
b = xdr.AppendBool(b, false)
}
b = xdr.AppendUint32(b, updateType)
return xdr.AppendVarOpaque(b, updateBody)
}
// AppendLayoutCommitRes encodes a successful LAYOUTCOMMIT result: the
// newsize4 union with the size the layout commit leaves the file at.
func AppendLayoutCommitRes(b []byte, newSize uint64) []byte {
b = xdr.AppendBool(b, true)
return xdr.AppendUint64(b, newSize)
}
// AppendLayoutReturnArgs encodes the LAYOUTRETURN argop of the whole file
// system or the whole client: no layoutreturn_file4 body follows.
func AppendLayoutReturnArgs(b []byte, reclaim bool, layoutType, iomode, returnKind uint32) []byte {
b = xdr.AppendUint32(b, OpLayoutReturn)
b = xdr.AppendBool(b, reclaim)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, iomode)
return xdr.AppendUint32(b, returnKind)
}
// AppendLayoutReturnFileArgs encodes the LAYOUTRETURN argop of one file:
// the offset, the length and the layout stateid ride after the header,
// with the empty ffsid body of NFSv4.2.
func AppendLayoutReturnFileArgs(b []byte, reclaim bool, layoutType, iomode uint32, offset, length uint64, st Stateid) []byte {
b = AppendLayoutReturnArgs(b, reclaim, layoutType, iomode, ReturnFile)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = st.Append(b)
return xdr.AppendUint32(b, 0) // ffsid_info_type: the empty default
}
// AppendLayoutReturnRes encodes a successful LAYOUTRETURN result: the
// layoutreturn_stateid4 union with the returned flag set and the stateid
// of the layout as returned.
func AppendLayoutReturnRes(b []byte, stateid Stateid) []byte {
b = xdr.AppendBool(b, true)
return stateid.Append(b)
}
// AppendGetDeviceInfoArgs encodes the GETDEVICEINFO argop: the device id,
// the layout type, the response size budget and the notification types.
func AppendGetDeviceInfoArgs(b []byte, deviceID [16]byte, layoutType, maxcount uint32, notifyTypes Bitmap) []byte {
b = xdr.AppendUint32(b, OpGetDeviceInfo)
b = append(b, deviceID[:]...)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, maxcount)
return notifyTypes.AppendTo(b)
}
// AppendGetDeviceInfoRes encodes a successful GETDEVICEINFO result: the
// device address of the flexfiles layout and the notification bitmap.
func AppendGetDeviceInfoRes(b []byte, addrBody []byte) []byte {
b = xdr.AppendUint32(b, LayoutTypeFlexfiles)
b = xdr.AppendVarOpaque(b, addrBody)
return Bitmap{}.AppendTo(b)
}
// A LayoutCommitArgs is the structured form of the LAYOUTCOMMIT argop.
type LayoutCommitArgs struct {
Offset uint64
Length uint64
Reclaim bool
LayoutSt Stateid
LastWriteSet bool
LastWrite uint64
TimeSet bool
Seconds int64
Nseconds uint32
UpdateType uint32
UpdateBody []byte
}
// Encode builds the whole LAYOUTCOMMIT argop.
func (a LayoutCommitArgs) Encode() []byte {
return AppendLayoutCommitArgs(nil, a.Offset, a.Length, a.Reclaim, a.LayoutSt,
a.LastWriteSet, a.LastWrite, a.TimeSet, a.Seconds, a.Nseconds, a.UpdateType, a.UpdateBody)
}
// A FlexDataServer is one ff_data_server4: the device the client reads
// and writes through, in the shape of RFC 8435 section 5.1.
type FlexDataServer struct {
DeviceID [16]byte
Efficiency uint32
Stateid Stateid
// FHs carries one file handle per file handle format the data server
// speaks; one entry is the common case.
FHs [][]byte
User string
Group string
}
// A FlexMirror is one ff_mirror4: a list of equivalent data servers that
// hold the same bytes.
type FlexMirror struct {
DataServers []FlexDataServer
}
// AppendFlexFileLayoutBody encodes the ff_layout4: the stripe unit, the
// mirrors with their data servers, the flags and the stats hint.
func AppendFlexFileLayoutBody(b []byte, flags uint32, stripeUnit uint64, mirrors []FlexMirror) []byte {
b = xdr.AppendUint64(b, stripeUnit)
b = xdr.AppendUint32(b, uint32(len(mirrors)))
for _, m := range mirrors {
b = xdr.AppendUint32(b, uint32(len(m.DataServers)))
for _, ds := range m.DataServers {
b = append(b, ds.DeviceID[:]...)
b = xdr.AppendUint32(b, ds.Efficiency)
b = ds.Stateid.Append(b)
b = xdr.AppendUint32(b, uint32(len(ds.FHs)))
for _, fh := range ds.FHs {
b = xdr.AppendVarOpaque(b, fh)
}
b = xdr.AppendString(b, ds.User)
b = xdr.AppendString(b, ds.Group)
}
}
b = xdr.AppendUint32(b, flags)
return xdr.AppendUint32(b, 0) // stats collect hint
}
// A FlexDeviceAddr is one ff_device_addr4: the net addresses of the data
// servers and the versions they speak.
type FlexDeviceAddr struct {
NetAddrs []NetAddr
Versions []FlexVersion
}
// A NetAddr is one netaddr4 of RFC 5661: a netid and a universal address.
type NetAddr struct {
Netid string
Uaddr string
}
// A FlexVersion is one ff_device_versions4: an NFS version a data server
// speaks and the transfer sizes it allows.
type FlexVersion struct {
Version uint32
MinorVersion uint32
RSize uint32
WSize uint32
TightlyBound bool
}
// AppendFlexDeviceAddr encodes the ff_device_addr4: the net addresses and
// the NFS versions the data servers speak.
func AppendFlexDeviceAddr(b []byte, d FlexDeviceAddr) []byte {
b = xdr.AppendUint32(b, uint32(len(d.NetAddrs)))
for _, na := range d.NetAddrs {
b = xdr.AppendString(b, na.Netid)
b = xdr.AppendString(b, na.Uaddr)
}
b = xdr.AppendUint32(b, uint32(len(d.Versions)))
for _, v := range d.Versions {
b = xdr.AppendUint32(b, v.Version)
b = xdr.AppendUint32(b, v.MinorVersion)
b = xdr.AppendUint32(b, v.RSize)
b = xdr.AppendUint32(b, v.WSize)
b = xdr.AppendBool(b, v.TightlyBound)
}
return b
}
// DecodeFlexDeviceAddr decodes an ff_device_addr4 body.
func DecodeFlexDeviceAddr(body []byte) (FlexDeviceAddr, error) {
d := xdr.NewDecoder(body)
var out FlexDeviceAddr
netCount, err := d.Uint32()
if err != nil {
return out, err
}
for range netCount {
netid, err := d.String()
if err != nil {
return out, err
}
uaddr, err := d.String()
if err != nil {
return out, err
}
out.NetAddrs = append(out.NetAddrs, NetAddr{Netid: netid, Uaddr: uaddr})
}
verCount, err := d.Uint32()
if err != nil {
return out, err
}
for range verCount {
v, err := d.Uint32()
if err != nil {
return out, err
}
minor, err := d.Uint32()
if err != nil {
return out, err
}
rsize, err := d.Uint32()
if err != nil {
return out, err
}
wsize, err := d.Uint32()
if err != nil {
return out, err
}
tight, err := d.Bool()
if err != nil {
return out, err
}
out.Versions = append(out.Versions, FlexVersion{Version: v, MinorVersion: minor, RSize: rsize, WSize: wsize, TightlyBound: tight})
}
return out, nil
}