Skip to content

Commit 2235311

Browse files
committed
Initial NFSv4 client
1 parent 88bcd8f commit 2235311

24 files changed

+3272
-481
lines changed

.github/workflows/main.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
on: [push]
2+
3+
jobs:
4+
test:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v2
8+
- name: Build the Docker image
9+
run: docker build -t local:test .
10+
- name: Test the NFS
11+
run: docker run local:test

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM golang:1.15-buster
2+
3+
RUN apt-get update; apt-get install -fy nfs-ganesha nfs-ganesha-mem vim
4+
RUN mkdir /nfs
5+
COPY ganesha.conf /etc/ganesha
6+
COPY run-tests.sh /
7+
8+
# Prepare the server
9+
RUN mkdir /app
10+
WORKDIR /app
11+
12+
COPY go.mod go.sum ./
13+
RUN go mod download
14+
COPY . ./
15+
RUN go build -o runtests cmd/main/runtests.go
16+
17+
EXPOSE 2049/tcp
18+
ENTRYPOINT /run-tests.sh

Makefile

Lines changed: 0 additions & 4 deletions
This file was deleted.

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Pure-Go NFSv4 client
2+
This library is a pure-Go client for NFSv4 (NFSv3 is NOT supported at all). It's mostly
3+
designed and tested for AWS EFS, but will work fine with Linux's `nfs4-server` or userspace-based
4+
Ganesha. It's fully synchronous, does nothing behind your back and fully supports
5+
`context.Context`-based cancellation and deadlines.
6+
7+
It's also a fairly minimal library with lots of limitations:
8+
1. No support for locking.
9+
2. No support for reconnection and session resumption.
10+
3. Minimalistic API.
11+
4. No support for ACLs or extended attributes.
12+
5. No support for any authentication methods.
13+
14+
# Usage example
15+
See `cmd/main/runtests.go` for the usage examples.
16+
17+
## Regenerating the XDR bindings
18+
If you need to regenerate the XDR bindings, then there's some manual work involved.
19+
20+
First use:
21+
```bash
22+
go run github.com/xdrpp/goxdr/cmd/goxdr -B -enum-comments -p internal internal/nfs4.x > internal/nfs4.go
23+
go run github.com/xdrpp/goxdr/cmd/goxdr -b -enum-comments -p internal internal/rpc.x > internal/rpc.go
24+
```
25+
26+
After this, manually do the following:
27+
1. Rename `_u` to `_U`.
28+
2. Rename `xdrProc_NFSPROC4_COMPOUND` to `XdrProc_NFSPROC4_COMPOUND`
29+
3. Rename `xdrProc_NFSPROC4_NULL` to `XdrProc_NFSPROC4_NULL`
30+
4. Change all the methods like `XDR_Offset4(v *Offset4) XdrType_Offset4` to return pointers (not values).

cmd/main/runtests.go

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/rand"
7+
"fmt"
8+
"github.com/aurorasolar/go-nfs-client/nfs4"
9+
"github.com/stretchr/testify/assert"
10+
"os"
11+
"strings"
12+
"sync"
13+
"sync/atomic"
14+
"time"
15+
)
16+
17+
const NumScaleThreads = 20
18+
const UploadSize = 1024*1024*5
19+
const UploadTarget = 1024*1024*1024*5
20+
21+
//goland:noinspection GoNilness
22+
func runTests(server, rootPath string) error {
23+
ctx := context.Background()
24+
25+
hostname, _ := os.Hostname()
26+
client, err := nfs4.NewNfsClient(ctx, server, nfs4.AuthParams{
27+
MachineName: hostname,
28+
})
29+
if err != nil {
30+
return err
31+
}
32+
defer client.Close()
33+
34+
if rootPath[len(rootPath)-1] != '/' {
35+
rootPath += "/"
36+
}
37+
38+
// Remove anything from the old tests
39+
println("Removing old files")
40+
err = nfs4.RemoveRecursive(client, rootPath + "tests")
41+
checkErr(err)
42+
43+
println("Making tests directory")
44+
err = client.MakePath(rootPath + "tests/directory/a")
45+
checkErr(err)
46+
47+
// Path creation is idempotent
48+
err = client.MakePath(rootPath + "tests/directory/a")
49+
checkErr(err)
50+
51+
println("Checking directory info")
52+
fi, err := client.GetFileInfo(rootPath + "tests/directory/a")
53+
checkErr(err)
54+
check(fi.IsDir && fi.Name == "a")
55+
check(approxNow(fi))
56+
57+
println("Checking directory deletion")
58+
err = client.DeleteFile(rootPath + "tests/directory/a")
59+
checkErr(err)
60+
_, err = client.GetFileInfo(rootPath + "tests/directory/a")
61+
check(nfs4.IsNfsError(err, nfs4.ERROR_NOENT))
62+
63+
// Deletion will fail with non-empty dir
64+
err = client.DeleteFile(rootPath + "tests")
65+
check(nfs4.IsNfsError(err, nfs4.ERROR_NOTEMPTY))
66+
67+
// Check file ops
68+
testFileOps(client, rootPath + "tests/")
69+
70+
testMassOps(client, rootPath + "tests/")
71+
72+
println("Cleaning up")
73+
err = nfs4.RemoveRecursive(client, rootPath + "tests")
74+
checkErr(err)
75+
76+
return nil
77+
}
78+
79+
//goland:noinspection GoNilness
80+
func testFileOps(cli nfs4.NfsInterface, path string) {
81+
data := make([]byte, 20*1024*1024)
82+
// Ganesha replaces all the data written by letters 'a'
83+
for i := range data {
84+
data[i] = 'a'
85+
}
86+
87+
println("Checking file upload")
88+
written, err := cli.ReWriteFile(path+"file.bin", bytes.NewReader(data))
89+
checkErr(err)
90+
check(written == uint64(len(data)))
91+
92+
println("Checking file download")
93+
buffer := bytes.NewBufferString("")
94+
read, err := cli.ReadFileAll(path + "file.bin", buffer)
95+
checkErr(err)
96+
check(read == uint64(len(data)))
97+
check(assert.ObjectsAreEqual(buffer.Bytes(), data))
98+
99+
println("Checking file meta")
100+
info, err := cli.GetFileInfo(path + "file.bin")
101+
checkErr(err)
102+
check(info.Size == uint64(len(data)))
103+
check(!info.IsDir)
104+
check(info.Name == "file.bin")
105+
check(approxNow(info))
106+
107+
println("Checking file deletion")
108+
err = cli.DeleteFile(path + "file.bin")
109+
checkErr(err)
110+
111+
println("Verifying file deletion")
112+
_, err = cli.GetFileInfo(path + "file.bin")
113+
check(nfs4.IsNfsError(err, nfs4.ERROR_NOENT))
114+
_, err = cli.ReadFileAll(path + "file.bin", buffer)
115+
check(nfs4.IsNfsError(err, nfs4.ERROR_NOENT))
116+
}
117+
118+
func testMassOps(cli nfs4.NfsInterface, path string) {
119+
println("Making the mass directory")
120+
err := cli.MakePath(path+"/mass")
121+
checkErr(err)
122+
123+
println("Checking creation")
124+
st := time.Now()
125+
files := make(map[string]bool)
126+
for i := 0; i<2000; i++ {
127+
curFile := fmt.Sprintf("%s/mass/file-%d", path, i)
128+
_, err = cli.ReWriteFile(curFile, strings.NewReader("aaaaaaa"))
129+
files[fmt.Sprintf("file-%d", i)] = true
130+
}
131+
println("Time diff (ms): ", time.Now().Sub(st).Milliseconds())
132+
println("Getting the file list")
133+
134+
lst, err := cli.GetFileList(path+"/mass")
135+
checkErr(err)
136+
for _, l := range lst {
137+
check(!l.IsDir)
138+
check(l.Size == 7)
139+
check(approxNow(l))
140+
delete(files, l.Name)
141+
}
142+
check(len(files) == 0)
143+
}
144+
145+
func approxNow(fi nfs4.FileInfo) bool {
146+
ms := fi.Mtime.Sub(time.Now()).Milliseconds()
147+
if ms < 0 {
148+
ms = -ms
149+
}
150+
return ms < 600000
151+
}
152+
153+
func checkErr(err error) {
154+
if err != nil {
155+
panic(err.Error())
156+
}
157+
}
158+
159+
func check(b bool) {
160+
if !b {
161+
panic("Check failed")
162+
}
163+
}
164+
165+
func runScale(server, rootPath string) error {
166+
ctx := context.Background()
167+
168+
hostname, _ := os.Hostname()
169+
var nfsClients []nfs4.NfsInterface
170+
171+
defer func() {
172+
for _, c := range nfsClients {
173+
c.Close()
174+
}
175+
}()
176+
177+
for i := 0; i<NumScaleThreads; i++ {
178+
client, err := nfs4.NewNfsClient(ctx, server, nfs4.AuthParams{
179+
MachineName: hostname,
180+
})
181+
if err != nil {
182+
return err
183+
}
184+
nfsClients = append(nfsClients, client)
185+
}
186+
187+
if rootPath[len(rootPath)-1] != '/' {
188+
rootPath += "/"
189+
}
190+
191+
// Remove anything from the old tests
192+
println("Removing old files")
193+
err := nfs4.RemoveRecursive(nfsClients[0], rootPath + "scale")
194+
checkErr(err)
195+
196+
err = nfsClients[0].MakePath(rootPath + "scale")
197+
checkErr(err)
198+
199+
defer func() {
200+
println("After test cleanup")
201+
_ = nfs4.RemoveRecursive(nfsClients[0], rootPath + "scale")
202+
}()
203+
204+
data := make([]byte, UploadSize)
205+
_, _ = rand.Read(data)
206+
207+
// Now run the scale test - upload files in multiple threads until we
208+
// reach the desired number of uploads
209+
infoMtx := sync.Mutex{}
210+
uploadedBytes := uint64(0)
211+
doneUploading := false
212+
start := time.Now()
213+
214+
var count int32
215+
wait := sync.WaitGroup{}
216+
println("Running the test threads")
217+
for _, c := range nfsClients {
218+
wait.Add(1)
219+
go func(c nfs4.NfsInterface) {
220+
defer wait.Done()
221+
for ;!doneUploading; {
222+
nm := fmt.Sprintf(rootPath + "scale/test-%d", atomic.AddInt32(&count, 1))
223+
n, err := c.ReWriteFile(nm, bytes.NewReader(data))
224+
if err != nil {
225+
println("Error: ", err.Error())
226+
doneUploading = true
227+
}
228+
229+
curUploaded := atomic.AddUint64(&uploadedBytes, n)
230+
if curUploaded > UploadTarget {
231+
infoMtx.Lock()
232+
if !doneUploading {
233+
doneUploading = true
234+
ms := time.Now().Sub(start).Milliseconds()
235+
rate := (float64(curUploaded)*1000.0/float64(ms))/1024/1024
236+
println("Uploaded bytes: ", curUploaded, ", time(ms): ", ms,
237+
" rate (MB/s): ", int64(rate))
238+
}
239+
infoMtx.Unlock()
240+
}
241+
}
242+
}(c)
243+
}
244+
wait.Wait()
245+
246+
return nil
247+
}
248+
249+
func main() {
250+
if len(os.Args) < 4 {
251+
_, _ = os.Stderr.WriteString("Usage: runtests [scale|test] <NFS-server> <root-path>\n")
252+
os.Exit(1)
253+
}
254+
defer func() {
255+
p := recover()
256+
if p != nil {
257+
_, _ = os.Stderr.WriteString(fmt.Sprintf("%v\n", p))
258+
os.Exit(2)
259+
}
260+
}()
261+
262+
var err error
263+
264+
if os.Args[1] == "scale" {
265+
println("Running scalability tests")
266+
err = runScale(os.Args[2], os.Args[3])
267+
} else if os.Args[1] == "test" {
268+
println("Running correctness tests")
269+
err = runTests(os.Args[2], os.Args[3])
270+
} else {
271+
println("Unknown test mode: ", os.Args[1])
272+
os.Exit(3)
273+
}
274+
275+
if err != nil {
276+
_, _ = os.Stderr.WriteString(fmt.Sprintf("%v\n", err))
277+
os.Exit(2)
278+
}
279+
}

ganesha.conf

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
NFSV4 {
2+
Graceless = true;
3+
}
4+
5+
NFS_CORE_PARAM {
6+
Protocols = 4;
7+
}
8+
9+
EXPORT_DEFAULTS {
10+
Transports = TCP;
11+
SecType = "sys";
12+
}
13+
14+
NFS_KRB5 {
15+
Active_krb5 = false;
16+
}
17+
18+
EXPORT {
19+
# Export Id (mandatory, each EXPORT must have a unique Export_Id)
20+
Export_Id = 0;
21+
22+
Path = "/";
23+
# Pseudo Path (the logical root)
24+
Pseudo = "/";
25+
26+
FSAL {
27+
name = MEM;
28+
}
29+
30+
Access_type = RW;
31+
Disable_ACL = true;
32+
Squash = "No_Root_Squash";
33+
Protocols = "4";
34+
}

go.mod

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
module github.com/aurorasolar/go-nfs-cli
1+
module github.com/aurorasolar/go-nfs-client
22

33
go 1.15
44

5-
require github.com/xdrpp/goxdr v0.0.0-20200911101125-ad8466545dae // indirect
5+
require (
6+
github.com/davecgh/go-spew v1.1.1 // indirect
7+
github.com/kr/pretty v0.1.0 // indirect
8+
github.com/stretchr/testify v1.6.1
9+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
10+
)

0 commit comments

Comments
 (0)