weed/shell: support -noLock flag in volumeServer.evacuate (#12501) (#12516)

* weed/shell: support -noLock flag in volumeServer.evacuate (#12501)

* shell: return flag parse errors from volumeServer.evacuate

A typo such as -noLock=invalid printed the parse error but the command
still succeeded, so a piped script treated the evacuation as done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shell: volumeServer.evacuate -h prints usage without failing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-11-10 21:25:42 -08:00
1 parent 09a63031c1
commit b9866bae4f
2 files changed
+61 -2

No files matched your search

+10 -2
View File
@@ -2,6 +2,7 @@ package shell
import (
"cmp"
"errors"
"flag"
"fmt"
"io"
@@ -43,6 +44,7 @@ func (c *commandVolumeServerEvacuate) Help() string {
good destination to meet the replication requirement.
E.g. a volume replication 001 in a cluster with 2 volume servers can not be moved.
You can use "-skipNonMoveable" to move the rest volumes.
You can use "-noLock" to run without cluster-wide lock.
`
}
@@ -62,14 +64,20 @@ func (c *commandVolumeServerEvacuate) Do(args []string, commandEnv *CommandEnv,
// TODO: remove this alias
applyChangeAlias := vsEvacuateCommand.Bool("force", false, "actually apply the changes (alias for -apply)")
retryCount := vsEvacuateCommand.Int("retry", 0, "how many times to retry")
noLock := vsEvacuateCommand.Bool("noLock", false, "do not lock the admin shell at one's own risk")
if err = vsEvacuateCommand.Parse(args); err != nil {
return nil
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
handleDeprecatedForceFlag(writer, vsEvacuateCommand, applyChangeAlias, applyChange)
infoAboutSimulationMode(writer, *applyChange, "-apply")
if err = commandEnv.confirmIsLocked(args); err != nil && *applyChange {
if *noLock {
commandEnv.SetNoLock(true)
} else if err = commandEnv.confirmIsLocked(args); err != nil && *applyChange {
return
}
if *volumeServer == "" && *c.volumeRack == "" {
@@ -1,8 +1,13 @@
package shell
import (
"io"
"os"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/wdclient/exclusive_locks"
)
func TestEvacuatePrefersLowerVolumeRatio(t *testing.T) {
@@ -36,3 +41,49 @@ func TestVolumeServerEvacuate(t *testing.T) {
}
}
func TestVolumeServerEvacuateNoLockFlag(t *testing.T) {
c := &commandVolumeServerEvacuate{}
// Test without -noLock: when -apply is set and shell is unlocked, Do must fail with lock error
unlockedEnv := &CommandEnv{locker: exclusive_locks.NewExclusiveLocker(nil, cluster.AdminShellLockName)}
err := c.Do([]string{"-apply"}, unlockedEnv, io.Discard)
if err == nil {
t.Fatalf("expected error when running with -apply without lock or -noLock")
}
if !strings.Contains(err.Error(), "need to run \"lock\" first to continue") {
t.Fatalf("expected lock error message, got: %v", err)
}
// Test with -noLock: should set commandEnv.noLock to true and bypass the lock requirement
noLockEnv := &CommandEnv{locker: exclusive_locks.NewExclusiveLocker(nil, cluster.AdminShellLockName)}
errNoLock := c.Do([]string{"-noLock", "-apply"}, noLockEnv, io.Discard)
if !noLockEnv.noLock {
t.Fatalf("expected commandEnv.noLock to be true when -noLock flag is provided")
}
if err := noLockEnv.confirmIsLocked([]string{"volumeServer.evacuate"}); err != nil {
t.Fatalf("confirmIsLocked should succeed when noLock is true: %v", err)
}
// The lock error should not be returned because -noLock bypassed it; it should reach node validation
if errNoLock == nil || !strings.Contains(errNoLock.Error(), "need to specify volume server") {
t.Fatalf("expected node validation error after bypassing lock check, got: %v", errNoLock)
}
}
func TestVolumeServerEvacuateReturnsFlagParseError(t *testing.T) {
env := &CommandEnv{locker: exclusive_locks.NewExclusiveLocker(nil, cluster.AdminShellLockName)}
err := (&commandVolumeServerEvacuate{}).Do([]string{"-noLock=invalid", "-apply"}, env, io.Discard)
if err == nil || !strings.Contains(err.Error(), "invalid boolean value") {
t.Fatalf("expected a flag parse error, got: %v", err)
}
if env.noLock {
t.Fatalf("a rejected -noLock must not unlock the shell")
}
}
func TestVolumeServerEvacuateHelpIsNotAnError(t *testing.T) {
env := &CommandEnv{locker: exclusive_locks.NewExclusiveLocker(nil, cluster.AdminShellLockName)}
if err := (&commandVolumeServerEvacuate{}).Do([]string{"-h"}, env, io.Discard); err != nil {
t.Fatalf("-h should print usage without failing, got: %v", err)
}
}