diff --git a/weed/shell/command_fs_merge_volumes.go b/weed/shell/command_fs_merge_volumes.go index 10653cd23..996ffecb2 100644 --- a/weed/shell/command_fs_merge_volumes.go +++ b/weed/shell/command_fs_merge_volumes.go @@ -70,10 +70,38 @@ func (c *commandFsMergeVolumes) Do(args []string, commandEnv *CommandEnv, writer dir = strings.TrimRight(dir, "/") } + // flag.Uint is a 64-bit uint on amd64 but needle.VolumeId is uint32, so a + // value that overflows (e.g. 4294967297) silently wraps to a valid id + // like 1. Reject instead of wrapping. + const maxVolumeID = uint(^uint32(0)) + if *fromVolumeArg > maxVolumeID { + return fmt.Errorf("fromVolumeId %d exceeds max volume id %d", *fromVolumeArg, maxVolumeID) + } + if *toVolumeArg > maxVolumeID { + return fmt.Errorf("toVolumeId %d exceeds max volume id %d", *toVolumeArg, maxVolumeID) + } + fromVolumeId := needle.VolumeId(*fromVolumeArg) toVolumeId := needle.VolumeId(*toVolumeArg) - c.reloadVolumesInfo(commandEnv.MasterClient) + if err = c.reloadVolumesInfo(commandEnv.MasterClient); err != nil { + return fmt.Errorf("reload volumes info: %w", err) + } + + // Reject unknown ids before createMergePlan silently produces an empty plan + // and we print just the "max volume size" header. That output is + // indistinguishable from a legitimate "nothing to merge" and hides typos, + // already-deleted volumes, and stale scripts. + if fromVolumeId != 0 { + if _, err := c.getVolumeInfoById(fromVolumeId); err != nil { + return fmt.Errorf("fromVolumeId %d not found on master", fromVolumeId) + } + } + if toVolumeId != 0 { + if _, err := c.getVolumeInfoById(toVolumeId); err != nil { + return fmt.Errorf("toVolumeId %d not found on master", toVolumeId) + } + } if fromVolumeId != 0 && toVolumeId != 0 { if fromVolumeId == toVolumeId { diff --git a/weed/shell/command_volume_fsck.go b/weed/shell/command_volume_fsck.go index 94c0a651d..b2f6ed257 100644 --- a/weed/shell/command_volume_fsck.go +++ b/weed/shell/command_volume_fsck.go @@ -14,6 +14,7 @@ import ( "os" "path" "path/filepath" + "sort" "strconv" "strings" "sync" @@ -161,8 +162,27 @@ func (c *commandVolumeFsck) Do(args []string, commandEnv *CommandEnv, writer io. return fmt.Errorf("failed to collect all volume locations: %w", err) } - if err != nil { - return fmt.Errorf("read filer buckets path: %w", err) + // If the operator specified -volumeId, reject unknown ids up front instead + // of silently filtering them out and reporting "no orphan data". A silent + // success on a missing volume looks identical to a clean volume and hides + // typos, already-deleted volumes, and stale scripts. + if len(c.volumeIds) > 0 { + known := make(map[uint32]bool) + for _, vidMap := range dataNodeVolumeIdToVInfo { + for vid := range vidMap { + known[vid] = true + } + } + var missing []uint32 + for vid := range c.volumeIds { + if !known[vid] { + missing = append(missing, vid) + } + } + if len(missing) > 0 { + sort.Slice(missing, func(i, j int) bool { return missing[i] < missing[j] }) + return fmt.Errorf("volume(s) not found on master: %v", missing) + } } var collectCutoffFromAtNs int64 = 0 diff --git a/weed/shell/command_volume_vacuum.go b/weed/shell/command_volume_vacuum.go index 2c14dcb70..34f7870d9 100644 --- a/weed/shell/command_volume_vacuum.go +++ b/weed/shell/command_volume_vacuum.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io" + "sort" "strconv" "strings" @@ -62,6 +63,41 @@ func (c *commandVacuum) Do(args []string, commandEnv *CommandEnv, writer io.Writ volumeIdInts = append(volumeIdInts, 0) } + // Reject unknown ids up front. The master's VacuumVolume RPC silently + // iterates matching volumes, so a typo or an already-deleted volume just + // returns success — making this command look like it worked when nothing + // happened. + if *volumeIds != "" { + topo, _, err := collectTopologyInfo(commandEnv, 0) + if err != nil { + return fmt.Errorf("collect topology: %w", err) + } + known := make(map[uint32]bool) + eachDataNode(topo, func(_ DataCenterId, _ RackId, dn *master_pb.DataNodeInfo) { + for _, disk := range dn.DiskInfos { + for _, vi := range disk.VolumeInfos { + known[vi.Id] = true + } + } + }) + // Dedupe via a set so "volume.vacuum -volumeId 5,5,5" on a missing + // volume 5 reports [5] once instead of [5 5 5]. + missingSet := make(map[uint32]bool) + for _, vid := range volumeIdInts { + if !known[vid] { + missingSet[vid] = true + } + } + if len(missingSet) > 0 { + missing := make([]uint32, 0, len(missingSet)) + for vid := range missingSet { + missing = append(missing, vid) + } + sort.Slice(missing, func(i, j int) bool { return missing[i] < missing[j] }) + return fmt.Errorf("volume(s) not found on master: %v", missing) + } + } + for _, volumeId := range volumeIdInts { err = commandEnv.MasterClient.WithClient(false, func(client master_pb.SeaweedClient) error { _, err = client.VacuumVolume(context.Background(), &master_pb.VacuumVolumeRequest{