diff --git a/weed/shell/command_nfs_disable.go b/weed/shell/command_nfs_disable.go new file mode 100644 index 000000000..fb203c7ad --- /dev/null +++ b/weed/shell/command_nfs_disable.go @@ -0,0 +1,177 @@ +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 +} diff --git a/weed/shell/command_nfs_enable.go b/weed/shell/command_nfs_enable.go new file mode 100644 index 000000000..8398c29f4 --- /dev/null +++ b/weed/shell/command_nfs_enable.go @@ -0,0 +1,219 @@ +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" +) + +func init() { + Commands = append(Commands, &commandNfsEnable{}) +} + +type commandNfsEnable struct { +} + +func (c *commandNfsEnable) Name() string { + return "nfs.enable" +} + +func (c *commandNfsEnable) Help() string { + return `enable NFS export support for a filer path + + # preview the filer.conf change + nfs.enable -path=/exports + + # persist it and backfill the index for existing entries + nfs.enable -path=/exports -apply + + The weed nfs gateway resolves NFS filehandles through an inode->path + index. This command marks the path with inode_index in filer.conf, so + every filer maintains index rows for entries under it (no restarts + needed), then walks the existing entries and writes their rows so + pre-existing files are immediately resolvable. Reverse with nfs.disable. +` +} + +func (c *commandNfsEnable) HasTag(CommandTag) bool { + return false +} + +func (c *commandNfsEnable) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) { + + nfsEnableCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + path := nfsEnableCommand.String("path", "", "filer path to export over NFS") + apply := nfsEnableCommand.Bool("apply", false, "update filer.conf and backfill the inode index") + if err = nfsEnableCommand.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 err = fc.AddLocationConf(&filer_pb.FilerConf_PathConf{ + LocationPrefix: *path, + InodeIndex: true, + }); err != nil { + return err + } + + 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 + } + + return backfillInodeIndex(commandEnv, writer, util.FullPath(*path)) +} + +func backfillInodeIndex(commandEnv *CommandEnv, writer io.Writer, exportPath util.FullPath) error { + var indexed, assigned atomic.Int64 + + if exportPath != "/" { + rootEntry, err := lookupPathEntry(commandEnv, exportPath) + if err == filer_pb.ErrNotFound { + fmt.Fprintf(writer, "%s does not exist yet; nothing to backfill\n", exportPath) + return nil + } + if err != nil { + return err + } + changed, err := backfillEntryInodeIndex(commandEnv, exportPath, rootEntry, &assigned) + if err != nil { + return err + } + if changed { + indexed.Add(1) + } + } + + err := filer_pb.TraverseBfs(context.Background(), commandEnv, exportPath, func(parentPath util.FullPath, entry *filer_pb.Entry) error { + fullPath := util.NewFullPath(string(parentPath), entry.Name) + changed, err := backfillEntryInodeIndex(commandEnv, fullPath, entry, &assigned) + if err != nil { + return err + } + if changed { + if count := indexed.Add(1); count%10000 == 0 { + fmt.Fprintf(writer, "indexed %d entries ...\n", count) + } + } + return nil + }) + if err != nil { + return fmt.Errorf("backfill inode index under %s: %v", exportPath, err) + } + + fmt.Fprintf(writer, "indexed %d entries, assigned %d missing inodes\n", indexed.Load(), assigned.Load()) + return nil +} + +func backfillEntryInodeIndex(commandEnv *CommandEnv, fullPath util.FullPath, entry *filer_pb.Entry, assigned *atomic.Int64) (changed bool, err error) { + inode := entry.GetAttributes().GetInode() + if inode == 0 { + // Entry predates filer-assigned inodes: re-save it so the filer + // assigns and persists one, then read the assignment back. + dir, name := fullPath.DirAndName() + if err := commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { + if _, updateErr := client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ + Directory: dir, + Entry: entry, + }); updateErr != nil { + return updateErr + } + resp, lookupErr := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{ + Directory: dir, + Name: name, + }) + if lookupErr != nil { + return lookupErr + } + inode = resp.Entry.GetAttributes().GetInode() + return nil + }); err != nil { + return false, fmt.Errorf("assign inode for %s: %v", fullPath, err) + } + if inode == 0 { + return false, fmt.Errorf("filer did not assign an inode for %s", fullPath) + } + assigned.Add(1) + } + return upsertInodeIndexRow(commandEnv, fullPath, inode) +} + +func lookupPathEntry(commandEnv *CommandEnv, fullPath util.FullPath) (entry *filer_pb.Entry, err error) { + dir, name := fullPath.DirAndName() + err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { + resp, lookupErr := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{ + Directory: dir, + Name: name, + }) + if lookupErr != nil { + return lookupErr + } + entry = resp.Entry + return nil + }) + return +} + +func upsertInodeIndexRow(commandEnv *CommandEnv, fullPath util.FullPath, inode uint64) (changed bool, err error) { + 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()) + } + record, decodeErr := filer.DecodeInodeIndexRecord(resp.GetValue()) + if decodeErr != nil { + // Unreadable row: rebuild it from this path. + record = &filer.InodeIndexRecord{} + } + if !record.AddPath(fullPath) { + return nil + } + value, encodeErr := record.Encode() + if 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()) + } + changed = true + return nil + }) + if err != nil { + return false, fmt.Errorf("index inode %d for %s: %v", inode, fullPath, err) + } + return changed, nil +}