Files
seaweedfs/weed/shell/command_nfs_disable.go
T
Chris Lu 6d876dfa3f shell: nfs.enable / nfs.disable
One command to make a filer path exportable over NFS: nfs.enable marks
the path with inode_index in filer.conf and backfills index rows for
the entries already under it, assigning inodes where missing, so
existing files resolve by filehandle right away. nfs.disable clears
the mark and deletes the rows, leaving no index data behind.

Claude-Session: https://claude.ai/code/session_01AQDAwEYZsGsmGZKo3myZxB
2026-07-08 16:58:50 -07:00

178 lines
5.0 KiB
Go

package shell
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"strings"
"sync/atomic"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/protobuf/proto"
)
func init() {
Commands = append(Commands, &commandNfsDisable{})
}
type commandNfsDisable struct {
}
func (c *commandNfsDisable) Name() string {
return "nfs.disable"
}
func (c *commandNfsDisable) Help() string {
return `disable NFS export support for a filer path
# preview the filer.conf change
nfs.disable -path=/exports
# persist it and delete the inode index rows under the path
nfs.disable -path=/exports -apply
Clears the inode_index mark that nfs.enable set on the path, then
walks the entries under it and removes their inode->path index rows,
so disabling leaves no index data behind. Pass -keepIndex to skip
the cleanup walk.
`
}
func (c *commandNfsDisable) HasTag(CommandTag) bool {
return false
}
func (c *commandNfsDisable) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
nfsDisableCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
path := nfsDisableCommand.String("path", "", "filer path that nfs.enable was applied to")
apply := nfsDisableCommand.Bool("apply", false, "update filer.conf and delete the inode index rows")
keepIndex := nfsDisableCommand.Bool("keepIndex", false, "keep existing inode index rows")
if err = nfsDisableCommand.Parse(args); err != nil {
return nil
}
if !strings.HasPrefix(*path, "/") {
return fmt.Errorf("-path must be an absolute filer path")
}
fc, err := filer.ReadFilerConf(commandEnv.option.FilerAddress, commandEnv.option.GrpcDialOption, commandEnv.MasterClient)
if err != nil {
return err
}
if locConf, found := fc.GetLocationConf(*path); found && locConf.InodeIndex {
locConf = filer.ClonePathConf(locConf)
locConf.InodeIndex = false
if proto.Equal(locConf, &filer_pb.FilerConf_PathConf{LocationPrefix: *path}) {
fc.DeleteLocationConf(*path)
} else if err = fc.SetLocationConf(locConf); err != nil {
return err
}
} else {
fmt.Fprintf(writer, "inode_index is not set on %s in filer.conf\n", *path)
}
var buf bytes.Buffer
fc.ToText(&buf)
fmt.Fprint(writer, buf.String())
fmt.Fprintln(writer)
if !*apply {
infoAboutSimulationMode(writer, *apply, "-apply")
return nil
}
if err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
return filer.SaveInsideFiler(context.Background(), client, filer.DirectoryEtcSeaweedFS, filer.FilerConfName, buf.Bytes())
}); err != nil && err != filer_pb.ErrNotFound {
return err
}
if *keepIndex {
return nil
}
return cleanupInodeIndex(commandEnv, writer, util.FullPath(*path))
}
func cleanupInodeIndex(commandEnv *CommandEnv, writer io.Writer, exportPath util.FullPath) error {
var cleared atomic.Int64
if exportPath != "/" {
rootEntry, err := lookupPathEntry(commandEnv, exportPath)
if err == filer_pb.ErrNotFound {
fmt.Fprintf(writer, "%s does not exist; nothing to clean up\n", exportPath)
return nil
}
if err != nil {
return err
}
if err := removeInodeIndexRow(commandEnv, exportPath, rootEntry.GetAttributes().GetInode(), &cleared); err != nil {
return err
}
}
err := filer_pb.TraverseBfs(context.Background(), commandEnv, exportPath, func(parentPath util.FullPath, entry *filer_pb.Entry) error {
fullPath := util.NewFullPath(string(parentPath), entry.Name)
if err := removeInodeIndexRow(commandEnv, fullPath, entry.GetAttributes().GetInode(), &cleared); err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("clean up inode index under %s: %v", exportPath, err)
}
fmt.Fprintf(writer, "cleared %d inode index rows\n", cleared.Load())
return nil
}
func removeInodeIndexRow(commandEnv *CommandEnv, fullPath util.FullPath, inode uint64, cleared *atomic.Int64) error {
if inode == 0 {
return nil
}
key := filer.InodeIndexKey(inode)
err := commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
resp, kvErr := client.KvGet(context.Background(), &filer_pb.KvGetRequest{Key: key})
if kvErr != nil {
return kvErr
}
if resp.GetError() != "" {
return errors.New(resp.GetError())
}
if len(resp.GetValue()) == 0 {
return nil
}
record, decodeErr := filer.DecodeInodeIndexRecord(resp.GetValue())
if decodeErr != nil || !record.RemovePath(fullPath) {
return nil
}
// A hard link can keep paths outside the export; only drop the row
// once no path remains. An empty KvPut value deletes the key.
var value []byte
if len(record.Paths) > 0 {
var encodeErr error
if value, encodeErr = record.Encode(); encodeErr != nil {
return encodeErr
}
}
putResp, putErr := client.KvPut(context.Background(), &filer_pb.KvPutRequest{Key: key, Value: value})
if putErr != nil {
return putErr
}
if putResp.GetError() != "" {
return errors.New(putResp.GetError())
}
cleared.Add(1)
return nil
})
if err != nil {
return fmt.Errorf("clear inode %d for %s: %v", inode, fullPath, err)
}
return nil
}