[#1416] lens/explorer: Support metabase schema v3

Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
Aleksey Savchuk 2024-11-11 12:33:31 +03:00 committed by Evgenii Stratonikov
parent 5ba0e2918e
commit bf8914fedc
11 changed files with 233 additions and 29 deletions

View file

@ -2,13 +2,17 @@ package meta
import ( import (
"context" "context"
"encoding/binary"
"errors"
"fmt" "fmt"
common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal" common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal"
schemaCommon "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
schema "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase" schema "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/tui" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/tui"
"github.com/rivo/tview" "github.com/rivo/tview"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.etcd.io/bbolt"
) )
var tuiCMD = &cobra.Command{ var tuiCMD = &cobra.Command{
@ -27,6 +31,11 @@ Available search filters:
var initialPrompt string var initialPrompt string
var parserPerSchemaVersion = map[uint64]schemaCommon.Parser{
2: schema.MetabaseParserV2,
3: schema.MetabaseParserV3,
}
func init() { func init() {
common.AddComponentPathFlag(tuiCMD, &vPath) common.AddComponentPathFlag(tuiCMD, &vPath)
@ -49,12 +58,22 @@ func runTUI(cmd *cobra.Command) error {
} }
defer db.Close() defer db.Close()
schemaVersion, hasVersion := lookupSchemaVersion(cmd, db)
if !hasVersion {
return errors.New("couldn't detect schema version")
}
metabaseParser, ok := parserPerSchemaVersion[schemaVersion]
if !ok {
return fmt.Errorf("unknown schema version %d", schemaVersion)
}
// Need if app was stopped with Ctrl-C. // Need if app was stopped with Ctrl-C.
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
defer cancel() defer cancel()
app := tview.NewApplication() app := tview.NewApplication()
ui := tui.NewUI(ctx, app, db, schema.MetabaseParser, nil) ui := tui.NewUI(ctx, app, db, metabaseParser, nil)
_ = ui.AddFilter("cid", tui.CIDParser, "CID") _ = ui.AddFilter("cid", tui.CIDParser, "CID")
_ = ui.AddFilter("oid", tui.OIDParser, "OID") _ = ui.AddFilter("oid", tui.OIDParser, "OID")
@ -69,3 +88,31 @@ func runTUI(cmd *cobra.Command) error {
app.SetRoot(ui, true).SetFocus(ui) app.SetRoot(ui, true).SetFocus(ui)
return app.Run() return app.Run()
} }
var (
shardInfoBucket = []byte{5}
versionRecord = []byte("version")
)
func lookupSchemaVersion(cmd *cobra.Command, db *bbolt.DB) (version uint64, ok bool) {
err := db.View(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(shardInfoBucket)
if bkt == nil {
return nil
}
rec := bkt.Get(versionRecord)
if rec == nil {
return nil
}
version = binary.LittleEndian.Uint64(rec)
ok = true
return nil
})
if err != nil {
common.ExitOnErr(cmd, fmt.Errorf("couldn't lookup version: %w", err))
}
return
}

View file

@ -80,10 +80,15 @@ var (
}, },
) )
UserAttributeParser = NewUserAttributeKeyBucketParser( UserAttributeParserV2 = NewUserAttributeKeyBucketParser(
NewUserAttributeValueBucketParser(records.UserAttributeRecordParser), NewUserAttributeValueBucketParser(records.UserAttributeRecordParser),
) )
UserAttributeParserV3 = NewUserAttributeKeyBucketParserWithSpecificKeys(
NewUserAttributeValueBucketParser(records.UserAttributeRecordParser),
[]string{"FilePath", "S3-Access-Box-CRDT-Name"},
)
PayloadHashParser = NewPrefixContainerBucketParser(PayloadHash, records.PayloadHashRecordParser, Resolvers{ PayloadHashParser = NewPrefixContainerBucketParser(PayloadHash, records.PayloadHashRecordParser, Resolvers{
cidResolver: StrictResolver, cidResolver: StrictResolver,
oidResolver: StrictResolver, oidResolver: StrictResolver,
@ -108,4 +113,14 @@ var (
cidResolver: StrictResolver, cidResolver: StrictResolver,
oidResolver: LenientResolver, oidResolver: LenientResolver,
}) })
ExpirationEpochToObjectParser = NewPrefixBucketParser(ExpirationEpochToObject, records.ExpirationEpochToObjectRecordParser, Resolvers{
cidResolver: LenientResolver,
oidResolver: LenientResolver,
})
ObjectToExpirationEpochParser = NewPrefixContainerBucketParser(ObjectToExpirationEpoch, records.ObjectToExpirationEpochRecordParser, Resolvers{
cidResolver: StrictResolver,
oidResolver: LenientResolver,
})
) )

View file

@ -22,6 +22,8 @@ const (
Split Split
ContainerCounters ContainerCounters
ECInfo ECInfo
ExpirationEpochToObject
ObjectToExpirationEpoch
) )
var x = map[Prefix]string{ var x = map[Prefix]string{
@ -43,6 +45,8 @@ var x = map[Prefix]string{
Split: "Split", Split: "Split",
ContainerCounters: "Container Counters", ContainerCounters: "Container Counters",
ECInfo: "EC Info", ECInfo: "EC Info",
ExpirationEpochToObject: "Exp. Epoch to Object",
ObjectToExpirationEpoch: "Object to Exp. Epoch",
} }
func (p Prefix) String() string { func (p Prefix) String() string {

View file

@ -9,7 +9,7 @@ import (
func (b *PrefixBucket) String() string { func (b *PrefixBucket) String() string {
return common.FormatSimple( return common.FormatSimple(
fmt.Sprintf("(%2d %-18s)", b.prefix, b.prefix), tcell.ColorLime, fmt.Sprintf("(%2d %-20s)", b.prefix, b.prefix), tcell.ColorLime,
) )
} }
@ -17,7 +17,7 @@ func (b *PrefixContainerBucket) String() string {
return fmt.Sprintf( return fmt.Sprintf(
"%s CID %s", "%s CID %s",
common.FormatSimple( common.FormatSimple(
fmt.Sprintf("(%2d %-18s)", b.prefix, b.prefix), tcell.ColorLime, fmt.Sprintf("(%2d %-20s)", b.prefix, b.prefix), tcell.ColorLime,
), ),
common.FormatSimple(b.id.String(), tcell.ColorAqua), common.FormatSimple(b.id.String(), tcell.ColorAqua),
) )
@ -34,7 +34,7 @@ func (b *ContainerBucket) String() string {
func (b *UserAttributeKeyBucket) String() string { func (b *UserAttributeKeyBucket) String() string {
return fmt.Sprintf("%s CID %s ATTR-KEY %s", return fmt.Sprintf("%s CID %s ATTR-KEY %s",
common.FormatSimple( common.FormatSimple(
fmt.Sprintf("(%2d %-18s)", b.prefix, b.prefix), tcell.ColorLime, fmt.Sprintf("(%2d %-20s)", b.prefix, b.prefix), tcell.ColorLime,
), ),
common.FormatSimple( common.FormatSimple(
fmt.Sprintf("%-44s", b.id), tcell.ColorAqua, fmt.Sprintf("%-44s", b.id), tcell.ColorAqua,

View file

@ -2,6 +2,7 @@ package buckets
import ( import (
"errors" "errors"
"slices"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -61,6 +62,7 @@ var (
ErrInvalidKeyLength = errors.New("invalid key length") ErrInvalidKeyLength = errors.New("invalid key length")
ErrInvalidValueLength = errors.New("invalid value length") ErrInvalidValueLength = errors.New("invalid value length")
ErrInvalidPrefix = errors.New("invalid prefix") ErrInvalidPrefix = errors.New("invalid prefix")
ErrUnexpectedAttributeKey = errors.New("unexpected attribute key")
) )
func NewPrefixBucketParser(prefix Prefix, next common.Parser, resolvers Resolvers) common.Parser { func NewPrefixBucketParser(prefix Prefix, next common.Parser, resolvers Resolvers) common.Parser {
@ -132,6 +134,10 @@ func NewContainerBucketParser(next common.Parser, resolvers Resolvers) common.Pa
} }
func NewUserAttributeKeyBucketParser(next common.Parser) common.Parser { func NewUserAttributeKeyBucketParser(next common.Parser) common.Parser {
return NewUserAttributeKeyBucketParserWithSpecificKeys(next, nil)
}
func NewUserAttributeKeyBucketParserWithSpecificKeys(next common.Parser, keys []string) common.Parser {
return func(key, value []byte) (common.SchemaEntry, common.Parser, error) { return func(key, value []byte) (common.SchemaEntry, common.Parser, error) {
if value != nil { if value != nil {
return nil, nil, ErrNotBucket return nil, nil, ErrNotBucket
@ -147,6 +153,11 @@ func NewUserAttributeKeyBucketParser(next common.Parser) common.Parser {
return nil, nil, err return nil, nil, err
} }
b.key = string(key[33:]) b.key = string(key[33:])
if len(keys) != 0 && !slices.Contains(keys, b.key) {
return nil, nil, ErrUnexpectedAttributeKey
}
return &b, next, nil return &b, next, nil
} }
} }

View file

@ -5,7 +5,30 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase/buckets" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase/buckets"
) )
var MetabaseParser = common.WithFallback( var MetabaseParserV3 = common.WithFallback(
common.Any(
buckets.GraveyardParser,
buckets.GarbageParser,
buckets.ContainerVolumeParser,
buckets.LockedParser,
buckets.ShardInfoParser,
buckets.PrimaryParser,
buckets.LockersParser,
buckets.TombstoneParser,
buckets.SmallParser,
buckets.RootParser,
buckets.UserAttributeParserV3,
buckets.ParentParser,
buckets.SplitParser,
buckets.ContainerCountersParser,
buckets.ECInfoParser,
buckets.ExpirationEpochToObjectParser,
buckets.ObjectToExpirationEpochParser,
),
common.RawParser.ToFallbackParser(),
)
var MetabaseParserV2 = common.WithFallback(
common.Any( common.Any(
buckets.GraveyardParser, buckets.GraveyardParser,
buckets.GarbageParser, buckets.GarbageParser,
@ -18,7 +41,7 @@ var MetabaseParser = common.WithFallback(
buckets.SmallParser, buckets.SmallParser,
buckets.RootParser, buckets.RootParser,
buckets.OwnerParser, buckets.OwnerParser,
buckets.UserAttributeParser, buckets.UserAttributeParserV2,
buckets.PayloadHashParser, buckets.PayloadHashParser,
buckets.ParentParser, buckets.ParentParser,
buckets.SplitParser, buckets.SplitParser,

View file

@ -63,3 +63,11 @@ func (r *ContainerCountersRecord) DetailedString() string {
func (r *ECInfoRecord) DetailedString() string { func (r *ECInfoRecord) DetailedString() string {
return spew.Sdump(*r) return spew.Sdump(*r)
} }
func (r *ExpirationEpochToObjectRecord) DetailedString() string {
return spew.Sdump(*r)
}
func (r *ObjectToExpirationEpochRecord) DetailedString() string {
return spew.Sdump(*r)
}

View file

@ -143,3 +143,26 @@ func (r *ECInfoRecord) Filter(typ string, val any) common.FilterResult {
return common.No return common.No
} }
} }
func (r *ExpirationEpochToObjectRecord) Filter(typ string, val any) common.FilterResult {
switch typ {
case "cid":
id := val.(cid.ID)
return common.IfThenElse(r.cnt.Equals(id), common.Yes, common.No)
case "oid":
id := val.(oid.ID)
return common.IfThenElse(r.obj.Equals(id), common.Yes, common.No)
default:
return common.No
}
}
func (r *ObjectToExpirationEpochRecord) Filter(typ string, val any) common.FilterResult {
switch typ {
case "oid":
id := val.(oid.ID)
return common.IfThenElse(r.obj.Equals(id), common.Yes, common.No)
default:
return common.No
}
}

View file

@ -249,3 +249,45 @@ func ECInfoRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, e
} }
return &r, nil, nil return &r, nil, nil
} }
func ExpirationEpochToObjectRecordParser(key, _ []byte) (common.SchemaEntry, common.Parser, error) {
if len(key) != 72 {
return nil, nil, ErrInvalidKeyLength
}
var (
r ExpirationEpochToObjectRecord
err error
)
r.epoch = binary.BigEndian.Uint64(key[:8])
if err = r.cnt.Decode(key[8:40]); err != nil {
return nil, nil, err
}
if err = r.obj.Decode(key[40:]); err != nil {
return nil, nil, err
}
return &r, nil, nil
}
func ObjectToExpirationEpochRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
if len(key) != 32 {
return nil, nil, ErrInvalidKeyLength
}
if len(value) != 8 {
return nil, nil, ErrInvalidValueLength
}
var (
r ObjectToExpirationEpochRecord
err error
)
if err = r.obj.Decode(key); err != nil {
return nil, nil, err
}
r.epoch = binary.LittleEndian.Uint64(value)
return &r, nil, nil
}

View file

@ -2,6 +2,7 @@ package records
import ( import (
"fmt" "fmt"
"strconv"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -133,3 +134,22 @@ func (r *ECInfoRecord) String() string {
len(r.ids), len(r.ids),
) )
} }
func (r *ExpirationEpochToObjectRecord) String() string {
return fmt.Sprintf(
"exp. epoch %s %c CID %s OID %s",
common.FormatSimple(fmt.Sprintf("%-20d", r.epoch), tcell.ColorAqua),
tview.Borders.Vertical,
common.FormatSimple(fmt.Sprintf("%-44s", r.cnt), tcell.ColorAqua),
common.FormatSimple(fmt.Sprintf("%-44s", r.obj), tcell.ColorAqua),
)
}
func (r *ObjectToExpirationEpochRecord) String() string {
return fmt.Sprintf(
"OID %s %c exp. epoch %s",
common.FormatSimple(fmt.Sprintf("%-44s", r.obj), tcell.ColorAqua),
tview.Borders.Vertical,
common.FormatSimple(strconv.FormatUint(r.epoch, 10), tcell.ColorAqua),
)
}

View file

@ -79,4 +79,15 @@ type (
id oid.ID id oid.ID
ids []oid.ID ids []oid.ID
} }
ExpirationEpochToObjectRecord struct {
epoch uint64
cnt cid.ID
obj oid.ID
}
ObjectToExpirationEpochRecord struct {
obj oid.ID
epoch uint64
}
) )