aboutsummaryrefslogtreecommitdiff
path: root/weed/shell
diff options
context:
space:
mode:
authorbingoohuang <bingoo.huang@gmail.com>2019-07-16 11:13:23 +0800
committerGitHub <noreply@github.com>2019-07-16 11:13:23 +0800
commitd19bbee98d89ec6cd603572bd9c5d55749610e61 (patch)
tree8d760dcee4dfcb4404af90b7d5e64def4549b4cc /weed/shell
parent01060c992591f412b0d5e180bde29991747a9462 (diff)
parent5b5e443d5b9985fd77f3d5470f1d5885a88bf2b9 (diff)
downloadseaweedfs-d19bbee98d89ec6cd603572bd9c5d55749610e61.tar.xz
seaweedfs-d19bbee98d89ec6cd603572bd9c5d55749610e61.zip
keep update from original (#1)
keep update from original
Diffstat (limited to 'weed/shell')
-rw-r--r--weed/shell/command_collection_delete.go51
-rw-r--r--weed/shell/command_collection_list.go59
-rw-r--r--weed/shell/command_ec_balance.go517
-rw-r--r--weed/shell/command_ec_common.go336
-rw-r--r--weed/shell/command_ec_encode.go289
-rw-r--r--weed/shell/command_ec_rebuild.go268
-rw-r--r--weed/shell/command_ec_test.go127
-rw-r--r--weed/shell/command_fs_cat.go68
-rw-r--r--weed/shell/command_fs_cd.go59
-rw-r--r--weed/shell/command_fs_du.go117
-rw-r--r--weed/shell/command_fs_ls.go148
-rw-r--r--weed/shell/command_fs_meta_load.go108
-rw-r--r--weed/shell/command_fs_meta_notify.go78
-rw-r--r--weed/shell/command_fs_meta_save.go150
-rw-r--r--weed/shell/command_fs_mv.go96
-rw-r--r--weed/shell/command_fs_pwd.go32
-rw-r--r--weed/shell/command_fs_tree.go147
-rw-r--r--weed/shell/command_volume_balance.go246
-rw-r--r--weed/shell/command_volume_copy.go53
-rw-r--r--weed/shell/command_volume_delete.go48
-rw-r--r--weed/shell/command_volume_fix_replication.go200
-rw-r--r--weed/shell/command_volume_list.go134
-rw-r--r--weed/shell/command_volume_mount.go60
-rw-r--r--weed/shell/command_volume_move.go126
-rw-r--r--weed/shell/command_volume_unmount.go60
-rw-r--r--weed/shell/commands.go130
-rw-r--r--weed/shell/shell_liner.go146
27 files changed, 3853 insertions, 0 deletions
diff --git a/weed/shell/command_collection_delete.go b/weed/shell/command_collection_delete.go
new file mode 100644
index 000000000..fbaddcd51
--- /dev/null
+++ b/weed/shell/command_collection_delete.go
@@ -0,0 +1,51 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "io"
+)
+
+func init() {
+ Commands = append(Commands, &commandCollectionDelete{})
+}
+
+type commandCollectionDelete struct {
+}
+
+func (c *commandCollectionDelete) Name() string {
+ return "collection.delete"
+}
+
+func (c *commandCollectionDelete) Help() string {
+ return `delete specified collection
+
+ collection.delete <collection_name>
+
+`
+}
+
+func (c *commandCollectionDelete) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) == 0 {
+ return nil
+ }
+
+ collectionName := args[0]
+
+ ctx := context.Background()
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ _, err = client.CollectionDelete(ctx, &master_pb.CollectionDeleteRequest{
+ Name: collectionName,
+ })
+ return err
+ })
+ if err != nil {
+ return
+ }
+
+ fmt.Fprintf(writer, "collection %s is deleted.\n", collectionName)
+
+ return nil
+}
diff --git a/weed/shell/command_collection_list.go b/weed/shell/command_collection_list.go
new file mode 100644
index 000000000..c4325c66f
--- /dev/null
+++ b/weed/shell/command_collection_list.go
@@ -0,0 +1,59 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "io"
+)
+
+func init() {
+ Commands = append(Commands, &commandCollectionList{})
+}
+
+type commandCollectionList struct {
+}
+
+func (c *commandCollectionList) Name() string {
+ return "collection.list"
+}
+
+func (c *commandCollectionList) Help() string {
+ return `list all collections`
+}
+
+func (c *commandCollectionList) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ collections, err := ListCollectionNames(commandEnv, true, true)
+
+ if err != nil {
+ return err
+ }
+
+ for _, c := range collections {
+ fmt.Fprintf(writer, "collection:\"%s\"\n", c)
+ }
+
+ fmt.Fprintf(writer, "Total %d collections.\n", len(collections))
+
+ return nil
+}
+
+func ListCollectionNames(commandEnv *CommandEnv, includeNormalVolumes, includeEcVolumes bool) (collections []string, err error) {
+ var resp *master_pb.CollectionListResponse
+ ctx := context.Background()
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ resp, err = client.CollectionList(ctx, &master_pb.CollectionListRequest{
+ IncludeNormalVolumes: includeNormalVolumes,
+ IncludeEcVolumes: includeEcVolumes,
+ })
+ return err
+ })
+ if err != nil {
+ return
+ }
+ for _, c := range resp.Collections {
+ collections = append(collections, c.Name)
+ }
+ return
+}
diff --git a/weed/shell/command_ec_balance.go b/weed/shell/command_ec_balance.go
new file mode 100644
index 000000000..47ae7bad3
--- /dev/null
+++ b/weed/shell/command_ec_balance.go
@@ -0,0 +1,517 @@
+package shell
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/chrislusf/seaweedfs/weed/storage/erasure_coding"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+)
+
+func init() {
+ Commands = append(Commands, &commandEcBalance{})
+}
+
+type commandEcBalance struct {
+}
+
+func (c *commandEcBalance) Name() string {
+ return "ec.balance"
+}
+
+func (c *commandEcBalance) Help() string {
+ return `balance all ec shards among all racks and volume servers
+
+ ec.balance [-c EACH_COLLECTION|<collection_name>] [-force] [-dataCenter <data_center>]
+
+ Algorithm:
+
+ For each type of volume server (different max volume count limit){
+ for each collection:
+ balanceEcVolumes(collectionName)
+ for each rack:
+ balanceEcRack(rack)
+ }
+
+ func balanceEcVolumes(collectionName){
+ for each volume:
+ doDeduplicateEcShards(volumeId)
+
+ tracks rack~shardCount mapping
+ for each volume:
+ doBalanceEcShardsAcrossRacks(volumeId)
+
+ for each volume:
+ doBalanceEcShardsWithinRacks(volumeId)
+ }
+
+ // spread ec shards into more racks
+ func doBalanceEcShardsAcrossRacks(volumeId){
+ tracks rack~volumeIdShardCount mapping
+ averageShardsPerEcRack = totalShardNumber / numRacks // totalShardNumber is 14 for now, later could varies for each dc
+ ecShardsToMove = select overflown ec shards from racks with ec shard counts > averageShardsPerEcRack
+ for each ecShardsToMove {
+ destRack = pickOneRack(rack~shardCount, rack~volumeIdShardCount, averageShardsPerEcRack)
+ destVolumeServers = volume servers on the destRack
+ pickOneEcNodeAndMoveOneShard(destVolumeServers)
+ }
+ }
+
+ func doBalanceEcShardsWithinRacks(volumeId){
+ racks = collect all racks that the volume id is on
+ for rack, shards := range racks
+ doBalanceEcShardsWithinOneRack(volumeId, shards, rack)
+ }
+
+ // move ec shards
+ func doBalanceEcShardsWithinOneRack(volumeId, shards, rackId){
+ tracks volumeServer~volumeIdShardCount mapping
+ averageShardCount = len(shards) / numVolumeServers
+ volumeServersOverAverage = volume servers with volumeId's ec shard counts > averageShardsPerEcRack
+ ecShardsToMove = select overflown ec shards from volumeServersOverAverage
+ for each ecShardsToMove {
+ destVolumeServer = pickOneVolumeServer(volumeServer~shardCount, volumeServer~volumeIdShardCount, averageShardCount)
+ pickOneEcNodeAndMoveOneShard(destVolumeServers)
+ }
+ }
+
+ // move ec shards while keeping shard distribution for the same volume unchanged or more even
+ func balanceEcRack(rack){
+ averageShardCount = total shards / numVolumeServers
+ for hasMovedOneEcShard {
+ sort all volume servers ordered by the number of local ec shards
+ pick the volume server A with the lowest number of ec shards x
+ pick the volume server B with the highest number of ec shards y
+ if y > averageShardCount and x +1 <= averageShardCount {
+ if B has a ec shard with volume id v that A does not have {
+ move one ec shard v from B to A
+ hasMovedOneEcShard = true
+ }
+ }
+ }
+ }
+
+`
+}
+
+func (c *commandEcBalance) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ balanceCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+ collection := balanceCommand.String("collection", "EACH_COLLECTION", "collection name, or \"EACH_COLLECTION\" for each collection")
+ dc := balanceCommand.String("dataCenter", "", "only apply the balancing for this dataCenter")
+ applyBalancing := balanceCommand.Bool("force", false, "apply the balancing plan")
+ if err = balanceCommand.Parse(args); err != nil {
+ return nil
+ }
+
+ ctx := context.Background()
+
+ // collect all ec nodes
+ allEcNodes, totalFreeEcSlots, err := collectEcNodes(ctx, commandEnv, *dc)
+ if err != nil {
+ return err
+ }
+ if totalFreeEcSlots < 1 {
+ return fmt.Errorf("no free ec shard slots. only %d left", totalFreeEcSlots)
+ }
+
+ racks := collectRacks(allEcNodes)
+
+ if *collection == "EACH_COLLECTION" {
+ collections, err := ListCollectionNames(commandEnv, false, true)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("balanceEcVolumes collections %+v\n", len(collections))
+ for _, c := range collections {
+ fmt.Printf("balanceEcVolumes collection %+v\n", c)
+ if err = balanceEcVolumes(commandEnv, c, allEcNodes, racks, *applyBalancing); err != nil {
+ return err
+ }
+ }
+ } else {
+ if err = balanceEcVolumes(commandEnv, *collection, allEcNodes, racks, *applyBalancing); err != nil {
+ return err
+ }
+ }
+
+ if err := balanceEcRacks(ctx, commandEnv, racks, *applyBalancing); err != nil {
+ return fmt.Errorf("balance ec racks: %v", err)
+ }
+
+ return nil
+}
+
+func collectRacks(allEcNodes []*EcNode) map[RackId]*EcRack {
+ // collect racks info
+ racks := make(map[RackId]*EcRack)
+ for _, ecNode := range allEcNodes {
+ if racks[ecNode.rack] == nil {
+ racks[ecNode.rack] = &EcRack{
+ ecNodes: make(map[EcNodeId]*EcNode),
+ }
+ }
+ racks[ecNode.rack].ecNodes[EcNodeId(ecNode.info.Id)] = ecNode
+ racks[ecNode.rack].freeEcSlot += ecNode.freeEcSlot
+ }
+ return racks
+}
+
+func balanceEcVolumes(commandEnv *CommandEnv, collection string, allEcNodes []*EcNode, racks map[RackId]*EcRack, applyBalancing bool) error {
+
+ ctx := context.Background()
+
+ fmt.Printf("balanceEcVolumes %s\n", collection)
+
+ if err := deleteDuplicatedEcShards(ctx, commandEnv, allEcNodes, collection, applyBalancing); err != nil {
+ return fmt.Errorf("delete duplicated collection %s ec shards: %v", collection, err)
+ }
+
+ if err := balanceEcShardsAcrossRacks(ctx, commandEnv, allEcNodes, racks, collection, applyBalancing); err != nil {
+ return fmt.Errorf("balance across racks collection %s ec shards: %v", collection, err)
+ }
+
+ if err := balanceEcShardsWithinRacks(ctx, commandEnv, allEcNodes, racks, collection, applyBalancing); err != nil {
+ return fmt.Errorf("balance across racks collection %s ec shards: %v", collection, err)
+ }
+
+ return nil
+}
+
+func deleteDuplicatedEcShards(ctx context.Context, commandEnv *CommandEnv, allEcNodes []*EcNode, collection string, applyBalancing bool) error {
+ // vid => []ecNode
+ vidLocations := collectVolumeIdToEcNodes(allEcNodes)
+ // deduplicate ec shards
+ for vid, locations := range vidLocations {
+ if err := doDeduplicateEcShards(ctx, commandEnv, collection, vid, locations, applyBalancing); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func doDeduplicateEcShards(ctx context.Context, commandEnv *CommandEnv, collection string, vid needle.VolumeId, locations []*EcNode, applyBalancing bool) error {
+
+ // check whether this volume has ecNodes that are over average
+ shardToLocations := make([][]*EcNode, erasure_coding.TotalShardsCount)
+ for _, ecNode := range locations {
+ shardBits := findEcVolumeShards(ecNode, vid)
+ for _, shardId := range shardBits.ShardIds() {
+ shardToLocations[shardId] = append(shardToLocations[shardId], ecNode)
+ }
+ }
+ for shardId, ecNodes := range shardToLocations {
+ if len(ecNodes) <= 1 {
+ continue
+ }
+ sortEcNodes(ecNodes)
+ fmt.Printf("ec shard %d.%d has %d copies, keeping %v\n", vid, shardId, len(ecNodes), ecNodes[0].info.Id)
+ if !applyBalancing {
+ continue
+ }
+
+ duplicatedShardIds := []uint32{uint32(shardId)}
+ for _, ecNode := range ecNodes[1:] {
+ if err := unmountEcShards(ctx, commandEnv.option.GrpcDialOption, vid, ecNode.info.Id, duplicatedShardIds); err != nil {
+ return err
+ }
+ if err := sourceServerDeleteEcShards(ctx, commandEnv.option.GrpcDialOption, collection, vid, ecNode.info.Id, duplicatedShardIds); err != nil {
+ return err
+ }
+ ecNode.deleteEcVolumeShards(vid, duplicatedShardIds)
+ }
+ }
+ return nil
+}
+
+func balanceEcShardsAcrossRacks(ctx context.Context, commandEnv *CommandEnv, allEcNodes []*EcNode, racks map[RackId]*EcRack, collection string, applyBalancing bool) error {
+ // collect vid => []ecNode, since previous steps can change the locations
+ vidLocations := collectVolumeIdToEcNodes(allEcNodes)
+ // spread the ec shards evenly
+ for vid, locations := range vidLocations {
+ if err := doBalanceEcShardsAcrossRacks(ctx, commandEnv, collection, vid, locations, racks, applyBalancing); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func doBalanceEcShardsAcrossRacks(ctx context.Context, commandEnv *CommandEnv, collection string, vid needle.VolumeId, locations []*EcNode, racks map[RackId]*EcRack, applyBalancing bool) error {
+
+ // calculate average number of shards an ec rack should have for one volume
+ averageShardsPerEcRack := ceilDivide(erasure_coding.TotalShardsCount, len(racks))
+
+ // see the volume's shards are in how many racks, and how many in each rack
+ rackToShardCount := groupByCount(locations, func(ecNode *EcNode) (id string, count int) {
+ shardBits := findEcVolumeShards(ecNode, vid)
+ return string(ecNode.rack), shardBits.ShardIdCount()
+ })
+ rackEcNodesWithVid := groupBy(locations, func(ecNode *EcNode) string {
+ return string(ecNode.rack)
+ })
+
+ // ecShardsToMove = select overflown ec shards from racks with ec shard counts > averageShardsPerEcRack
+ ecShardsToMove := make(map[erasure_coding.ShardId]*EcNode)
+ for rackId, count := range rackToShardCount {
+ if count > averageShardsPerEcRack {
+ possibleEcNodes := rackEcNodesWithVid[rackId]
+ for shardId, ecNode := range pickNEcShardsToMoveFrom(possibleEcNodes, vid, count-averageShardsPerEcRack) {
+ ecShardsToMove[shardId] = ecNode
+ }
+ }
+ }
+
+ for shardId, ecNode := range ecShardsToMove {
+ rackId := pickOneRack(racks, rackToShardCount, averageShardsPerEcRack)
+ var possibleDestinationEcNodes []*EcNode
+ for _, n := range racks[rackId].ecNodes {
+ possibleDestinationEcNodes = append(possibleDestinationEcNodes, n)
+ }
+ err := pickOneEcNodeAndMoveOneShard(ctx, commandEnv, averageShardsPerEcRack, ecNode, collection, vid, shardId, possibleDestinationEcNodes, applyBalancing)
+ if err != nil {
+ return err
+ }
+ rackToShardCount[string(rackId)] += 1
+ rackToShardCount[string(ecNode.rack)] -= 1
+ racks[rackId].freeEcSlot -= 1
+ racks[ecNode.rack].freeEcSlot += 1
+ }
+
+ return nil
+}
+
+func pickOneRack(rackToEcNodes map[RackId]*EcRack, rackToShardCount map[string]int, averageShardsPerEcRack int) RackId {
+
+ // TODO later may need to add some randomness
+
+ for rackId, rack := range rackToEcNodes {
+ if rackToShardCount[string(rackId)] >= averageShardsPerEcRack {
+ continue
+ }
+
+ if rack.freeEcSlot <= 0 {
+ continue
+ }
+
+ return rackId
+ }
+
+ return ""
+}
+
+func balanceEcShardsWithinRacks(ctx context.Context, commandEnv *CommandEnv, allEcNodes []*EcNode, racks map[RackId]*EcRack, collection string, applyBalancing bool) error {
+ // collect vid => []ecNode, since previous steps can change the locations
+ vidLocations := collectVolumeIdToEcNodes(allEcNodes)
+
+ // spread the ec shards evenly
+ for vid, locations := range vidLocations {
+
+ // see the volume's shards are in how many racks, and how many in each rack
+ rackToShardCount := groupByCount(locations, func(ecNode *EcNode) (id string, count int) {
+ shardBits := findEcVolumeShards(ecNode, vid)
+ return string(ecNode.rack), shardBits.ShardIdCount()
+ })
+ rackEcNodesWithVid := groupBy(locations, func(ecNode *EcNode) string {
+ return string(ecNode.rack)
+ })
+
+ for rackId, _ := range rackToShardCount {
+
+ var possibleDestinationEcNodes []*EcNode
+ for _, n := range racks[RackId(rackId)].ecNodes {
+ possibleDestinationEcNodes = append(possibleDestinationEcNodes, n)
+ }
+ sourceEcNodes := rackEcNodesWithVid[rackId]
+ averageShardsPerEcNode := ceilDivide(rackToShardCount[rackId], len(possibleDestinationEcNodes))
+ if err := doBalanceEcShardsWithinOneRack(ctx, commandEnv, averageShardsPerEcNode, collection, vid, sourceEcNodes, possibleDestinationEcNodes, applyBalancing); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func doBalanceEcShardsWithinOneRack(ctx context.Context, commandEnv *CommandEnv, averageShardsPerEcNode int, collection string, vid needle.VolumeId, existingLocations, possibleDestinationEcNodes []*EcNode, applyBalancing bool) error {
+
+ for _, ecNode := range existingLocations {
+
+ shardBits := findEcVolumeShards(ecNode, vid)
+ overLimitCount := shardBits.ShardIdCount() - averageShardsPerEcNode
+
+ for _, shardId := range shardBits.ShardIds() {
+
+ if overLimitCount <= 0 {
+ break
+ }
+
+ fmt.Printf("%s has %d overlimit, moving ec shard %d.%d\n", ecNode.info.Id, overLimitCount, vid, shardId)
+
+ err := pickOneEcNodeAndMoveOneShard(ctx, commandEnv, averageShardsPerEcNode, ecNode, collection, vid, shardId, possibleDestinationEcNodes, applyBalancing)
+ if err != nil {
+ return err
+ }
+
+ overLimitCount--
+ }
+ }
+
+ return nil
+}
+
+func balanceEcRacks(ctx context.Context, commandEnv *CommandEnv, racks map[RackId]*EcRack, applyBalancing bool) error {
+
+ // balance one rack for all ec shards
+ for _, ecRack := range racks {
+ if err := doBalanceEcRack(ctx, commandEnv, ecRack, applyBalancing); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func doBalanceEcRack(ctx context.Context, commandEnv *CommandEnv, ecRack *EcRack, applyBalancing bool) error {
+
+ if len(ecRack.ecNodes) <= 1 {
+ return nil
+ }
+
+ var rackEcNodes []*EcNode
+ for _, node := range ecRack.ecNodes {
+ rackEcNodes = append(rackEcNodes, node)
+ }
+
+ ecNodeIdToShardCount := groupByCount(rackEcNodes, func(node *EcNode) (id string, count int) {
+ for _, ecShardInfo := range node.info.EcShardInfos {
+ count += erasure_coding.ShardBits(ecShardInfo.EcIndexBits).ShardIdCount()
+ }
+ return node.info.Id, count
+ })
+
+ var totalShardCount int
+ for _, count := range ecNodeIdToShardCount {
+ totalShardCount += count
+ }
+
+ averageShardCount := ceilDivide(totalShardCount, len(rackEcNodes))
+
+ hasMove := true
+ for hasMove {
+ hasMove = false
+ sort.Slice(rackEcNodes, func(i, j int) bool {
+ return rackEcNodes[i].freeEcSlot > rackEcNodes[j].freeEcSlot
+ })
+ emptyNode, fullNode := rackEcNodes[0], rackEcNodes[len(rackEcNodes)-1]
+ emptyNodeShardCount, fullNodeShardCount := ecNodeIdToShardCount[emptyNode.info.Id], ecNodeIdToShardCount[fullNode.info.Id]
+ if fullNodeShardCount > averageShardCount && emptyNodeShardCount+1 <= averageShardCount {
+
+ emptyNodeIds := make(map[uint32]bool)
+ for _, shards := range emptyNode.info.EcShardInfos {
+ emptyNodeIds[shards.Id] = true
+ }
+ for _, shards := range fullNode.info.EcShardInfos {
+ if _, found := emptyNodeIds[shards.Id]; !found {
+ for _, shardId := range erasure_coding.ShardBits(shards.EcIndexBits).ShardIds() {
+
+ fmt.Printf("%s moves ec shards %d.%d to %s\n", fullNode.info.Id, shards.Id, shardId, emptyNode.info.Id)
+
+ err := moveMountedShardToEcNode(ctx, commandEnv, fullNode, shards.Collection, needle.VolumeId(shards.Id), shardId, emptyNode, applyBalancing)
+ if err != nil {
+ return err
+ }
+
+ ecNodeIdToShardCount[emptyNode.info.Id]++
+ ecNodeIdToShardCount[fullNode.info.Id]--
+ hasMove = true
+ break
+ }
+ break
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func pickOneEcNodeAndMoveOneShard(ctx context.Context, commandEnv *CommandEnv, expectedTotalEcShards int, existingLocation *EcNode, collection string, vid needle.VolumeId, shardId erasure_coding.ShardId, possibleDestinationEcNodes []*EcNode, applyBalancing bool) error {
+
+ sortEcNodes(possibleDestinationEcNodes)
+ averageShardsPerEcNode := ceilDivide(expectedTotalEcShards, len(possibleDestinationEcNodes))
+
+ for _, destEcNode := range possibleDestinationEcNodes {
+ if destEcNode.info.Id == existingLocation.info.Id {
+ continue
+ }
+
+ if destEcNode.freeEcSlot <= 0 {
+ continue
+ }
+ if findEcVolumeShards(destEcNode, vid).ShardIdCount() >= averageShardsPerEcNode {
+ continue
+ }
+
+ fmt.Printf("%s moves ec shard %d.%d to %s\n", existingLocation.info.Id, vid, shardId, destEcNode.info.Id)
+
+ err := moveMountedShardToEcNode(ctx, commandEnv, existingLocation, collection, vid, shardId, destEcNode, applyBalancing)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ return nil
+}
+
+func pickNEcShardsToMoveFrom(ecNodes []*EcNode, vid needle.VolumeId, n int) map[erasure_coding.ShardId]*EcNode {
+ picked := make(map[erasure_coding.ShardId]*EcNode)
+ var candidateEcNodes []*CandidateEcNode
+ for _, ecNode := range ecNodes {
+ shardBits := findEcVolumeShards(ecNode, vid)
+ if shardBits.ShardIdCount() > 0 {
+ candidateEcNodes = append(candidateEcNodes, &CandidateEcNode{
+ ecNode: ecNode,
+ shardCount: shardBits.ShardIdCount(),
+ })
+ }
+ }
+ sort.Slice(candidateEcNodes, func(i, j int) bool {
+ return candidateEcNodes[i].shardCount > candidateEcNodes[j].shardCount
+ })
+ for i := 0; i < n; i++ {
+ selectedEcNodeIndex := -1
+ for i, candidateEcNode := range candidateEcNodes {
+ shardBits := findEcVolumeShards(candidateEcNode.ecNode, vid)
+ if shardBits > 0 {
+ selectedEcNodeIndex = i
+ for _, shardId := range shardBits.ShardIds() {
+ candidateEcNode.shardCount--
+ picked[shardId] = candidateEcNode.ecNode
+ candidateEcNode.ecNode.deleteEcVolumeShards(vid, []uint32{uint32(shardId)})
+ break
+ }
+ break
+ }
+ }
+ if selectedEcNodeIndex >= 0 {
+ ensureSortedEcNodes(candidateEcNodes, selectedEcNodeIndex, func(i, j int) bool {
+ return candidateEcNodes[i].shardCount > candidateEcNodes[j].shardCount
+ })
+ }
+
+ }
+ return picked
+}
+
+func collectVolumeIdToEcNodes(allEcNodes []*EcNode) map[needle.VolumeId][]*EcNode {
+ vidLocations := make(map[needle.VolumeId][]*EcNode)
+ for _, ecNode := range allEcNodes {
+ for _, shardInfo := range ecNode.info.EcShardInfos {
+ vidLocations[needle.VolumeId(shardInfo.Id)] = append(vidLocations[needle.VolumeId(shardInfo.Id)], ecNode)
+ }
+ }
+ return vidLocations
+}
diff --git a/weed/shell/command_ec_common.go b/weed/shell/command_ec_common.go
new file mode 100644
index 000000000..d0fe16a68
--- /dev/null
+++ b/weed/shell/command_ec_common.go
@@ -0,0 +1,336 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "sort"
+
+ "github.com/chrislusf/seaweedfs/weed/glog"
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/erasure_coding"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+ "google.golang.org/grpc"
+)
+
+func moveMountedShardToEcNode(ctx context.Context, commandEnv *CommandEnv, existingLocation *EcNode, collection string, vid needle.VolumeId, shardId erasure_coding.ShardId, destinationEcNode *EcNode, applyBalancing bool) (err error) {
+
+ copiedShardIds := []uint32{uint32(shardId)}
+
+ if applyBalancing {
+
+ // ask destination node to copy shard and the ecx file from source node, and mount it
+ copiedShardIds, err = oneServerCopyAndMountEcShardsFromSource(ctx, commandEnv.option.GrpcDialOption, destinationEcNode, uint32(shardId), 1, vid, collection, existingLocation.info.Id)
+ if err != nil {
+ return err
+ }
+
+ // unmount the to be deleted shards
+ err = unmountEcShards(ctx, commandEnv.option.GrpcDialOption, vid, existingLocation.info.Id, copiedShardIds)
+ if err != nil {
+ return err
+ }
+
+ // ask source node to delete the shard, and maybe the ecx file
+ err = sourceServerDeleteEcShards(ctx, commandEnv.option.GrpcDialOption, collection, vid, existingLocation.info.Id, copiedShardIds)
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("moved ec shard %d.%d %s => %s\n", vid, shardId, existingLocation.info.Id, destinationEcNode.info.Id)
+
+ }
+
+ destinationEcNode.addEcVolumeShards(vid, collection, copiedShardIds)
+ existingLocation.deleteEcVolumeShards(vid, copiedShardIds)
+
+ return nil
+
+}
+
+func oneServerCopyAndMountEcShardsFromSource(ctx context.Context, grpcDialOption grpc.DialOption,
+ targetServer *EcNode, startFromShardId uint32, shardCount int,
+ volumeId needle.VolumeId, collection string, existingLocation string) (copiedShardIds []uint32, err error) {
+
+ var shardIdsToCopy []uint32
+ for shardId := startFromShardId; shardId < startFromShardId+uint32(shardCount); shardId++ {
+ shardIdsToCopy = append(shardIdsToCopy, shardId)
+ }
+ fmt.Printf("allocate %d.%v %s => %s\n", volumeId, shardIdsToCopy, existingLocation, targetServer.info.Id)
+
+ err = operation.WithVolumeServerClient(targetServer.info.Id, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+
+ if targetServer.info.Id != existingLocation {
+
+ fmt.Printf("copy %d.%v %s => %s\n", volumeId, shardIdsToCopy, existingLocation, targetServer.info.Id)
+ _, copyErr := volumeServerClient.VolumeEcShardsCopy(ctx, &volume_server_pb.VolumeEcShardsCopyRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ ShardIds: shardIdsToCopy,
+ CopyEcxFile: true,
+ SourceDataNode: existingLocation,
+ })
+ if copyErr != nil {
+ return fmt.Errorf("copy %d.%v %s => %s : %v\n", volumeId, shardIdsToCopy, existingLocation, targetServer.info.Id, copyErr)
+ }
+ }
+
+ fmt.Printf("mount %d.%v on %s\n", volumeId, shardIdsToCopy, targetServer.info.Id)
+ _, mountErr := volumeServerClient.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ ShardIds: shardIdsToCopy,
+ })
+ if mountErr != nil {
+ return fmt.Errorf("mount %d.%v on %s : %v\n", volumeId, shardIdsToCopy, targetServer.info.Id, mountErr)
+ }
+
+ if targetServer.info.Id != existingLocation {
+ copiedShardIds = shardIdsToCopy
+ glog.V(0).Infof("%s ec volume %d deletes shards %+v", existingLocation, volumeId, copiedShardIds)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+func eachDataNode(topo *master_pb.TopologyInfo, fn func(dc string, rack RackId, dn *master_pb.DataNodeInfo)) {
+ for _, dc := range topo.DataCenterInfos {
+ for _, rack := range dc.RackInfos {
+ for _, dn := range rack.DataNodeInfos {
+ fn(dc.Id, RackId(rack.Id), dn)
+ }
+ }
+ }
+}
+
+func sortEcNodes(ecNodes []*EcNode) {
+ sort.Slice(ecNodes, func(i, j int) bool {
+ return ecNodes[i].freeEcSlot > ecNodes[j].freeEcSlot
+ })
+}
+
+type CandidateEcNode struct {
+ ecNode *EcNode
+ shardCount int
+}
+
+// if the index node changed the freeEcSlot, need to keep every EcNode still sorted
+func ensureSortedEcNodes(data []*CandidateEcNode, index int, lessThan func(i, j int) bool) {
+ for i := index - 1; i >= 0; i-- {
+ if lessThan(i+1, i) {
+ swap(data, i, i+1)
+ } else {
+ break
+ }
+ }
+ for i := index + 1; i < len(data); i++ {
+ if lessThan(i, i-1) {
+ swap(data, i, i-1)
+ } else {
+ break
+ }
+ }
+}
+
+func swap(data []*CandidateEcNode, i, j int) {
+ t := data[i]
+ data[i] = data[j]
+ data[j] = t
+}
+
+func countShards(ecShardInfos []*master_pb.VolumeEcShardInformationMessage) (count int) {
+ for _, ecShardInfo := range ecShardInfos {
+ shardBits := erasure_coding.ShardBits(ecShardInfo.EcIndexBits)
+ count += shardBits.ShardIdCount()
+ }
+ return
+}
+
+func countFreeShardSlots(dn *master_pb.DataNodeInfo) (count int) {
+ return int(dn.FreeVolumeCount)*10 - countShards(dn.EcShardInfos)
+}
+
+type RackId string
+type EcNodeId string
+
+type EcNode struct {
+ info *master_pb.DataNodeInfo
+ dc string
+ rack RackId
+ freeEcSlot int
+}
+
+type EcRack struct {
+ ecNodes map[EcNodeId]*EcNode
+ freeEcSlot int
+}
+
+func collectEcNodes(ctx context.Context, commandEnv *CommandEnv, selectedDataCenter string) (ecNodes []*EcNode, totalFreeEcSlots int, err error) {
+
+ // list all possible locations
+ var resp *master_pb.VolumeListResponse
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ resp, err = client.VolumeList(ctx, &master_pb.VolumeListRequest{})
+ return err
+ })
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // find out all volume servers with one slot left.
+ eachDataNode(resp.TopologyInfo, func(dc string, rack RackId, dn *master_pb.DataNodeInfo) {
+ if selectedDataCenter != "" && selectedDataCenter != dc {
+ return
+ }
+ if freeEcSlots := countFreeShardSlots(dn); freeEcSlots > 0 {
+ ecNodes = append(ecNodes, &EcNode{
+ info: dn,
+ dc: dc,
+ rack: rack,
+ freeEcSlot: int(freeEcSlots),
+ })
+ totalFreeEcSlots += freeEcSlots
+ }
+ })
+
+ sortEcNodes(ecNodes)
+
+ return
+}
+
+func sourceServerDeleteEcShards(ctx context.Context, grpcDialOption grpc.DialOption,
+ collection string, volumeId needle.VolumeId, sourceLocation string, toBeDeletedShardIds []uint32) error {
+
+ fmt.Printf("delete %d.%v from %s\n", volumeId, toBeDeletedShardIds, sourceLocation)
+
+ return operation.WithVolumeServerClient(sourceLocation, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, deleteErr := volumeServerClient.VolumeEcShardsDelete(ctx, &volume_server_pb.VolumeEcShardsDeleteRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ ShardIds: toBeDeletedShardIds,
+ })
+ return deleteErr
+ })
+
+}
+
+func unmountEcShards(ctx context.Context, grpcDialOption grpc.DialOption,
+ volumeId needle.VolumeId, sourceLocation string, toBeUnmountedhardIds []uint32) error {
+
+ fmt.Printf("unmount %d.%v from %s\n", volumeId, toBeUnmountedhardIds, sourceLocation)
+
+ return operation.WithVolumeServerClient(sourceLocation, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, deleteErr := volumeServerClient.VolumeEcShardsUnmount(ctx, &volume_server_pb.VolumeEcShardsUnmountRequest{
+ VolumeId: uint32(volumeId),
+ ShardIds: toBeUnmountedhardIds,
+ })
+ return deleteErr
+ })
+}
+
+func mountEcShards(ctx context.Context, grpcDialOption grpc.DialOption,
+ collection string, volumeId needle.VolumeId, sourceLocation string, toBeMountedhardIds []uint32) error {
+
+ fmt.Printf("mount %d.%v on %s\n", volumeId, toBeMountedhardIds, sourceLocation)
+
+ return operation.WithVolumeServerClient(sourceLocation, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, mountErr := volumeServerClient.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ ShardIds: toBeMountedhardIds,
+ })
+ return mountErr
+ })
+}
+
+func ceilDivide(total, n int) int {
+ return int(math.Ceil(float64(total) / float64(n)))
+}
+
+func findEcVolumeShards(ecNode *EcNode, vid needle.VolumeId) erasure_coding.ShardBits {
+
+ for _, shardInfo := range ecNode.info.EcShardInfos {
+ if needle.VolumeId(shardInfo.Id) == vid {
+ return erasure_coding.ShardBits(shardInfo.EcIndexBits)
+ }
+ }
+
+ return 0
+}
+
+func (ecNode *EcNode) addEcVolumeShards(vid needle.VolumeId, collection string, shardIds []uint32) *EcNode {
+
+ foundVolume := false
+ for _, shardInfo := range ecNode.info.EcShardInfos {
+ if needle.VolumeId(shardInfo.Id) == vid {
+ oldShardBits := erasure_coding.ShardBits(shardInfo.EcIndexBits)
+ newShardBits := oldShardBits
+ for _, shardId := range shardIds {
+ newShardBits = newShardBits.AddShardId(erasure_coding.ShardId(shardId))
+ }
+ shardInfo.EcIndexBits = uint32(newShardBits)
+ ecNode.freeEcSlot -= newShardBits.ShardIdCount() - oldShardBits.ShardIdCount()
+ foundVolume = true
+ break
+ }
+ }
+
+ if !foundVolume {
+ var newShardBits erasure_coding.ShardBits
+ for _, shardId := range shardIds {
+ newShardBits = newShardBits.AddShardId(erasure_coding.ShardId(shardId))
+ }
+ ecNode.info.EcShardInfos = append(ecNode.info.EcShardInfos, &master_pb.VolumeEcShardInformationMessage{
+ Id: uint32(vid),
+ Collection: collection,
+ EcIndexBits: uint32(newShardBits),
+ })
+ ecNode.freeEcSlot -= len(shardIds)
+ }
+
+ return ecNode
+}
+
+func (ecNode *EcNode) deleteEcVolumeShards(vid needle.VolumeId, shardIds []uint32) *EcNode {
+
+ for _, shardInfo := range ecNode.info.EcShardInfos {
+ if needle.VolumeId(shardInfo.Id) == vid {
+ oldShardBits := erasure_coding.ShardBits(shardInfo.EcIndexBits)
+ newShardBits := oldShardBits
+ for _, shardId := range shardIds {
+ newShardBits = newShardBits.RemoveShardId(erasure_coding.ShardId(shardId))
+ }
+ shardInfo.EcIndexBits = uint32(newShardBits)
+ ecNode.freeEcSlot -= newShardBits.ShardIdCount() - oldShardBits.ShardIdCount()
+ }
+ }
+
+ return ecNode
+}
+
+func groupByCount(data []*EcNode, identifierFn func(*EcNode) (id string, count int)) map[string]int {
+ countMap := make(map[string]int)
+ for _, d := range data {
+ id, count := identifierFn(d)
+ countMap[id] += count
+ }
+ return countMap
+}
+
+func groupBy(data []*EcNode, identifierFn func(*EcNode) (id string)) map[string][]*EcNode {
+ groupMap := make(map[string][]*EcNode)
+ for _, d := range data {
+ id := identifierFn(d)
+ groupMap[id] = append(groupMap[id], d)
+ }
+ return groupMap
+}
diff --git a/weed/shell/command_ec_encode.go b/weed/shell/command_ec_encode.go
new file mode 100644
index 000000000..8ad0d51c8
--- /dev/null
+++ b/weed/shell/command_ec_encode.go
@@ -0,0 +1,289 @@
+package shell
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/erasure_coding"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+ "github.com/chrislusf/seaweedfs/weed/wdclient"
+ "google.golang.org/grpc"
+)
+
+func init() {
+ Commands = append(Commands, &commandEcEncode{})
+}
+
+type commandEcEncode struct {
+}
+
+func (c *commandEcEncode) Name() string {
+ return "ec.encode"
+}
+
+func (c *commandEcEncode) Help() string {
+ return `apply erasure coding to a volume
+
+ ec.encode [-collection=""] [-fullPercent=95] [-quietFor=1h]
+ ec.encode [-collection=""] [-volumeId=<volume_id>]
+
+ This command will:
+ 1. freeze one volume
+ 2. apply erasure coding to the volume
+ 3. move the encoded shards to multiple volume servers
+
+ The erasure coding is 10.4. So ideally you have more than 14 volume servers, and you can afford
+ to lose 4 volume servers.
+
+ If the number of volumes are not high, the worst case is that you only have 4 volume servers,
+ and the shards are spread as 4,4,3,3, respectively. You can afford to lose one volume server.
+
+ If you only have less than 4 volume servers, with erasure coding, at least you can afford to
+ have 4 corrupted shard files.
+
+`
+}
+
+func (c *commandEcEncode) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ encodeCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+ volumeId := encodeCommand.Int("volumeId", 0, "the volume id")
+ collection := encodeCommand.String("collection", "", "the collection name")
+ fullPercentage := encodeCommand.Float64("fullPercent", 95, "the volume reaches the percentage of max volume size")
+ quietPeriod := encodeCommand.Duration("quietFor", time.Hour, "select volumes without no writes for this period")
+ if err = encodeCommand.Parse(args); err != nil {
+ return nil
+ }
+
+ ctx := context.Background()
+ vid := needle.VolumeId(*volumeId)
+
+ // volumeId is provided
+ if vid != 0 {
+ return doEcEncode(ctx, commandEnv, *collection, vid)
+ }
+
+ // apply to all volumes in the collection
+ volumeIds, err := collectVolumeIdsForEcEncode(ctx, commandEnv, *collection, *fullPercentage, *quietPeriod)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("ec encode volumes: %v\n", volumeIds)
+ for _, vid := range volumeIds {
+ if err = doEcEncode(ctx, commandEnv, *collection, vid); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func doEcEncode(ctx context.Context, commandEnv *CommandEnv, collection string, vid needle.VolumeId) (err error) {
+ // find volume location
+ locations := commandEnv.MasterClient.GetLocations(uint32(vid))
+ if len(locations) == 0 {
+ return fmt.Errorf("volume %d not found", vid)
+ }
+
+ // mark the volume as readonly
+ err = markVolumeReadonly(ctx, commandEnv.option.GrpcDialOption, needle.VolumeId(vid), locations)
+ if err != nil {
+ return fmt.Errorf("generate ec shards for volume %d on %s: %v", vid, locations[0].Url, err)
+ }
+
+ // generate ec shards
+ err = generateEcShards(ctx, commandEnv.option.GrpcDialOption, needle.VolumeId(vid), collection, locations[0].Url)
+ if err != nil {
+ return fmt.Errorf("generate ec shards for volume %d on %s: %v", vid, locations[0].Url, err)
+ }
+
+ // balance the ec shards to current cluster
+ err = spreadEcShards(ctx, commandEnv, vid, collection, locations)
+ if err != nil {
+ return fmt.Errorf("spread ec shards for volume %d from %s: %v", vid, locations[0].Url, err)
+ }
+
+ return nil
+}
+
+func markVolumeReadonly(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, locations []wdclient.Location) error {
+
+ for _, location := range locations {
+
+ err := operation.WithVolumeServerClient(location.Url, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, markErr := volumeServerClient.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
+ VolumeId: uint32(volumeId),
+ })
+ return markErr
+ })
+
+ if err != nil {
+ return err
+ }
+
+ }
+
+ return nil
+}
+
+func generateEcShards(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, collection string, sourceVolumeServer string) error {
+
+ err := operation.WithVolumeServerClient(sourceVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, genErr := volumeServerClient.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ })
+ return genErr
+ })
+
+ return err
+
+}
+
+func spreadEcShards(ctx context.Context, commandEnv *CommandEnv, volumeId needle.VolumeId, collection string, existingLocations []wdclient.Location) (err error) {
+
+ allEcNodes, totalFreeEcSlots, err := collectEcNodes(ctx, commandEnv, "")
+ if err != nil {
+ return err
+ }
+
+ if totalFreeEcSlots < erasure_coding.TotalShardsCount {
+ return fmt.Errorf("not enough free ec shard slots. only %d left", totalFreeEcSlots)
+ }
+ allocatedDataNodes := allEcNodes
+ if len(allocatedDataNodes) > erasure_coding.TotalShardsCount {
+ allocatedDataNodes = allocatedDataNodes[:erasure_coding.TotalShardsCount]
+ }
+
+ // calculate how many shards to allocate for these servers
+ allocated := balancedEcDistribution(allocatedDataNodes)
+
+ // ask the data nodes to copy from the source volume server
+ copiedShardIds, err := parallelCopyEcShardsFromSource(ctx, commandEnv.option.GrpcDialOption, allocatedDataNodes, allocated, volumeId, collection, existingLocations[0])
+ if err != nil {
+ return err
+ }
+
+ // unmount the to be deleted shards
+ err = unmountEcShards(ctx, commandEnv.option.GrpcDialOption, volumeId, existingLocations[0].Url, copiedShardIds)
+ if err != nil {
+ return err
+ }
+
+ // ask the source volume server to clean up copied ec shards
+ err = sourceServerDeleteEcShards(ctx, commandEnv.option.GrpcDialOption, collection, volumeId, existingLocations[0].Url, copiedShardIds)
+ if err != nil {
+ return fmt.Errorf("source delete copied ecShards %s %d.%v: %v", existingLocations[0].Url, volumeId, copiedShardIds, err)
+ }
+
+ // ask the source volume server to delete the original volume
+ for _, location := range existingLocations {
+ err = deleteVolume(ctx, commandEnv.option.GrpcDialOption, volumeId, location.Url)
+ if err != nil {
+ return fmt.Errorf("deleteVolume %s volume %d: %v", location.Url, volumeId, err)
+ }
+ }
+
+ return err
+
+}
+
+func parallelCopyEcShardsFromSource(ctx context.Context, grpcDialOption grpc.DialOption,
+ targetServers []*EcNode, allocated []int,
+ volumeId needle.VolumeId, collection string, existingLocation wdclient.Location) (actuallyCopied []uint32, err error) {
+
+ // parallelize
+ shardIdChan := make(chan []uint32, len(targetServers))
+ var wg sync.WaitGroup
+ startFromShardId := uint32(0)
+ for i, server := range targetServers {
+ if allocated[i] <= 0 {
+ continue
+ }
+
+ wg.Add(1)
+ go func(server *EcNode, startFromShardId uint32, shardCount int) {
+ defer wg.Done()
+ copiedShardIds, copyErr := oneServerCopyAndMountEcShardsFromSource(ctx, grpcDialOption, server,
+ startFromShardId, shardCount, volumeId, collection, existingLocation.Url)
+ if copyErr != nil {
+ err = copyErr
+ } else {
+ shardIdChan <- copiedShardIds
+ server.addEcVolumeShards(volumeId, collection, copiedShardIds)
+ }
+ }(server, startFromShardId, allocated[i])
+ startFromShardId += uint32(allocated[i])
+ }
+ wg.Wait()
+ close(shardIdChan)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for shardIds := range shardIdChan {
+ actuallyCopied = append(actuallyCopied, shardIds...)
+ }
+
+ return
+}
+
+func balancedEcDistribution(servers []*EcNode) (allocated []int) {
+ allocated = make([]int, len(servers))
+ allocatedCount := 0
+ for allocatedCount < erasure_coding.TotalShardsCount {
+ for i, server := range servers {
+ if server.freeEcSlot-allocated[i] > 0 {
+ allocated[i] += 1
+ allocatedCount += 1
+ }
+ if allocatedCount >= erasure_coding.TotalShardsCount {
+ break
+ }
+ }
+ }
+
+ return allocated
+}
+
+func collectVolumeIdsForEcEncode(ctx context.Context, commandEnv *CommandEnv, selectedCollection string, fullPercentage float64, quietPeriod time.Duration) (vids []needle.VolumeId, err error) {
+
+ var resp *master_pb.VolumeListResponse
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ resp, err = client.VolumeList(ctx, &master_pb.VolumeListRequest{})
+ return err
+ })
+ if err != nil {
+ return
+ }
+
+ quietSeconds := int64(quietPeriod / time.Second)
+ nowUnixSeconds := time.Now().Unix()
+
+ fmt.Printf("ec encode volumes quiet for: %d seconds\n", quietSeconds)
+
+ vidMap := make(map[uint32]bool)
+ eachDataNode(resp.TopologyInfo, func(dc string, rack RackId, dn *master_pb.DataNodeInfo) {
+ for _, v := range dn.VolumeInfos {
+ if v.Collection == selectedCollection && v.ModifiedAtSecond+quietSeconds < nowUnixSeconds {
+ if float64(v.Size) > fullPercentage/100*float64(resp.VolumeSizeLimitMb)*1024*1024 {
+ vidMap[v.Id] = true
+ }
+ }
+ }
+ })
+
+ for vid, _ := range vidMap {
+ vids = append(vids, needle.VolumeId(vid))
+ }
+
+ return
+}
diff --git a/weed/shell/command_ec_rebuild.go b/weed/shell/command_ec_rebuild.go
new file mode 100644
index 000000000..63b7c4088
--- /dev/null
+++ b/weed/shell/command_ec_rebuild.go
@@ -0,0 +1,268 @@
+package shell
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "io"
+
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/erasure_coding"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+ "google.golang.org/grpc"
+)
+
+func init() {
+ Commands = append(Commands, &commandEcRebuild{})
+}
+
+type commandEcRebuild struct {
+}
+
+func (c *commandEcRebuild) Name() string {
+ return "ec.rebuild"
+}
+
+func (c *commandEcRebuild) Help() string {
+ return `find and rebuild missing ec shards among volume servers
+
+ ec.rebuild [-c EACH_COLLECTION|<collection_name>] [-force]
+
+ Algorithm:
+
+ For each type of volume server (different max volume count limit){
+ for each collection {
+ rebuildEcVolumes()
+ }
+ }
+
+ func rebuildEcVolumes(){
+ idealWritableVolumes = totalWritableVolumes / numVolumeServers
+ for {
+ sort all volume servers ordered by the number of local writable volumes
+ pick the volume server A with the lowest number of writable volumes x
+ pick the volume server B with the highest number of writable volumes y
+ if y > idealWritableVolumes and x +1 <= idealWritableVolumes {
+ if B has a writable volume id v that A does not have {
+ move writable volume v from A to B
+ }
+ }
+ }
+ }
+
+`
+}
+
+func (c *commandEcRebuild) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ fixCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+ collection := fixCommand.String("collection", "EACH_COLLECTION", "collection name, or \"EACH_COLLECTION\" for each collection")
+ applyChanges := fixCommand.Bool("force", false, "apply the changes")
+ if err = fixCommand.Parse(args); err != nil {
+ return nil
+ }
+
+ // collect all ec nodes
+ allEcNodes, _, err := collectEcNodes(context.Background(), commandEnv, "")
+ if err != nil {
+ return err
+ }
+
+ if *collection == "EACH_COLLECTION" {
+ collections, err := ListCollectionNames(commandEnv, false, true)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("rebuildEcVolumes collections %+v\n", len(collections))
+ for _, c := range collections {
+ fmt.Printf("rebuildEcVolumes collection %+v\n", c)
+ if err = rebuildEcVolumes(commandEnv, allEcNodes, c, writer, *applyChanges); err != nil {
+ return err
+ }
+ }
+ } else {
+ if err = rebuildEcVolumes(commandEnv, allEcNodes, *collection, writer, *applyChanges); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func rebuildEcVolumes(commandEnv *CommandEnv, allEcNodes []*EcNode, collection string, writer io.Writer, applyChanges bool) error {
+
+ ctx := context.Background()
+
+ fmt.Printf("rebuildEcVolumes %s\n", collection)
+
+ // collect vid => each shard locations, similar to ecShardMap in topology.go
+ ecShardMap := make(EcShardMap)
+ for _, ecNode := range allEcNodes {
+ ecShardMap.registerEcNode(ecNode, collection)
+ }
+
+ for vid, locations := range ecShardMap {
+ shardCount := locations.shardCount()
+ if shardCount == erasure_coding.TotalShardsCount {
+ continue
+ }
+ if shardCount < erasure_coding.DataShardsCount {
+ return fmt.Errorf("ec volume %d is unrepairable with %d shards\n", vid, shardCount)
+ }
+
+ sortEcNodes(allEcNodes)
+
+ if allEcNodes[0].freeEcSlot < erasure_coding.TotalShardsCount {
+ return fmt.Errorf("disk space is not enough")
+ }
+
+ if err := rebuildOneEcVolume(ctx, commandEnv, allEcNodes[0], collection, vid, locations, writer, applyChanges); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func rebuildOneEcVolume(ctx context.Context, commandEnv *CommandEnv, rebuilder *EcNode, collection string, volumeId needle.VolumeId, locations EcShardLocations, writer io.Writer, applyChanges bool) error {
+
+ fmt.Printf("rebuildOneEcVolume %s %d\n", collection, volumeId)
+
+ // collect shard files to rebuilder local disk
+ var generatedShardIds []uint32
+ copiedShardIds, _, err := prepareDataToRecover(ctx, commandEnv, rebuilder, collection, volumeId, locations, writer, applyChanges)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ // clean up working files
+
+ // ask the rebuilder to delete the copied shards
+ err = sourceServerDeleteEcShards(ctx, commandEnv.option.GrpcDialOption, collection, volumeId, rebuilder.info.Id, copiedShardIds)
+ if err != nil {
+ fmt.Fprintf(writer, "%s delete copied ec shards %s %d.%v\n", rebuilder.info.Id, collection, volumeId, copiedShardIds)
+ }
+
+ }()
+
+ if !applyChanges {
+ return nil
+ }
+
+ // generate ec shards, and maybe ecx file
+ generatedShardIds, err = generateMissingShards(ctx, commandEnv.option.GrpcDialOption, collection, volumeId, rebuilder.info.Id)
+ if err != nil {
+ return err
+ }
+
+ // mount the generated shards
+ err = mountEcShards(ctx, commandEnv.option.GrpcDialOption, collection, volumeId, rebuilder.info.Id, generatedShardIds)
+ if err != nil {
+ return err
+ }
+
+ rebuilder.addEcVolumeShards(volumeId, collection, generatedShardIds)
+
+ return nil
+}
+
+func generateMissingShards(ctx context.Context, grpcDialOption grpc.DialOption,
+ collection string, volumeId needle.VolumeId, sourceLocation string) (rebuiltShardIds []uint32, err error) {
+
+ err = operation.WithVolumeServerClient(sourceLocation, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ resp, rebultErr := volumeServerClient.VolumeEcShardsRebuild(ctx, &volume_server_pb.VolumeEcShardsRebuildRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ })
+ if rebultErr == nil {
+ rebuiltShardIds = resp.RebuiltShardIds
+ }
+ return rebultErr
+ })
+ return
+}
+
+func prepareDataToRecover(ctx context.Context, commandEnv *CommandEnv, rebuilder *EcNode, collection string, volumeId needle.VolumeId, locations EcShardLocations, writer io.Writer, applyBalancing bool) (copiedShardIds []uint32, localShardIds []uint32, err error) {
+
+ needEcxFile := true
+ var localShardBits erasure_coding.ShardBits
+ for _, ecShardInfo := range rebuilder.info.EcShardInfos {
+ if ecShardInfo.Collection == collection && needle.VolumeId(ecShardInfo.Id) == volumeId {
+ needEcxFile = false
+ localShardBits = erasure_coding.ShardBits(ecShardInfo.EcIndexBits)
+ }
+ }
+
+ for shardId, ecNodes := range locations {
+
+ if len(ecNodes) == 0 {
+ fmt.Fprintf(writer, "missing shard %d.%d\n", volumeId, shardId)
+ continue
+ }
+
+ if localShardBits.HasShardId(erasure_coding.ShardId(shardId)) {
+ localShardIds = append(localShardIds, uint32(shardId))
+ fmt.Fprintf(writer, "use existing shard %d.%d\n", volumeId, shardId)
+ continue
+ }
+
+ var copyErr error
+ if applyBalancing {
+ copyErr = operation.WithVolumeServerClient(rebuilder.info.Id, commandEnv.option.GrpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, copyErr := volumeServerClient.VolumeEcShardsCopy(ctx, &volume_server_pb.VolumeEcShardsCopyRequest{
+ VolumeId: uint32(volumeId),
+ Collection: collection,
+ ShardIds: []uint32{uint32(shardId)},
+ CopyEcxFile: needEcxFile,
+ SourceDataNode: ecNodes[0].info.Id,
+ })
+ return copyErr
+ })
+ if copyErr == nil && needEcxFile {
+ needEcxFile = false
+ }
+ }
+ if copyErr != nil {
+ fmt.Fprintf(writer, "%s failed to copy %d.%d from %s: %v\n", rebuilder.info.Id, volumeId, shardId, ecNodes[0].info.Id, copyErr)
+ } else {
+ fmt.Fprintf(writer, "%s copied %d.%d from %s\n", rebuilder.info.Id, volumeId, shardId, ecNodes[0].info.Id)
+ copiedShardIds = append(copiedShardIds, uint32(shardId))
+ }
+
+ }
+
+ if len(copiedShardIds)+len(localShardIds) >= erasure_coding.DataShardsCount {
+ return copiedShardIds, localShardIds, nil
+ }
+
+ return nil, nil, fmt.Errorf("%d shards are not enough to recover volume %d", len(copiedShardIds)+len(localShardIds), volumeId)
+
+}
+
+type EcShardMap map[needle.VolumeId]EcShardLocations
+type EcShardLocations [][]*EcNode
+
+func (ecShardMap EcShardMap) registerEcNode(ecNode *EcNode, collection string) {
+ for _, shardInfo := range ecNode.info.EcShardInfos {
+ if shardInfo.Collection == collection {
+ existing, found := ecShardMap[needle.VolumeId(shardInfo.Id)]
+ if !found {
+ existing = make([][]*EcNode, erasure_coding.TotalShardsCount)
+ ecShardMap[needle.VolumeId(shardInfo.Id)] = existing
+ }
+ for _, shardId := range erasure_coding.ShardBits(shardInfo.EcIndexBits).ShardIds() {
+ existing[shardId] = append(existing[shardId], ecNode)
+ }
+ }
+ }
+}
+
+func (ecShardLocations EcShardLocations) shardCount() (count int) {
+ for _, locations := range ecShardLocations {
+ if len(locations) > 0 {
+ count++
+ }
+ }
+ return
+}
diff --git a/weed/shell/command_ec_test.go b/weed/shell/command_ec_test.go
new file mode 100644
index 000000000..9e578ed28
--- /dev/null
+++ b/weed/shell/command_ec_test.go
@@ -0,0 +1,127 @@
+package shell
+
+import (
+ "context"
+ "testing"
+
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+)
+
+func TestCommandEcBalanceSmall(t *testing.T) {
+
+ allEcNodes := []*EcNode{
+ newEcNode("dc1", "rack1", "dn1", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}),
+ newEcNode("dc1", "rack2", "dn2", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}),
+ }
+
+ racks := collectRacks(allEcNodes)
+ balanceEcVolumes(nil, "c1", allEcNodes, racks, false)
+}
+
+func TestCommandEcBalanceNothingToMove(t *testing.T) {
+
+ allEcNodes := []*EcNode{
+ newEcNode("dc1", "rack1", "dn1", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{0, 1, 2, 3, 4, 5, 6}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{7, 8, 9, 10, 11, 12, 13}),
+ newEcNode("dc1", "rack1", "dn2", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{7, 8, 9, 10, 11, 12, 13}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{0, 1, 2, 3, 4, 5, 6}),
+ }
+
+ racks := collectRacks(allEcNodes)
+ balanceEcVolumes(nil, "c1", allEcNodes, racks, false)
+}
+
+func TestCommandEcBalanceAddNewServers(t *testing.T) {
+
+ allEcNodes := []*EcNode{
+ newEcNode("dc1", "rack1", "dn1", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{0, 1, 2, 3, 4, 5, 6}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{7, 8, 9, 10, 11, 12, 13}),
+ newEcNode("dc1", "rack1", "dn2", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{7, 8, 9, 10, 11, 12, 13}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{0, 1, 2, 3, 4, 5, 6}),
+ newEcNode("dc1", "rack1", "dn3", 100),
+ newEcNode("dc1", "rack1", "dn4", 100),
+ }
+
+ racks := collectRacks(allEcNodes)
+ balanceEcVolumes(nil, "c1", allEcNodes, racks, false)
+}
+
+func TestCommandEcBalanceAddNewRacks(t *testing.T) {
+
+ allEcNodes := []*EcNode{
+ newEcNode("dc1", "rack1", "dn1", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{0, 1, 2, 3, 4, 5, 6}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{7, 8, 9, 10, 11, 12, 13}),
+ newEcNode("dc1", "rack1", "dn2", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{7, 8, 9, 10, 11, 12, 13}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{0, 1, 2, 3, 4, 5, 6}),
+ newEcNode("dc1", "rack2", "dn3", 100),
+ newEcNode("dc1", "rack2", "dn4", 100),
+ }
+
+ racks := collectRacks(allEcNodes)
+ balanceEcVolumes(nil, "c1", allEcNodes, racks, false)
+}
+
+func TestCommandEcBalanceVolumeEvenButRackUneven(t *testing.T) {
+
+ allEcNodes := []*EcNode{
+ newEcNode("dc1", "rack1", "dn_shared", 100).
+ addEcVolumeAndShardsForTest(1, "c1", []uint32{0}).
+ addEcVolumeAndShardsForTest(2, "c1", []uint32{0}),
+
+ newEcNode("dc1", "rack1", "dn_a1", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{1}),
+ newEcNode("dc1", "rack1", "dn_a2", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{2}),
+ newEcNode("dc1", "rack1", "dn_a3", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{3}),
+ newEcNode("dc1", "rack1", "dn_a4", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{4}),
+ newEcNode("dc1", "rack1", "dn_a5", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{5}),
+ newEcNode("dc1", "rack1", "dn_a6", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{6}),
+ newEcNode("dc1", "rack1", "dn_a7", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{7}),
+ newEcNode("dc1", "rack1", "dn_a8", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{8}),
+ newEcNode("dc1", "rack1", "dn_a9", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{9}),
+ newEcNode("dc1", "rack1", "dn_a10", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{10}),
+ newEcNode("dc1", "rack1", "dn_a11", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{11}),
+ newEcNode("dc1", "rack1", "dn_a12", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{12}),
+ newEcNode("dc1", "rack1", "dn_a13", 100).addEcVolumeAndShardsForTest(1, "c1", []uint32{13}),
+
+ newEcNode("dc1", "rack1", "dn_b1", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{1}),
+ newEcNode("dc1", "rack1", "dn_b2", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{2}),
+ newEcNode("dc1", "rack1", "dn_b3", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{3}),
+ newEcNode("dc1", "rack1", "dn_b4", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{4}),
+ newEcNode("dc1", "rack1", "dn_b5", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{5}),
+ newEcNode("dc1", "rack1", "dn_b6", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{6}),
+ newEcNode("dc1", "rack1", "dn_b7", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{7}),
+ newEcNode("dc1", "rack1", "dn_b8", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{8}),
+ newEcNode("dc1", "rack1", "dn_b9", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{9}),
+ newEcNode("dc1", "rack1", "dn_b10", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{10}),
+ newEcNode("dc1", "rack1", "dn_b11", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{11}),
+ newEcNode("dc1", "rack1", "dn_b12", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{12}),
+ newEcNode("dc1", "rack1", "dn_b13", 100).addEcVolumeAndShardsForTest(2, "c1", []uint32{13}),
+
+ newEcNode("dc1", "rack1", "dn3", 100),
+ }
+
+ racks := collectRacks(allEcNodes)
+ balanceEcVolumes(nil, "c1", allEcNodes, racks, false)
+ balanceEcRacks(context.Background(), nil, racks, false)
+}
+
+func newEcNode(dc string, rack string, dataNodeId string, freeEcSlot int) *EcNode {
+ return &EcNode{
+ info: &master_pb.DataNodeInfo{
+ Id: dataNodeId,
+ },
+ dc: dc,
+ rack: RackId(rack),
+ freeEcSlot: freeEcSlot,
+ }
+}
+
+func (ecNode *EcNode) addEcVolumeAndShardsForTest(vid uint32, collection string, shardIds []uint32) *EcNode {
+ return ecNode.addEcVolumeShards(needle.VolumeId(vid), collection, shardIds)
+}
diff --git a/weed/shell/command_fs_cat.go b/weed/shell/command_fs_cat.go
new file mode 100644
index 000000000..66ced46c5
--- /dev/null
+++ b/weed/shell/command_fs_cat.go
@@ -0,0 +1,68 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math"
+
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsCat{})
+}
+
+type commandFsCat struct {
+}
+
+func (c *commandFsCat) Name() string {
+ return "fs.cat"
+}
+
+func (c *commandFsCat) Help() string {
+ return `stream the file content on to the screen
+
+ fs.cat /dir/
+ fs.cat /dir/file_name
+ fs.cat /dir/file_prefix
+ fs.cat http://<filer_server>:<port>/dir/
+ fs.cat http://<filer_server>:<port>/dir/file_name
+ fs.cat http://<filer_server>:<port>/dir/file_prefix
+`
+}
+
+func (c *commandFsCat) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ input := findInputDirectory(args)
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(input)
+ if err != nil {
+ return err
+ }
+
+ ctx := context.Background()
+
+ if commandEnv.isDirectory(ctx, filerServer, filerPort, path) {
+ return fmt.Errorf("%s is a directory", path)
+ }
+
+ dir, name := filer2.FullPath(path).DirAndName()
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ request := &filer_pb.LookupDirectoryEntryRequest{
+ Name: name,
+ Directory: dir,
+ }
+ respLookupEntry, err := client.LookupDirectoryEntry(ctx, request)
+ if err != nil {
+ return err
+ }
+
+ return filer2.StreamContent(commandEnv.MasterClient, writer, respLookupEntry.Entry.Chunks, 0, math.MaxInt32)
+
+ })
+
+}
diff --git a/weed/shell/command_fs_cd.go b/weed/shell/command_fs_cd.go
new file mode 100644
index 000000000..408ec86c8
--- /dev/null
+++ b/weed/shell/command_fs_cd.go
@@ -0,0 +1,59 @@
+package shell
+
+import (
+ "context"
+ "io"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsCd{})
+}
+
+type commandFsCd struct {
+}
+
+func (c *commandFsCd) Name() string {
+ return "fs.cd"
+}
+
+func (c *commandFsCd) Help() string {
+ return `change directory to http://<filer_server>:<port>/dir/
+
+ The full path can be too long to type. For example,
+ fs.ls http://<filer_server>:<port>/some/path/to/file_name
+
+ can be simplified as
+
+ fs.cd http://<filer_server>:<port>/some/path
+ fs.ls to/file_name
+`
+}
+
+func (c *commandFsCd) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ input := findInputDirectory(args)
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(input)
+ if err != nil {
+ return err
+ }
+
+ if path == "/" {
+ commandEnv.option.FilerHost = filerServer
+ commandEnv.option.FilerPort = filerPort
+ commandEnv.option.Directory = "/"
+ return nil
+ }
+
+ ctx := context.Background()
+
+ err = commandEnv.checkDirectory(ctx, filerServer, filerPort, path)
+
+ if err == nil {
+ commandEnv.option.FilerHost = filerServer
+ commandEnv.option.FilerPort = filerPort
+ commandEnv.option.Directory = path
+ }
+
+ return err
+}
diff --git a/weed/shell/command_fs_du.go b/weed/shell/command_fs_du.go
new file mode 100644
index 000000000..5e634c82a
--- /dev/null
+++ b/weed/shell/command_fs_du.go
@@ -0,0 +1,117 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "github.com/chrislusf/seaweedfs/weed/util"
+ "google.golang.org/grpc"
+ "io"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsDu{})
+}
+
+type commandFsDu struct {
+}
+
+func (c *commandFsDu) Name() string {
+ return "fs.du"
+}
+
+func (c *commandFsDu) Help() string {
+ return `show disk usage
+
+ fs.du http://<filer_server>:<port>/dir
+ fs.du http://<filer_server>:<port>/dir/file_name
+ fs.du http://<filer_server>:<port>/dir/file_prefix
+`
+}
+
+func (c *commandFsDu) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(findInputDirectory(args))
+ if err != nil {
+ return err
+ }
+
+ ctx := context.Background()
+
+ if commandEnv.isDirectory(ctx, filerServer, filerPort, path) {
+ path = path + "/"
+ }
+
+ dir, name := filer2.FullPath(path).DirAndName()
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ _, _, err = paginateDirectory(ctx, writer, client, dir, name, 1000)
+
+ return err
+
+ })
+
+}
+
+func paginateDirectory(ctx context.Context, writer io.Writer, client filer_pb.SeaweedFilerClient, dir, name string, paginateSize int) (blockCount uint64, byteCount uint64, err error) {
+
+ paginatedCount := -1
+ startFromFileName := ""
+
+ for paginatedCount == -1 || paginatedCount == paginateSize {
+ resp, listErr := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
+ Directory: dir,
+ Prefix: name,
+ StartFromFileName: startFromFileName,
+ InclusiveStartFrom: false,
+ Limit: uint32(paginateSize),
+ })
+ if listErr != nil {
+ err = listErr
+ return
+ }
+
+ paginatedCount = len(resp.Entries)
+
+ for _, entry := range resp.Entries {
+ if entry.IsDirectory {
+ subDir := fmt.Sprintf("%s/%s", dir, entry.Name)
+ if dir == "/" {
+ subDir = "/" + entry.Name
+ }
+ numBlock, numByte, err := paginateDirectory(ctx, writer, client, subDir, "", paginateSize)
+ if err == nil {
+ blockCount += numBlock
+ byteCount += numByte
+ }
+ } else {
+ blockCount += uint64(len(entry.Chunks))
+ byteCount += filer2.TotalSize(entry.Chunks)
+ }
+ startFromFileName = entry.Name
+
+ if name != "" && !entry.IsDirectory {
+ fmt.Fprintf(writer, "block:%4d\tbyte:%10d\t%s/%s\n", blockCount, byteCount, dir, name)
+ }
+ }
+ }
+
+ if name == "" {
+ fmt.Fprintf(writer, "block:%4d\tbyte:%10d\t%s\n", blockCount, byteCount, dir)
+ }
+
+ return
+
+}
+
+func (env *CommandEnv) withFilerClient(ctx context.Context, filerServer string, filerPort int64, fn func(filer_pb.SeaweedFilerClient) error) error {
+
+ filerGrpcAddress := fmt.Sprintf("%s:%d", filerServer, filerPort+10000)
+ return util.WithCachedGrpcClient(ctx, func(grpcConnection *grpc.ClientConn) error {
+ client := filer_pb.NewSeaweedFilerClient(grpcConnection)
+ return fn(client)
+ }, filerGrpcAddress, env.option.GrpcDialOption)
+
+}
diff --git a/weed/shell/command_fs_ls.go b/weed/shell/command_fs_ls.go
new file mode 100644
index 000000000..6979635e1
--- /dev/null
+++ b/weed/shell/command_fs_ls.go
@@ -0,0 +1,148 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "io"
+ "os"
+ "os/user"
+ "strconv"
+ "strings"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsLs{})
+}
+
+type commandFsLs struct {
+}
+
+func (c *commandFsLs) Name() string {
+ return "fs.ls"
+}
+
+func (c *commandFsLs) Help() string {
+ return `list all files under a directory
+
+ fs.ls [-l] [-a] /dir/
+ fs.ls [-l] [-a] /dir/file_name
+ fs.ls [-l] [-a] /dir/file_prefix
+ fs.ls [-l] [-a] http://<filer_server>:<port>/dir/
+ fs.ls [-l] [-a] http://<filer_server>:<port>/dir/file_name
+ fs.ls [-l] [-a] http://<filer_server>:<port>/dir/file_prefix
+`
+}
+
+func (c *commandFsLs) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ var isLongFormat, showHidden bool
+ for _, arg := range args {
+ if !strings.HasPrefix(arg, "-") {
+ break
+ }
+ for _, t := range arg {
+ switch t {
+ case 'a':
+ showHidden = true
+ case 'l':
+ isLongFormat = true
+ }
+ }
+ }
+
+ input := findInputDirectory(args)
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(input)
+ if err != nil {
+ return err
+ }
+
+ ctx := context.Background()
+
+ if commandEnv.isDirectory(ctx, filerServer, filerPort, path) {
+ path = path + "/"
+ }
+
+ dir, name := filer2.FullPath(path).DirAndName()
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ return paginateOneDirectory(ctx, writer, client, dir, name, 1000, isLongFormat, showHidden)
+
+ })
+
+}
+
+func paginateOneDirectory(ctx context.Context, writer io.Writer, client filer_pb.SeaweedFilerClient, dir, name string, paginateSize int, isLongFormat, showHidden bool) (err error) {
+
+ entryCount := 0
+ paginatedCount := -1
+ startFromFileName := ""
+
+ for paginatedCount == -1 || paginatedCount == paginateSize {
+ resp, listErr := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
+ Directory: dir,
+ Prefix: name,
+ StartFromFileName: startFromFileName,
+ InclusiveStartFrom: false,
+ Limit: uint32(paginateSize),
+ })
+ if listErr != nil {
+ err = listErr
+ return
+ }
+
+ paginatedCount = len(resp.Entries)
+
+ for _, entry := range resp.Entries {
+
+ if !showHidden && strings.HasPrefix(entry.Name, ".") {
+ continue
+ }
+
+ entryCount++
+
+ if isLongFormat {
+ fileMode := os.FileMode(entry.Attributes.FileMode)
+ userName, groupNames := entry.Attributes.UserName, entry.Attributes.GroupName
+ if userName == "" {
+ if user, userErr := user.LookupId(strconv.Itoa(int(entry.Attributes.Uid))); userErr == nil {
+ userName = user.Username
+ }
+ }
+ groupName := ""
+ if len(groupNames) > 0 {
+ groupName = groupNames[0]
+ }
+ if groupName == "" {
+ if group, groupErr := user.LookupGroupId(strconv.Itoa(int(entry.Attributes.Gid))); groupErr == nil {
+ groupName = group.Name
+ }
+ }
+
+ if dir == "/" {
+ // just for printing
+ dir = ""
+ }
+ fmt.Fprintf(writer, "%s %3d %s %s %6d %s/%s\n",
+ fileMode, len(entry.Chunks),
+ userName, groupName,
+ filer2.TotalSize(entry.Chunks), dir, entry.Name)
+ } else {
+ fmt.Fprintf(writer, "%s\n", entry.Name)
+ }
+
+ startFromFileName = entry.Name
+
+ }
+ }
+
+ if isLongFormat {
+ fmt.Fprintf(writer, "total %d\n", entryCount)
+ }
+
+ return
+
+}
diff --git a/weed/shell/command_fs_meta_load.go b/weed/shell/command_fs_meta_load.go
new file mode 100644
index 000000000..5ea8de9f5
--- /dev/null
+++ b/weed/shell/command_fs_meta_load.go
@@ -0,0 +1,108 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "github.com/chrislusf/seaweedfs/weed/util"
+ "github.com/golang/protobuf/proto"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsMetaLoad{})
+}
+
+type commandFsMetaLoad struct {
+}
+
+func (c *commandFsMetaLoad) Name() string {
+ return "fs.meta.load"
+}
+
+func (c *commandFsMetaLoad) Help() string {
+ return `load saved filer meta data to restore the directory and file structure
+
+ fs.meta.load <filer_host>-<port>-<time>.meta
+
+`
+}
+
+func (c *commandFsMetaLoad) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) == 0 {
+ fmt.Fprintf(writer, "missing a metadata file\n")
+ return nil
+ }
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(findInputDirectory(nil))
+ if err != nil {
+ return err
+ }
+
+ fileName := args[len(args)-1]
+
+ dst, err := os.OpenFile(fileName, os.O_RDONLY, 0644)
+ if err != nil {
+ return nil
+ }
+ defer dst.Close()
+
+ var dirCount, fileCount uint64
+
+ ctx := context.Background()
+
+ err = commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ sizeBuf := make([]byte, 4)
+
+ for {
+ if n, err := dst.Read(sizeBuf); n != 4 {
+ if err == io.EOF {
+ return nil
+ }
+ return err
+ }
+
+ size := util.BytesToUint32(sizeBuf)
+
+ data := make([]byte, int(size))
+
+ if n, err := dst.Read(data); n != len(data) {
+ return err
+ }
+
+ fullEntry := &filer_pb.FullEntry{}
+ if err = proto.Unmarshal(data, fullEntry); err != nil {
+ return err
+ }
+
+ if _, err = client.CreateEntry(ctx, &filer_pb.CreateEntryRequest{
+ Directory: fullEntry.Dir,
+ Entry: fullEntry.Entry,
+ }); err != nil {
+ return err
+ }
+
+ fmt.Fprintf(writer, "load %s\n", filer2.FullPath(fullEntry.Dir).Child(fullEntry.Entry.Name))
+
+ if fullEntry.Entry.IsDirectory {
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ }
+
+ })
+
+ if err == nil {
+ fmt.Fprintf(writer, "\ntotal %d directories, %d files", dirCount, fileCount)
+ fmt.Fprintf(writer, "\n%s is loaded to http://%s:%d%s\n", fileName, filerServer, filerPort, path)
+ }
+
+ return err
+}
diff --git a/weed/shell/command_fs_meta_notify.go b/weed/shell/command_fs_meta_notify.go
new file mode 100644
index 000000000..13b272fbf
--- /dev/null
+++ b/weed/shell/command_fs_meta_notify.go
@@ -0,0 +1,78 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/notification"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "github.com/chrislusf/seaweedfs/weed/util"
+ "github.com/spf13/viper"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsMetaNotify{})
+}
+
+type commandFsMetaNotify struct {
+}
+
+func (c *commandFsMetaNotify) Name() string {
+ return "fs.meta.notify"
+}
+
+func (c *commandFsMetaNotify) Help() string {
+ return `recursively send directory and file meta data to notifiction message queue
+
+ fs.meta.notify # send meta data from current directory to notification message queue
+
+ The message queue will use it to trigger replication from this filer.
+
+`
+}
+
+func (c *commandFsMetaNotify) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(findInputDirectory(args))
+ if err != nil {
+ return err
+ }
+
+ util.LoadConfiguration("notification", true)
+ v := viper.GetViper()
+ notification.LoadConfiguration(v.Sub("notification"))
+
+ ctx := context.Background()
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ var dirCount, fileCount uint64
+
+ err = doTraverse(ctx, writer, client, filer2.FullPath(path), func(parentPath filer2.FullPath, entry *filer_pb.Entry) error {
+
+ if entry.IsDirectory {
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ return notification.Queue.SendMessage(
+ string(parentPath.Child(entry.Name)),
+ &filer_pb.EventNotification{
+ NewEntry: entry,
+ },
+ )
+
+ })
+
+ if err == nil {
+ fmt.Fprintf(writer, "\ntotal notified %d directories, %d files\n", dirCount, fileCount)
+ }
+
+ return err
+
+ })
+
+}
diff --git a/weed/shell/command_fs_meta_save.go b/weed/shell/command_fs_meta_save.go
new file mode 100644
index 000000000..6ca395fae
--- /dev/null
+++ b/weed/shell/command_fs_meta_save.go
@@ -0,0 +1,150 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "github.com/chrislusf/seaweedfs/weed/util"
+ "github.com/golang/protobuf/proto"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsMetaSave{})
+}
+
+type commandFsMetaSave struct {
+}
+
+func (c *commandFsMetaSave) Name() string {
+ return "fs.meta.save"
+}
+
+func (c *commandFsMetaSave) Help() string {
+ return `save all directory and file meta data to a local file for metadata backup.
+
+ fs.meta.save / # save from the root
+ fs.meta.save /path/to/save # save from the directory /path/to/save
+ fs.meta.save . # save from current directory
+ fs.meta.save # save from current directory
+
+ The meta data will be saved into a local <filer_host>-<port>-<time>.meta file.
+ These meta data can be later loaded by fs.meta.load command,
+
+ This assumes there are no deletions, so this is different from taking a snapshot.
+
+`
+}
+
+func (c *commandFsMetaSave) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(findInputDirectory(args))
+ if err != nil {
+ return err
+ }
+
+ ctx := context.Background()
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ t := time.Now()
+ fileName := fmt.Sprintf("%s-%d-%4d%02d%02d-%02d%02d%02d.meta",
+ filerServer, filerPort, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
+
+ dst, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return nil
+ }
+ defer dst.Close()
+
+ var dirCount, fileCount uint64
+
+ sizeBuf := make([]byte, 4)
+
+ err = doTraverse(ctx, writer, client, filer2.FullPath(path), func(parentPath filer2.FullPath, entry *filer_pb.Entry) error {
+
+ protoMessage := &filer_pb.FullEntry{
+ Dir: string(parentPath),
+ Entry: entry,
+ }
+
+ bytes, err := proto.Marshal(protoMessage)
+ if err != nil {
+ return fmt.Errorf("marshall error: %v", err)
+ }
+
+ util.Uint32toBytes(sizeBuf, uint32(len(bytes)))
+
+ dst.Write(sizeBuf)
+ dst.Write(bytes)
+
+ if entry.IsDirectory {
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ println(parentPath.Child(entry.Name))
+
+ return nil
+
+ })
+
+ if err == nil {
+ fmt.Fprintf(writer, "\ntotal %d directories, %d files", dirCount, fileCount)
+ fmt.Fprintf(writer, "\nmeta data for http://%s:%d%s is saved to %s\n", filerServer, filerPort, path, fileName)
+ }
+
+ return err
+
+ })
+
+}
+func doTraverse(ctx context.Context, writer io.Writer, client filer_pb.SeaweedFilerClient, parentPath filer2.FullPath, fn func(parentPath filer2.FullPath, entry *filer_pb.Entry) error) (err error) {
+
+ paginatedCount := -1
+ startFromFileName := ""
+ paginateSize := 1000
+
+ for paginatedCount == -1 || paginatedCount == paginateSize {
+ resp, listErr := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
+ Directory: string(parentPath),
+ Prefix: "",
+ StartFromFileName: startFromFileName,
+ InclusiveStartFrom: false,
+ Limit: uint32(paginateSize),
+ })
+ if listErr != nil {
+ err = listErr
+ return
+ }
+
+ paginatedCount = len(resp.Entries)
+
+ for _, entry := range resp.Entries {
+
+ if err = fn(parentPath, entry); err != nil {
+ return err
+ }
+
+ if entry.IsDirectory {
+ subDir := fmt.Sprintf("%s/%s", parentPath, entry.Name)
+ if parentPath == "/" {
+ subDir = "/" + entry.Name
+ }
+ if err = doTraverse(ctx, writer, client, filer2.FullPath(subDir), fn); err != nil {
+ return err
+ }
+ }
+ startFromFileName = entry.Name
+
+ }
+ }
+
+ return
+
+}
diff --git a/weed/shell/command_fs_mv.go b/weed/shell/command_fs_mv.go
new file mode 100644
index 000000000..130bfe4e7
--- /dev/null
+++ b/weed/shell/command_fs_mv.go
@@ -0,0 +1,96 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "path/filepath"
+
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsMv{})
+}
+
+type commandFsMv struct {
+}
+
+func (c *commandFsMv) Name() string {
+ return "fs.mv"
+}
+
+func (c *commandFsMv) Help() string {
+ return `move or rename a file or a folder
+
+ fs.mv <source entry> <destination entry>
+
+ fs.mv /dir/file_name /dir2/filename2
+ fs.mv /dir/file_name /dir2
+
+ fs.mv /dir/dir2 /dir3/dir4/
+ fs.mv /dir/dir2 /dir3/new_dir
+
+`
+}
+
+func (c *commandFsMv) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ filerServer, filerPort, sourcePath, err := commandEnv.parseUrl(args[0])
+ if err != nil {
+ return err
+ }
+
+ _, _, destinationPath, err := commandEnv.parseUrl(args[1])
+ if err != nil {
+ return err
+ }
+
+ ctx := context.Background()
+
+
+ sourceDir, sourceName := filer2.FullPath(sourcePath).DirAndName()
+
+ destinationDir, destinationName := filer2.FullPath(destinationPath).DirAndName()
+
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ // collect destination entry info
+ destinationRequest := &filer_pb.LookupDirectoryEntryRequest{
+ Name: destinationDir,
+ Directory: destinationName,
+ }
+ respDestinationLookupEntry, err := client.LookupDirectoryEntry(ctx, destinationRequest)
+
+ var targetDir, targetName string
+
+ // moving a file or folder
+ if err == nil && respDestinationLookupEntry.Entry.IsDirectory {
+ // to a directory
+ targetDir = filepath.ToSlash(filepath.Join(destinationDir, destinationName))
+ targetName = sourceName
+ } else {
+ // to a file or folder
+ targetDir = destinationDir
+ targetName = destinationName
+ }
+
+
+ request := &filer_pb.AtomicRenameEntryRequest{
+ OldDirectory: sourceDir,
+ OldName: sourceName,
+ NewDirectory: targetDir,
+ NewName: targetName,
+ }
+
+ _, err = client.AtomicRenameEntry(ctx, request)
+
+ fmt.Fprintf(writer, "move: %s => %s\n", sourcePath, filer2.NewFullPath(targetDir, targetName))
+
+ return err
+
+ })
+
+}
diff --git a/weed/shell/command_fs_pwd.go b/weed/shell/command_fs_pwd.go
new file mode 100644
index 000000000..084a5e90a
--- /dev/null
+++ b/weed/shell/command_fs_pwd.go
@@ -0,0 +1,32 @@
+package shell
+
+import (
+ "fmt"
+ "io"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsPwd{})
+}
+
+type commandFsPwd struct {
+}
+
+func (c *commandFsPwd) Name() string {
+ return "fs.pwd"
+}
+
+func (c *commandFsPwd) Help() string {
+ return `print out current directory`
+}
+
+func (c *commandFsPwd) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ fmt.Fprintf(writer, "http://%s:%d%s\n",
+ commandEnv.option.FilerHost,
+ commandEnv.option.FilerPort,
+ commandEnv.option.Directory,
+ )
+
+ return nil
+}
diff --git a/weed/shell/command_fs_tree.go b/weed/shell/command_fs_tree.go
new file mode 100644
index 000000000..8474e43ea
--- /dev/null
+++ b/weed/shell/command_fs_tree.go
@@ -0,0 +1,147 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "io"
+ "strings"
+)
+
+func init() {
+ Commands = append(Commands, &commandFsTree{})
+}
+
+type commandFsTree struct {
+}
+
+func (c *commandFsTree) Name() string {
+ return "fs.tree"
+}
+
+func (c *commandFsTree) Help() string {
+ return `recursively list all files under a directory
+
+ fs.tree http://<filer_server>:<port>/dir/
+`
+}
+
+func (c *commandFsTree) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ filerServer, filerPort, path, err := commandEnv.parseUrl(findInputDirectory(args))
+ if err != nil {
+ return err
+ }
+
+ dir, name := filer2.FullPath(path).DirAndName()
+
+ ctx := context.Background()
+
+ return commandEnv.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ dirCount, fCount, terr := treeTraverseDirectory(ctx, writer, client, dir, name, newPrefix(), -1)
+
+ if terr == nil {
+ fmt.Fprintf(writer, "%d directories, %d files\n", dirCount, fCount)
+ }
+
+ return terr
+
+ })
+
+}
+func treeTraverseDirectory(ctx context.Context, writer io.Writer, client filer_pb.SeaweedFilerClient, dir, name string, prefix *Prefix, level int) (directoryCount, fileCount int64, err error) {
+
+ paginatedCount := -1
+ startFromFileName := ""
+ paginateSize := 1000
+
+ for paginatedCount == -1 || paginatedCount == paginateSize {
+ resp, listErr := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
+ Directory: dir,
+ Prefix: name,
+ StartFromFileName: startFromFileName,
+ InclusiveStartFrom: false,
+ Limit: uint32(paginateSize),
+ })
+ if listErr != nil {
+ err = listErr
+ return
+ }
+
+ paginatedCount = len(resp.Entries)
+ if paginatedCount > 0 {
+ prefix.addMarker(level)
+ }
+
+ for i, entry := range resp.Entries {
+
+ if level < 0 && name != "" {
+ if entry.Name != name {
+ break
+ }
+ }
+
+ // 0.1% wrong prefix here, but fixing it would need to paginate to the next batch first
+ isLast := paginatedCount < paginateSize && i == paginatedCount-1
+ fmt.Fprintf(writer, "%s%s\n", prefix.getPrefix(level, isLast), entry.Name)
+
+ if entry.IsDirectory {
+ directoryCount++
+ subDir := fmt.Sprintf("%s/%s", dir, entry.Name)
+ if dir == "/" {
+ subDir = "/" + entry.Name
+ }
+ dirCount, fCount, terr := treeTraverseDirectory(ctx, writer, client, subDir, "", prefix, level+1)
+ directoryCount += dirCount
+ fileCount += fCount
+ err = terr
+ } else {
+ fileCount++
+ }
+ startFromFileName = entry.Name
+
+ }
+ }
+
+ return
+
+}
+
+type Prefix struct {
+ markers map[int]bool
+}
+
+func newPrefix() *Prefix {
+ return &Prefix{
+ markers: make(map[int]bool),
+ }
+}
+func (p *Prefix) addMarker(marker int) {
+ p.markers[marker] = true
+}
+func (p *Prefix) removeMarker(marker int) {
+ delete(p.markers, marker)
+}
+func (p *Prefix) getPrefix(level int, isLastChild bool) string {
+ var sb strings.Builder
+ if level < 0 {
+ return ""
+ }
+ for i := 0; i < level; i++ {
+ if _, ok := p.markers[i]; ok {
+ sb.WriteString("│")
+ } else {
+ sb.WriteString(" ")
+ }
+ sb.WriteString(" ")
+ }
+ if isLastChild {
+ sb.WriteString("└──")
+ p.removeMarker(level)
+ } else {
+ sb.WriteString("├──")
+ }
+ return sb.String()
+}
diff --git a/weed/shell/command_volume_balance.go b/weed/shell/command_volume_balance.go
new file mode 100644
index 000000000..d7ef0d005
--- /dev/null
+++ b/weed/shell/command_volume_balance.go
@@ -0,0 +1,246 @@
+package shell
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "time"
+
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeBalance{})
+}
+
+type commandVolumeBalance struct {
+}
+
+func (c *commandVolumeBalance) Name() string {
+ return "volume.balance"
+}
+
+func (c *commandVolumeBalance) Help() string {
+ return `balance all volumes among volume servers
+
+ volume.balance [-c ALL|EACH_COLLECTION|<collection_name>] [-force] [-dataCenter=<data_center_name>]
+
+ Algorithm:
+
+ For each type of volume server (different max volume count limit){
+ for each collection {
+ balanceWritableVolumes()
+ balanceReadOnlyVolumes()
+ }
+ }
+
+ func balanceWritableVolumes(){
+ idealWritableVolumes = totalWritableVolumes / numVolumeServers
+ for hasMovedOneVolume {
+ sort all volume servers ordered by the number of local writable volumes
+ pick the volume server A with the lowest number of writable volumes x
+ pick the volume server B with the highest number of writable volumes y
+ if y > idealWritableVolumes and x +1 <= idealWritableVolumes {
+ if B has a writable volume id v that A does not have {
+ move writable volume v from A to B
+ }
+ }
+ }
+ }
+ func balanceReadOnlyVolumes(){
+ //similar to balanceWritableVolumes
+ }
+
+`
+}
+
+func (c *commandVolumeBalance) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ balanceCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+ collection := balanceCommand.String("collection", "EACH_COLLECTION", "collection name, or use \"ALL_COLLECTIONS\" across collections, \"EACH_COLLECTION\" for each collection")
+ dc := balanceCommand.String("dataCenter", "", "only apply the balancing for this dataCenter")
+ applyBalancing := balanceCommand.Bool("force", false, "apply the balancing plan.")
+ if err = balanceCommand.Parse(args); err != nil {
+ return nil
+ }
+
+ var resp *master_pb.VolumeListResponse
+ ctx := context.Background()
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ resp, err = client.VolumeList(ctx, &master_pb.VolumeListRequest{})
+ return err
+ })
+ if err != nil {
+ return err
+ }
+
+ typeToNodes := collectVolumeServersByType(resp.TopologyInfo, *dc)
+ for _, volumeServers := range typeToNodes {
+ if len(volumeServers) < 2 {
+ continue
+ }
+ if *collection == "EACH_COLLECTION" {
+ collections, err := ListCollectionNames(commandEnv, true, false)
+ if err != nil {
+ return err
+ }
+ for _, c := range collections {
+ if err = balanceVolumeServers(commandEnv, volumeServers, resp.VolumeSizeLimitMb*1024*1024, c, *applyBalancing); err != nil {
+ return err
+ }
+ }
+ } else if *collection == "ALL" {
+ if err = balanceVolumeServers(commandEnv, volumeServers, resp.VolumeSizeLimitMb*1024*1024, "ALL", *applyBalancing); err != nil {
+ return err
+ }
+ } else {
+ if err = balanceVolumeServers(commandEnv, volumeServers, resp.VolumeSizeLimitMb*1024*1024, *collection, *applyBalancing); err != nil {
+ return err
+ }
+ }
+
+ }
+ return nil
+}
+
+func balanceVolumeServers(commandEnv *CommandEnv, dataNodeInfos []*master_pb.DataNodeInfo, volumeSizeLimit uint64, collection string, applyBalancing bool) error {
+ var nodes []*Node
+ for _, dn := range dataNodeInfos {
+ nodes = append(nodes, &Node{
+ info: dn,
+ })
+ }
+
+ // balance writable volumes
+ for _, n := range nodes {
+ n.selectVolumes(func(v *master_pb.VolumeInformationMessage) bool {
+ if collection != "ALL" {
+ if v.Collection != collection {
+ return false
+ }
+ }
+ return !v.ReadOnly && v.Size < volumeSizeLimit
+ })
+ }
+ if err := balanceSelectedVolume(commandEnv, nodes, sortWritableVolumes, applyBalancing); err != nil {
+ return err
+ }
+
+ // balance readable volumes
+ for _, n := range nodes {
+ n.selectVolumes(func(v *master_pb.VolumeInformationMessage) bool {
+ if collection != "ALL" {
+ if v.Collection != collection {
+ return false
+ }
+ }
+ return v.ReadOnly || v.Size >= volumeSizeLimit
+ })
+ }
+ if err := balanceSelectedVolume(commandEnv, nodes, sortReadOnlyVolumes, applyBalancing); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func collectVolumeServersByType(t *master_pb.TopologyInfo, selectedDataCenter string) (typeToNodes map[uint64][]*master_pb.DataNodeInfo) {
+ typeToNodes = make(map[uint64][]*master_pb.DataNodeInfo)
+ for _, dc := range t.DataCenterInfos {
+ if selectedDataCenter != "" && dc.Id != selectedDataCenter {
+ continue
+ }
+ for _, r := range dc.RackInfos {
+ for _, dn := range r.DataNodeInfos {
+ typeToNodes[dn.MaxVolumeCount] = append(typeToNodes[dn.MaxVolumeCount], dn)
+ }
+ }
+ }
+ return
+}
+
+type Node struct {
+ info *master_pb.DataNodeInfo
+ selectedVolumes map[uint32]*master_pb.VolumeInformationMessage
+}
+
+func sortWritableVolumes(volumes []*master_pb.VolumeInformationMessage) {
+ sort.Slice(volumes, func(i, j int) bool {
+ return volumes[i].Size < volumes[j].Size
+ })
+}
+
+func sortReadOnlyVolumes(volumes []*master_pb.VolumeInformationMessage) {
+ sort.Slice(volumes, func(i, j int) bool {
+ return volumes[i].Id < volumes[j].Id
+ })
+}
+
+func balanceSelectedVolume(commandEnv *CommandEnv, nodes []*Node, sortCandidatesFn func(volumes []*master_pb.VolumeInformationMessage), applyBalancing bool) error {
+ selectedVolumeCount := 0
+ for _, dn := range nodes {
+ selectedVolumeCount += len(dn.selectedVolumes)
+ }
+
+ idealSelectedVolumes := ceilDivide(selectedVolumeCount, len(nodes))
+
+ hasMove := true
+
+ for hasMove {
+ hasMove = false
+ sort.Slice(nodes, func(i, j int) bool {
+ // TODO sort by free volume slots???
+ return len(nodes[i].selectedVolumes) < len(nodes[j].selectedVolumes)
+ })
+ emptyNode, fullNode := nodes[0], nodes[len(nodes)-1]
+ if len(fullNode.selectedVolumes) > idealSelectedVolumes && len(emptyNode.selectedVolumes)+1 <= idealSelectedVolumes {
+
+ // sort the volumes to move
+ var candidateVolumes []*master_pb.VolumeInformationMessage
+ for _, v := range fullNode.selectedVolumes {
+ candidateVolumes = append(candidateVolumes, v)
+ }
+ sortCandidatesFn(candidateVolumes)
+
+ for _, v := range candidateVolumes {
+ if _, found := emptyNode.selectedVolumes[v.Id]; !found {
+ if err := moveVolume(commandEnv, v, fullNode, emptyNode, applyBalancing); err == nil {
+ delete(fullNode.selectedVolumes, v.Id)
+ emptyNode.selectedVolumes[v.Id] = v
+ hasMove = true
+ break
+ } else {
+ return err
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func moveVolume(commandEnv *CommandEnv, v *master_pb.VolumeInformationMessage, fullNode *Node, emptyNode *Node, applyBalancing bool) error {
+ collectionPrefix := v.Collection + "_"
+ if v.Collection == "" {
+ collectionPrefix = ""
+ }
+ fmt.Fprintf(os.Stdout, "moving volume %s%d %s => %s\n", collectionPrefix, v.Id, fullNode.info.Id, emptyNode.info.Id)
+ if applyBalancing {
+ ctx := context.Background()
+ return LiveMoveVolume(ctx, commandEnv.option.GrpcDialOption, needle.VolumeId(v.Id), fullNode.info.Id, emptyNode.info.Id, 5*time.Second)
+ }
+ return nil
+}
+
+func (node *Node) selectVolumes(fn func(v *master_pb.VolumeInformationMessage) bool) {
+ node.selectedVolumes = make(map[uint32]*master_pb.VolumeInformationMessage)
+ for _, v := range node.info.VolumeInfos {
+ if fn(v) {
+ node.selectedVolumes[v.Id] = v
+ }
+ }
+}
diff --git a/weed/shell/command_volume_copy.go b/weed/shell/command_volume_copy.go
new file mode 100644
index 000000000..1c83ba655
--- /dev/null
+++ b/weed/shell/command_volume_copy.go
@@ -0,0 +1,53 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeCopy{})
+}
+
+type commandVolumeCopy struct {
+}
+
+func (c *commandVolumeCopy) Name() string {
+ return "volume.copy"
+}
+
+func (c *commandVolumeCopy) Help() string {
+ return `copy a volume from one volume server to another volume server
+
+ volume.copy <source volume server host:port> <target volume server host:port> <volume id>
+
+ This command copies a volume from one volume server to another volume server.
+ Usually you will want to unmount the volume first before copying.
+
+`
+}
+
+func (c *commandVolumeCopy) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) != 3 {
+ fmt.Fprintf(writer, "received args: %+v\n", args)
+ return fmt.Errorf("need 3 args of <source volume server host:port> <target volume server host:port> <volume id>")
+ }
+ sourceVolumeServer, targetVolumeServer, volumeIdString := args[0], args[1], args[2]
+
+ volumeId, err := needle.NewVolumeId(volumeIdString)
+ if err != nil {
+ return fmt.Errorf("wrong volume id format %s: %v", volumeId, err)
+ }
+
+ if sourceVolumeServer == targetVolumeServer {
+ return fmt.Errorf("source and target volume servers are the same!")
+ }
+
+ ctx := context.Background()
+ _, err = copyVolume(ctx, commandEnv.option.GrpcDialOption, volumeId, sourceVolumeServer, targetVolumeServer)
+ return
+}
diff --git a/weed/shell/command_volume_delete.go b/weed/shell/command_volume_delete.go
new file mode 100644
index 000000000..17d27ea3a
--- /dev/null
+++ b/weed/shell/command_volume_delete.go
@@ -0,0 +1,48 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeDelete{})
+}
+
+type commandVolumeDelete struct {
+}
+
+func (c *commandVolumeDelete) Name() string {
+ return "volume.delete"
+}
+
+func (c *commandVolumeDelete) Help() string {
+ return `delete a live volume from one volume server
+
+ volume.delete <volume server host:port> <volume id>
+
+ This command deletes a volume from one volume server.
+
+`
+}
+
+func (c *commandVolumeDelete) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) != 2 {
+ fmt.Fprintf(writer, "received args: %+v\n", args)
+ return fmt.Errorf("need 2 args of <volume server host:port> <volume id>")
+ }
+ sourceVolumeServer, volumeIdString := args[0], args[1]
+
+ volumeId, err := needle.NewVolumeId(volumeIdString)
+ if err != nil {
+ return fmt.Errorf("wrong volume id format %s: %v", volumeId, err)
+ }
+
+ ctx := context.Background()
+ return deleteVolume(ctx, commandEnv.option.GrpcDialOption, volumeId, sourceVolumeServer)
+
+}
diff --git a/weed/shell/command_volume_fix_replication.go b/weed/shell/command_volume_fix_replication.go
new file mode 100644
index 000000000..4c7a794c0
--- /dev/null
+++ b/weed/shell/command_volume_fix_replication.go
@@ -0,0 +1,200 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage"
+ "io"
+ "math/rand"
+ "sort"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeFixReplication{})
+}
+
+type commandVolumeFixReplication struct {
+}
+
+func (c *commandVolumeFixReplication) Name() string {
+ return "volume.fix.replication"
+}
+
+func (c *commandVolumeFixReplication) Help() string {
+ return `add replicas to volumes that are missing replicas
+
+ This command file all under-replicated volumes, and find volume servers with free slots.
+ If the free slots satisfy the replication requirement, the volume content is copied over and mounted.
+
+ volume.fix.replication -n # do not take action
+ volume.fix.replication # actually copying the volume files and mount the volume
+
+ Note:
+ * each time this will only add back one replica for one volume id. If there are multiple replicas
+ are missing, e.g. multiple volume servers are new, you may need to run this multiple times.
+ * do not run this too quick within seconds, since the new volume replica may take a few seconds
+ to register itself to the master.
+
+`
+}
+
+func (c *commandVolumeFixReplication) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ takeAction := true
+ if len(args) > 0 && args[0] == "-n" {
+ takeAction = false
+ }
+
+ var resp *master_pb.VolumeListResponse
+ ctx := context.Background()
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ resp, err = client.VolumeList(ctx, &master_pb.VolumeListRequest{})
+ return err
+ })
+ if err != nil {
+ return err
+ }
+
+ // find all volumes that needs replication
+ // collect all data nodes
+ replicatedVolumeLocations := make(map[uint32][]location)
+ replicatedVolumeInfo := make(map[uint32]*master_pb.VolumeInformationMessage)
+ var allLocations []location
+ eachDataNode(resp.TopologyInfo, func(dc string, rack RackId, dn *master_pb.DataNodeInfo) {
+ loc := newLocation(dc, string(rack), dn)
+ for _, v := range dn.VolumeInfos {
+ if v.ReplicaPlacement > 0 {
+ replicatedVolumeLocations[v.Id] = append(replicatedVolumeLocations[v.Id], loc)
+ replicatedVolumeInfo[v.Id] = v
+ }
+ }
+ allLocations = append(allLocations, loc)
+ })
+
+ // find all under replicated volumes
+ underReplicatedVolumeLocations := make(map[uint32][]location)
+ for vid, locations := range replicatedVolumeLocations {
+ volumeInfo := replicatedVolumeInfo[vid]
+ replicaPlacement, _ := storage.NewReplicaPlacementFromByte(byte(volumeInfo.ReplicaPlacement))
+ if replicaPlacement.GetCopyCount() > len(locations) {
+ underReplicatedVolumeLocations[vid] = locations
+ }
+ }
+
+ if len(underReplicatedVolumeLocations) == 0 {
+ return fmt.Errorf("no under replicated volumes")
+ }
+
+ if len(allLocations) == 0 {
+ return fmt.Errorf("no data nodes at all")
+ }
+
+ // find the most under populated data nodes
+ keepDataNodesSorted(allLocations)
+
+ for vid, locations := range underReplicatedVolumeLocations {
+ volumeInfo := replicatedVolumeInfo[vid]
+ replicaPlacement, _ := storage.NewReplicaPlacementFromByte(byte(volumeInfo.ReplicaPlacement))
+ foundNewLocation := false
+ for _, dst := range allLocations {
+ // check whether data nodes satisfy the constraints
+ if dst.dataNode.FreeVolumeCount > 0 && satisfyReplicaPlacement(replicaPlacement, locations, dst) {
+ // ask the volume server to replicate the volume
+ sourceNodes := underReplicatedVolumeLocations[vid]
+ sourceNode := sourceNodes[rand.Intn(len(sourceNodes))]
+ foundNewLocation = true
+ fmt.Fprintf(writer, "replicating volume %d %s from %s to dataNode %s ...\n", volumeInfo.Id, replicaPlacement, sourceNode.dataNode.Id, dst.dataNode.Id)
+
+ if !takeAction {
+ break
+ }
+
+ err := operation.WithVolumeServerClient(dst.dataNode.Id, commandEnv.option.GrpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, replicateErr := volumeServerClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
+ VolumeId: volumeInfo.Id,
+ SourceDataNode: sourceNode.dataNode.Id,
+ })
+ return replicateErr
+ })
+
+ if err != nil {
+ return err
+ }
+
+ // adjust free volume count
+ dst.dataNode.FreeVolumeCount--
+ keepDataNodesSorted(allLocations)
+ break
+ }
+ }
+ if !foundNewLocation {
+ fmt.Fprintf(writer, "failed to place volume %d replica as %s, existing:%+v\n", volumeInfo.Id, replicaPlacement, locations)
+ }
+
+ }
+
+ return nil
+}
+
+func keepDataNodesSorted(dataNodes []location) {
+ sort.Slice(dataNodes, func(i, j int) bool {
+ return dataNodes[i].dataNode.FreeVolumeCount > dataNodes[j].dataNode.FreeVolumeCount
+ })
+}
+
+func satisfyReplicaPlacement(replicaPlacement *storage.ReplicaPlacement, existingLocations []location, possibleLocation location) bool {
+
+ existingDataCenters := make(map[string]bool)
+ existingRacks := make(map[string]bool)
+ existingDataNodes := make(map[string]bool)
+ for _, loc := range existingLocations {
+ existingDataCenters[loc.DataCenter()] = true
+ existingRacks[loc.Rack()] = true
+ existingDataNodes[loc.String()] = true
+ }
+
+ if replicaPlacement.DiffDataCenterCount >= len(existingDataCenters) {
+ // check dc, good if different from any existing data centers
+ _, found := existingDataCenters[possibleLocation.DataCenter()]
+ return !found
+ } else if replicaPlacement.DiffRackCount >= len(existingRacks) {
+ // check rack, good if different from any existing racks
+ _, found := existingRacks[possibleLocation.Rack()]
+ return !found
+ } else if replicaPlacement.SameRackCount >= len(existingDataNodes) {
+ // check data node, good if different from any existing data nodes
+ _, found := existingDataNodes[possibleLocation.String()]
+ return !found
+ }
+
+ return false
+}
+
+type location struct {
+ dc string
+ rack string
+ dataNode *master_pb.DataNodeInfo
+}
+
+func newLocation(dc, rack string, dataNode *master_pb.DataNodeInfo) location {
+ return location{
+ dc: dc,
+ rack: rack,
+ dataNode: dataNode,
+ }
+}
+
+func (l location) String() string {
+ return fmt.Sprintf("%s %s %s", l.dc, l.rack, l.dataNode.Id)
+}
+
+func (l location) Rack() string {
+ return fmt.Sprintf("%s %s", l.dc, l.rack)
+}
+
+func (l location) DataCenter() string {
+ return l.dc
+}
diff --git a/weed/shell/command_volume_list.go b/weed/shell/command_volume_list.go
new file mode 100644
index 000000000..134580ffe
--- /dev/null
+++ b/weed/shell/command_volume_list.go
@@ -0,0 +1,134 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/pb/master_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/erasure_coding"
+
+ "io"
+ "sort"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeList{})
+}
+
+type commandVolumeList struct {
+}
+
+func (c *commandVolumeList) Name() string {
+ return "volume.list"
+}
+
+func (c *commandVolumeList) Help() string {
+ return `list all volumes
+
+ This command list all volumes as a tree of dataCenter > rack > dataNode > volume.
+
+`
+}
+
+func (c *commandVolumeList) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ var resp *master_pb.VolumeListResponse
+ ctx := context.Background()
+ err = commandEnv.MasterClient.WithClient(ctx, func(client master_pb.SeaweedClient) error {
+ resp, err = client.VolumeList(ctx, &master_pb.VolumeListRequest{})
+ return err
+ })
+ if err != nil {
+ return err
+ }
+
+ writeTopologyInfo(writer, resp.TopologyInfo, resp.VolumeSizeLimitMb)
+ return nil
+}
+
+func writeTopologyInfo(writer io.Writer, t *master_pb.TopologyInfo, volumeSizeLimitMb uint64) statistics {
+ fmt.Fprintf(writer, "Topology volume:%d/%d active:%d free:%d volumeSizeLimit:%d MB\n", t.VolumeCount, t.MaxVolumeCount, t.ActiveVolumeCount, t.FreeVolumeCount, volumeSizeLimitMb)
+ sort.Slice(t.DataCenterInfos, func(i, j int) bool {
+ return t.DataCenterInfos[i].Id < t.DataCenterInfos[j].Id
+ })
+ var s statistics
+ for _, dc := range t.DataCenterInfos {
+ s = s.plus(writeDataCenterInfo(writer, dc))
+ }
+ fmt.Fprintf(writer, "%+v \n", s)
+ return s
+}
+func writeDataCenterInfo(writer io.Writer, t *master_pb.DataCenterInfo) statistics {
+ fmt.Fprintf(writer, " DataCenter %s volume:%d/%d active:%d free:%d\n", t.Id, t.VolumeCount, t.MaxVolumeCount, t.ActiveVolumeCount, t.FreeVolumeCount)
+ var s statistics
+ sort.Slice(t.RackInfos, func(i, j int) bool {
+ return t.RackInfos[i].Id < t.RackInfos[j].Id
+ })
+ for _, r := range t.RackInfos {
+ s = s.plus(writeRackInfo(writer, r))
+ }
+ fmt.Fprintf(writer, " DataCenter %s %+v \n", t.Id, s)
+ return s
+}
+func writeRackInfo(writer io.Writer, t *master_pb.RackInfo) statistics {
+ fmt.Fprintf(writer, " Rack %s volume:%d/%d active:%d free:%d\n", t.Id, t.VolumeCount, t.MaxVolumeCount, t.ActiveVolumeCount, t.FreeVolumeCount)
+ var s statistics
+ sort.Slice(t.DataNodeInfos, func(i, j int) bool {
+ return t.DataNodeInfos[i].Id < t.DataNodeInfos[j].Id
+ })
+ for _, dn := range t.DataNodeInfos {
+ s = s.plus(writeDataNodeInfo(writer, dn))
+ }
+ fmt.Fprintf(writer, " Rack %s %+v \n", t.Id, s)
+ return s
+}
+func writeDataNodeInfo(writer io.Writer, t *master_pb.DataNodeInfo) statistics {
+ fmt.Fprintf(writer, " DataNode %s volume:%d/%d active:%d free:%d\n", t.Id, t.VolumeCount, t.MaxVolumeCount, t.ActiveVolumeCount, t.FreeVolumeCount)
+ var s statistics
+ sort.Slice(t.VolumeInfos, func(i, j int) bool {
+ return t.VolumeInfos[i].Id < t.VolumeInfos[j].Id
+ })
+ for _, vi := range t.VolumeInfos {
+ s = s.plus(writeVolumeInformationMessage(writer, vi))
+ }
+ for _, ecShardInfo := range t.EcShardInfos {
+ fmt.Fprintf(writer, " ec volume id:%v collection:%v shards:%v\n", ecShardInfo.Id, ecShardInfo.Collection, erasure_coding.ShardBits(ecShardInfo.EcIndexBits).ShardIds())
+ }
+ fmt.Fprintf(writer, " DataNode %s %+v \n", t.Id, s)
+ return s
+}
+func writeVolumeInformationMessage(writer io.Writer, t *master_pb.VolumeInformationMessage) statistics {
+ fmt.Fprintf(writer, " volume %+v \n", t)
+ return newStatiscis(t)
+}
+
+type statistics struct {
+ Size uint64
+ FileCount uint64
+ DeletedFileCount uint64
+ DeletedBytes uint64
+}
+
+func newStatiscis(t *master_pb.VolumeInformationMessage) statistics {
+ return statistics{
+ Size: t.Size,
+ FileCount: t.FileCount,
+ DeletedFileCount: t.DeleteCount,
+ DeletedBytes: t.DeletedByteCount,
+ }
+}
+
+func (s statistics) plus(t statistics) statistics {
+ return statistics{
+ Size: s.Size + t.Size,
+ FileCount: s.FileCount + t.FileCount,
+ DeletedFileCount: s.DeletedFileCount + t.DeletedFileCount,
+ DeletedBytes: s.DeletedBytes + t.DeletedBytes,
+ }
+}
+
+func (s statistics) String() string {
+ if s.DeletedFileCount > 0 {
+ return fmt.Sprintf("total size:%d file_count:%d deleted_file:%d deleted_bytes:%d", s.Size, s.FileCount, s.DeletedFileCount, s.DeletedBytes)
+ }
+ return fmt.Sprintf("total size:%d file_count:%d", s.Size, s.FileCount)
+}
diff --git a/weed/shell/command_volume_mount.go b/weed/shell/command_volume_mount.go
new file mode 100644
index 000000000..50a307492
--- /dev/null
+++ b/weed/shell/command_volume_mount.go
@@ -0,0 +1,60 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+ "google.golang.org/grpc"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeMount{})
+}
+
+type commandVolumeMount struct {
+}
+
+func (c *commandVolumeMount) Name() string {
+ return "volume.mount"
+}
+
+func (c *commandVolumeMount) Help() string {
+ return `mount a volume from one volume server
+
+ volume.mount <volume server host:port> <volume id>
+
+ This command mounts a volume from one volume server.
+
+`
+}
+
+func (c *commandVolumeMount) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) != 2 {
+ fmt.Fprintf(writer, "received args: %+v\n", args)
+ return fmt.Errorf("need 2 args of <volume server host:port> <volume id>")
+ }
+ sourceVolumeServer, volumeIdString := args[0], args[1]
+
+ volumeId, err := needle.NewVolumeId(volumeIdString)
+ if err != nil {
+ return fmt.Errorf("wrong volume id format %s: %v", volumeId, err)
+ }
+
+ ctx := context.Background()
+ return mountVolume(ctx, commandEnv.option.GrpcDialOption, volumeId, sourceVolumeServer)
+
+}
+
+func mountVolume(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, sourceVolumeServer string) (err error) {
+ return operation.WithVolumeServerClient(sourceVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, mountErr := volumeServerClient.VolumeMount(ctx, &volume_server_pb.VolumeMountRequest{
+ VolumeId: uint32(volumeId),
+ })
+ return mountErr
+ })
+}
diff --git a/weed/shell/command_volume_move.go b/weed/shell/command_volume_move.go
new file mode 100644
index 000000000..08d87c988
--- /dev/null
+++ b/weed/shell/command_volume_move.go
@@ -0,0 +1,126 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "time"
+
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+ "google.golang.org/grpc"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeMove{})
+}
+
+type commandVolumeMove struct {
+}
+
+func (c *commandVolumeMove) Name() string {
+ return "volume.move"
+}
+
+func (c *commandVolumeMove) Help() string {
+ return `<experimental> move a live volume from one volume server to another volume server
+
+ volume.move <source volume server host:port> <target volume server host:port> <volume id>
+
+ This command move a live volume from one volume server to another volume server. Here are the steps:
+
+ 1. This command asks the target volume server to copy the source volume from source volume server, remember the last entry's timestamp.
+ 2. This command asks the target volume server to mount the new volume
+ Now the master will mark this volume id as readonly.
+ 3. This command asks the target volume server to tail the source volume for updates after the timestamp, for 1 minutes to drain the requests.
+ 4. This command asks the source volume server to unmount the source volume
+ Now the master will mark this volume id as writable.
+ 5. This command asks the source volume server to delete the source volume
+
+`
+}
+
+func (c *commandVolumeMove) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) != 3 {
+ fmt.Fprintf(writer, "received args: %+v\n", args)
+ return fmt.Errorf("need 3 args of <source volume server host:port> <target volume server host:port> <volume id>")
+ }
+ sourceVolumeServer, targetVolumeServer, volumeIdString := args[0], args[1], args[2]
+
+ volumeId, err := needle.NewVolumeId(volumeIdString)
+ if err != nil {
+ return fmt.Errorf("wrong volume id format %s: %v", volumeId, err)
+ }
+
+ if sourceVolumeServer == targetVolumeServer {
+ return fmt.Errorf("source and target volume servers are the same!")
+ }
+
+ ctx := context.Background()
+ return LiveMoveVolume(ctx, commandEnv.option.GrpcDialOption, volumeId, sourceVolumeServer, targetVolumeServer, 5*time.Second)
+}
+
+// LiveMoveVolume moves one volume from one source volume server to one target volume server, with idleTimeout to drain the incoming requests.
+func LiveMoveVolume(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, sourceVolumeServer, targetVolumeServer string, idleTimeout time.Duration) (err error) {
+
+ log.Printf("copying volume %d from %s to %s", volumeId, sourceVolumeServer, targetVolumeServer)
+ lastAppendAtNs, err := copyVolume(ctx, grpcDialOption, volumeId, sourceVolumeServer, targetVolumeServer)
+ if err != nil {
+ return fmt.Errorf("copy volume %d from %s to %s: %v", volumeId, sourceVolumeServer, targetVolumeServer, err)
+ }
+
+ log.Printf("tailing volume %d from %s to %s", volumeId, sourceVolumeServer, targetVolumeServer)
+ if err = tailVolume(ctx, grpcDialOption, volumeId, sourceVolumeServer, targetVolumeServer, lastAppendAtNs, idleTimeout); err != nil {
+ return fmt.Errorf("tail volume %d from %s to %s: %v", volumeId, sourceVolumeServer, targetVolumeServer, err)
+ }
+
+ log.Printf("deleting volume %d from %s", volumeId, sourceVolumeServer)
+ if err = deleteVolume(ctx, grpcDialOption, volumeId, sourceVolumeServer); err != nil {
+ return fmt.Errorf("delete volume %d from %s: %v", volumeId, sourceVolumeServer, err)
+ }
+
+ log.Printf("moved volume %d from %s to %s", volumeId, sourceVolumeServer, targetVolumeServer)
+ return nil
+}
+
+func copyVolume(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, sourceVolumeServer, targetVolumeServer string) (lastAppendAtNs uint64, err error) {
+
+ err = operation.WithVolumeServerClient(targetVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ resp, replicateErr := volumeServerClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
+ VolumeId: uint32(volumeId),
+ SourceDataNode: sourceVolumeServer,
+ })
+ if replicateErr == nil {
+ lastAppendAtNs = resp.LastAppendAtNs
+ }
+ return replicateErr
+ })
+
+ return
+}
+
+func tailVolume(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, sourceVolumeServer, targetVolumeServer string, lastAppendAtNs uint64, idleTimeout time.Duration) (err error) {
+
+ return operation.WithVolumeServerClient(targetVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, replicateErr := volumeServerClient.VolumeTailReceiver(ctx, &volume_server_pb.VolumeTailReceiverRequest{
+ VolumeId: uint32(volumeId),
+ SinceNs: lastAppendAtNs,
+ IdleTimeoutSeconds: uint32(idleTimeout.Seconds()),
+ SourceVolumeServer: sourceVolumeServer,
+ })
+ return replicateErr
+ })
+
+}
+
+func deleteVolume(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, sourceVolumeServer string) (err error) {
+ return operation.WithVolumeServerClient(sourceVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, deleteErr := volumeServerClient.VolumeDelete(ctx, &volume_server_pb.VolumeDeleteRequest{
+ VolumeId: uint32(volumeId),
+ })
+ return deleteErr
+ })
+}
diff --git a/weed/shell/command_volume_unmount.go b/weed/shell/command_volume_unmount.go
new file mode 100644
index 000000000..8096f34d8
--- /dev/null
+++ b/weed/shell/command_volume_unmount.go
@@ -0,0 +1,60 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/chrislusf/seaweedfs/weed/operation"
+ "github.com/chrislusf/seaweedfs/weed/pb/volume_server_pb"
+ "github.com/chrislusf/seaweedfs/weed/storage/needle"
+ "google.golang.org/grpc"
+)
+
+func init() {
+ Commands = append(Commands, &commandVolumeUnmount{})
+}
+
+type commandVolumeUnmount struct {
+}
+
+func (c *commandVolumeUnmount) Name() string {
+ return "volume.unmount"
+}
+
+func (c *commandVolumeUnmount) Help() string {
+ return `unmount a volume from one volume server
+
+ volume.unmount <volume server host:port> <volume id>
+
+ This command unmounts a volume from one volume server.
+
+`
+}
+
+func (c *commandVolumeUnmount) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
+
+ if len(args) != 2 {
+ fmt.Fprintf(writer, "received args: %+v\n", args)
+ return fmt.Errorf("need 2 args of <volume server host:port> <volume id>")
+ }
+ sourceVolumeServer, volumeIdString := args[0], args[1]
+
+ volumeId, err := needle.NewVolumeId(volumeIdString)
+ if err != nil {
+ return fmt.Errorf("wrong volume id format %s: %v", volumeId, err)
+ }
+
+ ctx := context.Background()
+ return unmountVolume(ctx, commandEnv.option.GrpcDialOption, volumeId, sourceVolumeServer)
+
+}
+
+func unmountVolume(ctx context.Context, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, sourceVolumeServer string) (err error) {
+ return operation.WithVolumeServerClient(sourceVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
+ _, unmountErr := volumeServerClient.VolumeUnmount(ctx, &volume_server_pb.VolumeUnmountRequest{
+ VolumeId: uint32(volumeId),
+ })
+ return unmountErr
+ })
+}
diff --git a/weed/shell/commands.go b/weed/shell/commands.go
new file mode 100644
index 000000000..b642ec253
--- /dev/null
+++ b/weed/shell/commands.go
@@ -0,0 +1,130 @@
+package shell
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/chrislusf/seaweedfs/weed/filer2"
+ "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
+ "github.com/chrislusf/seaweedfs/weed/wdclient"
+ "google.golang.org/grpc"
+)
+
+type ShellOptions struct {
+ Masters *string
+ GrpcDialOption grpc.DialOption
+ // shell transient context
+ FilerHost string
+ FilerPort int64
+ Directory string
+}
+
+type CommandEnv struct {
+ env map[string]string
+ MasterClient *wdclient.MasterClient
+ option ShellOptions
+}
+
+type command interface {
+ Name() string
+ Help() string
+ Do([]string, *CommandEnv, io.Writer) error
+}
+
+var (
+ Commands = []command{}
+)
+
+func NewCommandEnv(options ShellOptions) *CommandEnv {
+ return &CommandEnv{
+ env: make(map[string]string),
+ MasterClient: wdclient.NewMasterClient(context.Background(),
+ options.GrpcDialOption, "shell", strings.Split(*options.Masters, ",")),
+ option: options,
+ }
+}
+
+func (ce *CommandEnv) parseUrl(input string) (filerServer string, filerPort int64, path string, err error) {
+ if strings.HasPrefix(input, "http") {
+ return parseFilerUrl(input)
+ }
+ if !strings.HasPrefix(input, "/") {
+ input = filepath.ToSlash(filepath.Join(ce.option.Directory, input))
+ }
+ return ce.option.FilerHost, ce.option.FilerPort, input, err
+}
+
+func (ce *CommandEnv) isDirectory(ctx context.Context, filerServer string, filerPort int64, path string) bool {
+
+ return ce.checkDirectory(ctx, filerServer, filerPort, path) == nil
+
+}
+
+func (ce *CommandEnv) checkDirectory(ctx context.Context, filerServer string, filerPort int64, path string) error {
+
+ dir, name := filer2.FullPath(path).DirAndName()
+
+ return ce.withFilerClient(ctx, filerServer, filerPort, func(client filer_pb.SeaweedFilerClient) error {
+
+ resp, listErr := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
+ Directory: dir,
+ Prefix: name,
+ StartFromFileName: name,
+ InclusiveStartFrom: true,
+ Limit: 1,
+ })
+ if listErr != nil {
+ return listErr
+ }
+
+ if len(resp.Entries) == 0 {
+ return fmt.Errorf("entry not found")
+ }
+
+ if resp.Entries[0].Name != name {
+ return fmt.Errorf("not a valid directory, found %s", resp.Entries[0].Name)
+ }
+
+ if !resp.Entries[0].IsDirectory {
+ return fmt.Errorf("not a directory")
+ }
+
+ return nil
+ })
+
+}
+
+func parseFilerUrl(entryPath string) (filerServer string, filerPort int64, path string, err error) {
+ if strings.HasPrefix(entryPath, "http") {
+ var u *url.URL
+ u, err = url.Parse(entryPath)
+ if err != nil {
+ return
+ }
+ filerServer = u.Hostname()
+ portString := u.Port()
+ if portString != "" {
+ filerPort, err = strconv.ParseInt(portString, 10, 32)
+ }
+ path = u.Path
+ } else {
+ err = fmt.Errorf("path should have full url http://<filer_server>:<port>/path/to/dirOrFile : %s", entryPath)
+ }
+ return
+}
+
+func findInputDirectory(args []string) (input string) {
+ input = "."
+ if len(args) > 0 {
+ input = args[len(args)-1]
+ if strings.HasPrefix(input, "-") {
+ input = "."
+ }
+ }
+ return input
+}
diff --git a/weed/shell/shell_liner.go b/weed/shell/shell_liner.go
new file mode 100644
index 000000000..a4f17e0fa
--- /dev/null
+++ b/weed/shell/shell_liner.go
@@ -0,0 +1,146 @@
+package shell
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "regexp"
+ "strings"
+
+ "sort"
+
+ "github.com/peterh/liner"
+)
+
+var (
+ line *liner.State
+ historyPath = path.Join(os.TempDir(), "weed-shell")
+)
+
+func RunShell(options ShellOptions) {
+
+ line = liner.NewLiner()
+ defer line.Close()
+
+ line.SetCtrlCAborts(true)
+
+ setCompletionHandler()
+ loadHistory()
+
+ defer saveHistory()
+
+ reg, _ := regexp.Compile(`'.*?'|".*?"|\S+`)
+
+ commandEnv := NewCommandEnv(options)
+
+ go commandEnv.MasterClient.KeepConnectedToMaster()
+ commandEnv.MasterClient.WaitUntilConnected()
+
+ for {
+ cmd, err := line.Prompt("> ")
+ if err != nil {
+ if err != io.EOF {
+ fmt.Printf("%v\n", err)
+ }
+ return
+ }
+
+ cmds := reg.FindAllString(cmd, -1)
+ if len(cmds) == 0 {
+ continue
+ } else {
+ line.AppendHistory(cmd)
+
+ args := make([]string, len(cmds[1:]))
+
+ for i := range args {
+ args[i] = strings.Trim(string(cmds[1+i]), "\"'")
+ }
+
+ cmd := strings.ToLower(cmds[0])
+ if cmd == "help" || cmd == "?" {
+ printHelp(cmds)
+ } else if cmd == "exit" || cmd == "quit" {
+ return
+ } else {
+ foundCommand := false
+ for _, c := range Commands {
+ if c.Name() == cmd {
+ if err := c.Do(args, commandEnv, os.Stdout); err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ }
+ foundCommand = true
+ }
+ }
+ if !foundCommand {
+ fmt.Fprintf(os.Stderr, "unknown command: %v\n", cmd)
+ }
+ }
+
+ }
+ }
+}
+
+func printGenericHelp() {
+ msg :=
+ `Type: "help <command>" for help on <command>
+`
+ fmt.Print(msg)
+
+ sort.Slice(Commands, func(i, j int) bool {
+ return strings.Compare(Commands[i].Name(), Commands[j].Name()) < 0
+ })
+ for _, c := range Commands {
+ helpTexts := strings.SplitN(c.Help(), "\n", 2)
+ fmt.Printf(" %-30s\t# %s \n", c.Name(), helpTexts[0])
+ }
+}
+
+func printHelp(cmds []string) {
+ args := cmds[1:]
+ if len(args) == 0 {
+ printGenericHelp()
+ } else if len(args) > 1 {
+ fmt.Println()
+ } else {
+ cmd := strings.ToLower(args[0])
+
+ sort.Slice(Commands, func(i, j int) bool {
+ return strings.Compare(Commands[i].Name(), Commands[j].Name()) < 0
+ })
+
+ for _, c := range Commands {
+ if c.Name() == cmd {
+ fmt.Printf(" %s\t# %s\n", c.Name(), c.Help())
+ }
+ }
+ }
+}
+
+func setCompletionHandler() {
+ line.SetCompleter(func(line string) (c []string) {
+ for _, i := range Commands {
+ if strings.HasPrefix(i.Name(), strings.ToLower(line)) {
+ c = append(c, i.Name())
+ }
+ }
+ return
+ })
+}
+
+func loadHistory() {
+ if f, err := os.Open(historyPath); err == nil {
+ line.ReadHistory(f)
+ f.Close()
+ }
+}
+
+func saveHistory() {
+ if f, err := os.Create(historyPath); err != nil {
+ fmt.Printf("Error writing history file: %v\n", err)
+ } else {
+ line.WriteHistory(f)
+ f.Close()
+ }
+}