diff --git a/cmd/serve/nbd/chunked_backend.go b/cmd/serve/nbd/chunked_backend.go new file mode 100644 index 000000000..fb4a80d1c --- /dev/null +++ b/cmd/serve/nbd/chunked_backend.go @@ -0,0 +1,140 @@ +// Implements an nbd.Backend for serving from a chunked file in the VFS. + +package nbd + +import ( + "errors" + "fmt" + + "github.com/rclone/gonbdserver/nbd" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/log" + "github.com/rclone/rclone/vfs/chunked" + "golang.org/x/net/context" +) + +// Backend for a single chunked file +type chunkedBackend struct { + file *chunked.File + ec *nbd.ExportConfig +} + +// Create Backend for a single chunked file +type chunkedBackendFactory struct { + s *NBD + file *chunked.File +} + +// WriteAt implements Backend.WriteAt +func (cb *chunkedBackend) WriteAt(ctx context.Context, b []byte, offset int64, fua bool) (n int, err error) { + defer log.Trace(logPrefix, "size=%d, off=%d", len(b), offset)("n=%d, err=%v", &n, &err) + n, err = cb.file.WriteAt(b, offset) + if err != nil || !fua { + return n, err + } + err = cb.file.Sync() + if err != nil { + return 0, err + } + return n, err +} + +// ReadAt implements Backend.ReadAt +func (cb *chunkedBackend) ReadAt(ctx context.Context, b []byte, offset int64) (n int, err error) { + defer log.Trace(logPrefix, "size=%d, off=%d", len(b), offset)("n=%d, err=%v", &n, &err) + return cb.file.ReadAt(b, offset) +} + +// TrimAt implements Backend.TrimAt +func (cb *chunkedBackend) TrimAt(ctx context.Context, length int, offset int64) (n int, err error) { + defer log.Trace(logPrefix, "size=%d, off=%d", length, offset)("n=%d, err=%v", &n, &err) + return length, nil +} + +// Flush implements Backend.Flush +func (cb *chunkedBackend) Flush(ctx context.Context) (err error) { + defer log.Trace(logPrefix, "")("err=%v", &err) + return nil +} + +// Close implements Backend.Close +func (cb *chunkedBackend) Close(ctx context.Context) (err error) { + defer log.Trace(logPrefix, "")("err=%v", &err) + err = cb.file.Close() + return nil +} + +// Geometry implements Backend.Geometry +func (cb *chunkedBackend) Geometry(ctx context.Context) (size uint64, minBS uint64, prefBS uint64, maxBS uint64, err error) { + defer log.Trace(logPrefix, "")("size=%d, minBS=%d, prefBS=%d, maxBS=%d, err=%v", &size, &minBS, &prefBS, &maxBS, &err) + size = uint64(cb.file.Size()) + minBS = cb.ec.MinimumBlockSize + prefBS = cb.ec.PreferredBlockSize + maxBS = cb.ec.MaximumBlockSize + err = nil + return +} + +// HasFua implements Backend.HasFua +func (cb *chunkedBackend) HasFua(ctx context.Context) (fua bool) { + defer log.Trace(logPrefix, "")("fua=%v", &fua) + return true +} + +// HasFlush implements Backend.HasFua +func (cb *chunkedBackend) HasFlush(ctx context.Context) (flush bool) { + defer log.Trace(logPrefix, "")("flush=%v", &flush) + return true +} + +// New generates a new chunked backend +func (cbf *chunkedBackendFactory) newBackend(ctx context.Context, ec *nbd.ExportConfig) (nbd.Backend, error) { + err := cbf.file.Open(false, 0) + if err != nil { + return nil, fmt.Errorf("failed to open chunked file: %w", err) + } + cb := &chunkedBackend{ + file: cbf.file, + ec: ec, + } + return cb, nil +} + +// Generate a chunked backend factory +func (s *NBD) newChunkedBackendFactory(ctx context.Context) (bf backendFactory, err error) { + create := s.opt.Create > 0 + if s.vfs.Opt.ReadOnly && create { + return nil, errors.New("can't create files with --read-only") + } + file := chunked.New(s.vfs, s.leaf) + err = file.Open(create, s.log2ChunkSize) + if err != nil { + return nil, fmt.Errorf("failed to open chunked file: %w", err) + } + defer fs.CheckClose(file, &err) + var truncateSize fs.SizeSuffix + if create { + if file.Size() == 0 { + truncateSize = s.opt.Create + } + } else { + truncateSize = s.opt.Resize + } + if truncateSize > 0 { + err = file.Truncate(int64(truncateSize)) + if err != nil { + return nil, fmt.Errorf("failed to create chunked file: %w", err) + } + fs.Logf(logPrefix, "Size of network block device is now %v", truncateSize) + } + return &chunkedBackendFactory{ + s: s, + file: file, + }, nil +} + +// Check interfaces +var ( + _ nbd.Backend = (*chunkedBackend)(nil) + _ backendFactory = (*chunkedBackendFactory)(nil) +) diff --git a/cmd/serve/nbd/file_backend.go b/cmd/serve/nbd/file_backend.go new file mode 100644 index 000000000..2126596d8 --- /dev/null +++ b/cmd/serve/nbd/file_backend.go @@ -0,0 +1,140 @@ +// Implements an nbd.Backend for serving from the VFS. + +package nbd + +import ( + "fmt" + "os" + + "github.com/rclone/gonbdserver/nbd" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/log" + "github.com/rclone/rclone/vfs" + "golang.org/x/net/context" +) + +// Backend for a single file +type fileBackend struct { + file vfs.Handle + ec *nbd.ExportConfig +} + +// Create Backend for a single file +type fileBackendFactory struct { + s *NBD + vfs *vfs.VFS + filePath string + perms int +} + +// WriteAt implements Backend.WriteAt +func (fb *fileBackend) WriteAt(ctx context.Context, b []byte, offset int64, fua bool) (n int, err error) { + defer log.Trace(logPrefix, "size=%d, off=%d", len(b), offset)("n=%d, err=%v", &n, &err) + n, err = fb.file.WriteAt(b, offset) + if err != nil || !fua { + return n, err + } + err = fb.file.Sync() + if err != nil { + return 0, err + } + return n, err +} + +// ReadAt implements Backend.ReadAt +func (fb *fileBackend) ReadAt(ctx context.Context, b []byte, offset int64) (n int, err error) { + defer log.Trace(logPrefix, "size=%d, off=%d", len(b), offset)("n=%d, err=%v", &n, &err) + return fb.file.ReadAt(b, offset) +} + +// TrimAt implements Backend.TrimAt +func (fb *fileBackend) TrimAt(ctx context.Context, length int, offset int64) (n int, err error) { + defer log.Trace(logPrefix, "size=%d, off=%d", length, offset)("n=%d, err=%v", &n, &err) + return length, nil +} + +// Flush implements Backend.Flush +func (fb *fileBackend) Flush(ctx context.Context) (err error) { + defer log.Trace(logPrefix, "")("err=%v", &err) + return nil +} + +// Close implements Backend.Close +func (fb *fileBackend) Close(ctx context.Context) (err error) { + defer log.Trace(logPrefix, "")("err=%v", &err) + err = fb.file.Close() + return nil +} + +// Geometry implements Backend.Geometry +func (fb *fileBackend) Geometry(ctx context.Context) (size uint64, minBS uint64, prefBS uint64, maxBS uint64, err error) { + defer log.Trace(logPrefix, "")("size=%d, minBS=%d, prefBS=%d, maxBS=%d, err=%v", &size, &minBS, &prefBS, &maxBS, &err) + fi, err := fb.file.Stat() + if err != nil { + err = fmt.Errorf("failed read info about open backing file: %w", err) + return + } + size = uint64(fi.Size()) + minBS = fb.ec.MinimumBlockSize + prefBS = fb.ec.PreferredBlockSize + maxBS = fb.ec.MaximumBlockSize + err = nil + return +} + +// HasFua implements Backend.HasFua +func (fb *fileBackend) HasFua(ctx context.Context) (fua bool) { + defer log.Trace(logPrefix, "")("fua=%v", &fua) + return true +} + +// HasFlush implements Backend.HasFua +func (fb *fileBackend) HasFlush(ctx context.Context) (flush bool) { + defer log.Trace(logPrefix, "")("flush=%v", &flush) + return true +} + +// open the backing file +func (fbf *fileBackendFactory) open() (vfs.Handle, error) { + return fbf.vfs.OpenFile(fbf.filePath, fbf.perms, 0700) +} + +// New generates a new file backend +func (fbf *fileBackendFactory) newBackend(ctx context.Context, ec *nbd.ExportConfig) (nbd.Backend, error) { + fd, err := fbf.open() + if err != nil { + return nil, fmt.Errorf("failed to open backing file: %w", err) + } + fb := &fileBackend{ + file: fd, + ec: ec, + } + return fb, nil +} + +// Generate a file backend factory +func (s *NBD) newFileBackendFactory(ctx context.Context) (bf backendFactory, err error) { + perms := os.O_RDWR + if s.vfs.Opt.ReadOnly { + perms = os.O_RDONLY + } + fbf := &fileBackendFactory{ + s: s, + vfs: s.vfs, + perms: perms, + filePath: s.leaf, + } + // Try opening the file so we get errors now rather than later when they are more difficult to report. + fd, err := fbf.open() + if err != nil { + return nil, fmt.Errorf("failed to open backing file: %w", err) + } + defer fs.CheckClose(fd, &err) + return fbf, nil +} + +// Check interfaces +var ( + _ nbd.Backend = (*fileBackend)(nil) + _ backendFactory = (*fileBackendFactory)(nil) +) diff --git a/cmd/serve/nbd/nbd.go b/cmd/serve/nbd/nbd.go new file mode 100644 index 000000000..e3b27e9c6 --- /dev/null +++ b/cmd/serve/nbd/nbd.go @@ -0,0 +1,260 @@ +// Package nbd provides a network block device server +package nbd + +import ( + "bufio" + "context" + _ "embed" + "errors" + "fmt" + "io" + "log" + "math/bits" + "path/filepath" + "strings" + "sync" + + "github.com/rclone/gonbdserver/nbd" + "github.com/rclone/rclone/cmd" + "github.com/rclone/rclone/cmd/serve/proxy/proxyflags" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config/flags" + "github.com/rclone/rclone/lib/systemd" + "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfscommon" + "github.com/rclone/rclone/vfs/vfsflags" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const logPrefix = "nbd" + +// OptionsInfo descripts the Options in use +var OptionsInfo = fs.Options{{ + Name: "addr", + Default: "localhost:10809", + Help: "IPaddress:Port or :Port to bind server to", +}, { + Name: "min_block_size", + Default: fs.SizeSuffix(512), // FIXME + Help: "Minimum block size to advertise", +}, { + Name: "preferred_block_size", + Default: fs.SizeSuffix(4096), // FIXME this is the max according to nbd-client + Help: "Preferred block size to advertise", +}, { + Name: "max_block_size", + Default: fs.SizeSuffix(1024 * 1024), // FIXME, + Help: "Maximum block size to advertise", +}, { + Name: "create", + Default: fs.SizeSuffix(-1), + Help: "If the destination does not exist, create it with this size", +}, { + Name: "chunk_size", + Default: fs.SizeSuffix(0), + Help: "If creating the destination use this chunk size. Must be a power of 2.", +}, { + Name: "resize", + Default: fs.SizeSuffix(-1), + Help: "If the destination exists, resize it to this size", +}} + +// name := flag.String("name", "default", "Export name") +// description := flag.String("description", "The default export", "Export description") + +// Options required for nbd server +type Options struct { + ListenAddr string `config:"addr"` // Port to listen on + MinBlockSize fs.SizeSuffix `config:"min_block_size"` + PreferredBlockSize fs.SizeSuffix `config:"preferred_block_size"` + MaxBlockSize fs.SizeSuffix `config:"max_block_size"` + Create fs.SizeSuffix `config:"create"` + ChunkSize fs.SizeSuffix `config:"chunk_size"` + Resize fs.SizeSuffix `config:"resize"` +} + +func init() { + fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "nbd", Opt: &Opt, Options: OptionsInfo}) +} + +// Opt is options set by command line flags +var Opt Options + +// AddFlags adds flags for the nbd +func AddFlags(flagSet *pflag.FlagSet, Opt *Options) { + flags.AddFlagsFromOptions(flagSet, "", OptionsInfo) +} + +func init() { + flagSet := Command.Flags() + vfsflags.AddFlags(flagSet) + proxyflags.AddFlags(flagSet) + AddFlags(flagSet, &Opt) +} + +//go:embed nbd.md +var helpText string + +// Command definition for cobra +var Command = &cobra.Command{ + Use: "nbd remote:path", + Short: `Serve the remote over NBD.`, + Long: helpText + vfs.Help(), + Annotations: map[string]string{ + "versionIntroduced": "v1.65", + "status": "experimental", + }, + Run: func(command *cobra.Command, args []string) { + // FIXME could serve more than one nbd? + cmd.CheckArgs(1, 1, command, args) + f, leaf := cmd.NewFsFile(args[0]) + + cmd.Run(false, true, command, func() error { + s, err := run(context.Background(), f, leaf, Opt) + if err != nil { + log.Fatal(err) + } + + defer systemd.Notify()() + // FIXME + _ = s + s.Wait() + return nil + }) + }, +} + +// NBD contains everything to run the server +type NBD struct { + f fs.Fs + leaf string + vfs *vfs.VFS // don't use directly, use getVFS + opt Options + wg sync.WaitGroup + sessionWaitGroup sync.WaitGroup + logRd *io.PipeReader + logWr *io.PipeWriter + log2ChunkSize uint + readOnly bool // Set for read only by vfs config + + backendFactory backendFactory +} + +// interface for creating backend factories +type backendFactory interface { + newBackend(ctx context.Context, ec *nbd.ExportConfig) (nbd.Backend, error) +} + +// Create and start the server for nbd either on directory f or using file leaf in f +func run(ctx context.Context, f fs.Fs, leaf string, opt Options) (s *NBD, err error) { + s = &NBD{ + f: f, + leaf: leaf, + opt: opt, + vfs: vfs.New(f, &vfscommon.Opt), + readOnly: vfscommon.Opt.ReadOnly, + } + + if opt.ChunkSize != 0 { + if set := bits.OnesCount64(uint64(opt.ChunkSize)); set != 1 { + return nil, fmt.Errorf("--chunk-size must be a power of 2 (counted %d bits set)", set) + } + s.log2ChunkSize = uint(bits.TrailingZeros64(uint64(opt.ChunkSize))) + fs.Debugf(logPrefix, "Using ChunkSize %v (%v), Log2ChunkSize %d", opt.ChunkSize, fs.SizeSuffix(1< /home/ncw/go/src/github.com/rclone/gonbdserver + require ( bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 @@ -53,6 +55,7 @@ require ( github.com/prometheus/client_golang v1.19.1 github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8 github.com/rclone/gofakes3 v0.0.3-0.20240716093803-d6abc178be56 + github.com/rclone/gonbdserver v0.0.0-20230928185136-7adb4642e1cb github.com/rfjakob/eme v1.1.2 github.com/rivo/uniseg v0.4.7 github.com/rogpeppe/go-internal v1.12.0 diff --git a/go.sum b/go.sum index f0aaa8f62..0f7045e23 100644 --- a/go.sum +++ b/go.sum @@ -460,6 +460,8 @@ github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQ github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o= github.com/rclone/gofakes3 v0.0.3-0.20240716093803-d6abc178be56 h1:JmCt3EsTnlZrg/PHIyZqvKDRvBCde/rmThAQFliE9bU= github.com/rclone/gofakes3 v0.0.3-0.20240716093803-d6abc178be56/go.mod h1:L0VIBE0mT6ArN/5dfHsJm3UjqCpi5B/cdN+qWDNh7ko= +github.com/rclone/gonbdserver v0.0.0-20230928185136-7adb4642e1cb h1:4FyF15nQLPIhLcJDpn2ItwcuO3E/pYQXdPVOt+v3Duk= +github.com/rclone/gonbdserver v0.0.0-20230928185136-7adb4642e1cb/go.mod h1:HwROhGq4gA7vncM5mLZjoNbI9CrS52aDuHReB3NMWp4= github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4=