Files
seaweedfs/weed/shell/command_collection_mark.go
T
lianyu.sun 50c2bbf94c feat: add collection.mark shell command
Add collection.mark to mark all existing normal volume replicas in a collection as readonly or writable. The command runs in preview mode by default and requires -apply to execute changes. It reuses existing volume mark RPCs, supports default collection aliases, skips EC shards, and adds unit tests for option parsing and target collection logic.
2026-05-20 20:32:52 +08:00

167 lines
4.8 KiB
Go

package shell
import (
"flag"
"fmt"
"io"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
func init() {
Commands = append(Commands, &commandCollectionMark{})
}
type commandCollectionMark struct {
}
func (c *commandCollectionMark) Name() string {
return "collection.mark"
}
func (c *commandCollectionMark) Help() string {
return `Mark all existing normal volume replicas in one collection writable or readonly
collection.mark -collection <collection_name> -readonly -apply
collection.mark -collection <collection_name> -writable -apply
Use '_default' or '_default_' for the empty-named collection.
Without -apply, this command only prints the volume replicas that would be marked.
`
}
func (c *commandCollectionMark) HasTag(CommandTag) bool {
return false
}
type collectionMarkOptions struct {
collection string
markWritable bool
apply bool
}
type collectionMarkTarget struct {
volumeId needle.VolumeId
collection string
server pb.ServerAddress
readOnly bool
}
func (c *commandCollectionMark) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
opts, err := parseCollectionMarkOptions(args)
if err != nil {
return err
}
infoAboutSimulationMode(writer, opts.apply, "-apply")
if err = commandEnv.confirmIsLocked(args); err != nil {
return
}
topologyInfo, _, err := collectTopologyInfo(commandEnv, 0)
if err != nil {
return err
}
targets, skippedEC := collectCollectionMarkTargets(topologyInfo, opts.collection)
action := "readonly"
if opts.markWritable {
action = "writable"
}
for _, target := range targets {
fmt.Fprintf(writer, "volume %d collection %q on %s currentReadonly:%t will be marked %s\n",
target.volumeId, target.collection, target.server, target.readOnly, action)
}
if skippedEC > 0 {
fmt.Fprintf(writer, "Skipped %d EC shard volume(s); collection.mark only marks normal volumes.\n", skippedEC)
}
if len(targets) == 0 {
fmt.Fprintf(writer, "No normal volume replicas found for collection %q.\n", opts.collection)
return nil
}
if !opts.apply {
fmt.Fprintf(writer, "Found %d normal volume replica(s) for collection %q. Use -apply to mark them %s.\n",
len(targets), opts.collection, action)
return nil
}
for _, target := range targets {
if err := markVolumeWritable(commandEnv.option.GrpcDialOption, target.volumeId, target.server, opts.markWritable, true); err != nil {
return fmt.Errorf("mark volume %d on %s as %s: %w", target.volumeId, target.server, action, err)
}
}
fmt.Fprintf(writer, "Marked %d normal volume replica(s) in collection %q as %s.\n", len(targets), opts.collection, action)
return nil
}
func parseCollectionMarkOptions(args []string) (*collectionMarkOptions, error) {
collectionMarkCommand := flag.NewFlagSet("collection.mark", flag.ContinueOnError)
collectionName := collectionMarkCommand.String("collection", "", "collection to mark. Use '_default' or '_default_' for the empty-named collection.")
writable := collectionMarkCommand.Bool("writable", false, "collection mark writable")
readonly := collectionMarkCommand.Bool("readonly", false, "collection mark readonly")
apply := collectionMarkCommand.Bool("apply", false, "apply the collection mark")
if err := collectionMarkCommand.Parse(args); err != nil {
return nil, err
}
if *collectionName == "" {
return nil, fmt.Errorf("empty collection name is not allowed")
}
if (*writable && *readonly) || (!*writable && !*readonly) {
return nil, fmt.Errorf("use -readonly or -writable")
}
return &collectionMarkOptions{
collection: normalizeCollectionMarkName(*collectionName),
markWritable: *writable,
apply: *apply,
}, nil
}
func normalizeCollectionMarkName(collectionName string) string {
if collectionName == CollectionDefault || collectionName == "_default_" {
return ""
}
return collectionName
}
func collectCollectionMarkTargets(topo *master_pb.TopologyInfo, collection string) (targets []collectionMarkTarget, skippedEC int) {
if topo == nil {
return nil, 0
}
for _, dc := range topo.GetDataCenterInfos() {
for _, rack := range dc.GetRackInfos() {
for _, dn := range rack.GetDataNodeInfos() {
server := pb.NewServerAddressFromDataNode(dn)
for _, diskType := range sortMapKey(dn.GetDiskInfos()) {
diskInfo := dn.GetDiskInfos()[diskType]
for _, vi := range diskInfo.GetVolumeInfos() {
if vi.GetCollection() != collection {
continue
}
targets = append(targets, collectionMarkTarget{
volumeId: needle.VolumeId(vi.GetId()),
collection: vi.GetCollection(),
server: server,
readOnly: vi.GetReadOnly(),
})
}
for _, ecShardInfo := range diskInfo.GetEcShardInfos() {
if ecShardInfo.GetCollection() == collection {
skippedEC++
}
}
}
}
}
}
return targets, skippedEC
}