diff --git a/weed/shell/command_volume_server_evacuate.go b/weed/shell/command_volume_server_evacuate.go index cd62bf0eda..287fa10346 100644 --- a/weed/shell/command_volume_server_evacuate.go +++ b/weed/shell/command_volume_server_evacuate.go @@ -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 == "" { diff --git a/weed/shell/command_volume_server_evacuate_test.go b/weed/shell/command_volume_server_evacuate_test.go index ba03323987..7621cf14a2 100644 --- a/weed/shell/command_volume_server_evacuate_test.go +++ b/weed/shell/command_volume_server_evacuate_test.go @@ -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) + } +}