[#13] Add info FrostFS backend cmd

Signed-off-by: Aleksey Kravchenko <al.kravchenko@yadro.com>
This commit is contained in:
Aleksey Kravchenko 2025-02-17 20:55:30 +03:00
parent 12b572e62a
commit 141fbb783e

View file

@ -16,6 +16,8 @@ import (
"time"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
sdkClient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -40,6 +42,7 @@ func init() {
Name: "frostfs",
Description: "Distributed, decentralized object storage FrostFS",
NewFs: NewFs,
CommandHelp: commandHelp,
Options: []fs.Option{
{
Name: "endpoint",
@ -155,6 +158,59 @@ func init() {
})
}
var commandHelp = []fs.CommandHelp{
{
Name: "info",
Short: "Show information about the FrostFS objects and containers",
Long: `This command can be used to get information about the FrostFS objects and containers.
Usage Examples:
rclone backend info frostfs:container/path/to/dir
rclone backend info frostfs:container/path/to/dir path/to/file/in/dir.txt
rclone backend info frostfs:container/path/to/dir path/to/file/in/dir.txt -o "format={cid}:{oid}"
The optional "format" flag overrides the information output. In this example, if an object is stored in
a container with the identifier "9mvN7hsUcYoGoHjxpRWtqmDipnmaeRmGVDqRxxPyy2n1" and
its own identifier is "4VPCNFsZ2SQt1GNfYw2uTBNnz5bLgC7i4k4ovtuXKyJP", the output of this command will be
"9mvN7hsucoGoHjxPqrWmDipnMaemGVDqrxxPyynn1:4VpcNFsZqsQt1Gnfw2utBnzn5Blgc7i4kvtuXyKyJp".
The default output format is the same as that of the frostfs-cli utility,
with the "container get" and "object head" options. Here is an example of output:
--- Container info ---
CID: 9mvN7hsUcYoGoHjxpRWtqmDipnmaeRmGVDqRxxPyy2n1
Owner ID: NQL7q6PvPaisWNwdWfoR1LsEsAyje8P3jX
Created: 2025-02-17 15:07:51 +0300 MSK
Attributes:
Timestamp=1739794071
Name=test
__SYSTEM__NAME=test
__SYSTEM__ZONE=container
__SYSTEM__DISABLE_HOMOMORPHIC_HASHING=true
Placement policy:
REP 3
--- Object info ---
ID: 4VPCNFsZ2SQt1GNfYw2uTBNnz5bLgC7i4k4ovtuXKyJP
CID: 9mvN7hsUcYoGoHjxpRWtqmDipnmaeRmGVDqRxxPyy2n1
Owner: NQL7q6PvPaisWNwdWfoR1LsEsAyje8P3jX
CreatedAt: 559
Size: 402905
HomoHash: <empty>
Checksum: 2a068fe24c53bc8bf7d6bbb997414f7938b080305dc45f9fd3ff684bc11fbb7b
Type: REGULAR
Attributes:
FileName=cat.png
FilePath=dir1/dir2/dir3/cat.png
Timestamp=1733410524 (2024-12-05 17:55:24 +0300 MSK)
ID signature:
public key: 026b7c7a7a16225eb13a5a733495a1bcdd1f016dfa9193498821379b0de2ba6870
signature: 049f6712c8378d323269b605a282bcacd7565ce2eefe1f10a9739c48945f739d95102c478b9cb1d429cd3330b4b5262e725392e322de3bbfa4ce18a9c842289219
`,
},
}
// Options defines the configuration for this backend
type Options struct {
FrostfsEndpoint string `config:"endpoint"`
@ -210,6 +266,272 @@ type Object struct {
timestamp time.Time
}
// Command the backend to run a named command
//
// The command run is name
// args may be used to read arguments from
// opts may be used to read optional arguments from
func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[string]string) (out interface{}, err error) {
switch name {
case "info":
return f.infoCmd(ctx, arg, opt)
default:
return nil, fs.ErrorCommandNotFound
}
}
func (f *Fs) containerInfo(ctx context.Context, cnrID cid.ID) (container.Container, error) {
prm := pool.PrmContainerGet{
ContainerID: cnrID,
}
var cnr container.Container
cnr, err := f.pool.GetContainer(ctx, prm)
if err != nil {
return container.Container{}, fmt.Errorf("couldn't get container '%s': %w", cnrID, err)
}
return cnr, err
}
func (f *Fs) getObjectsHead(ctx context.Context, cnrID cid.ID, objIDs []oid.ID) ([]object.Object, error) {
var res []object.Object
for _, objID := range objIDs {
var prmHead pool.PrmObjectHead
prmHead.SetAddress(newAddress(cnrID, objID))
obj, err := f.pool.HeadObject(ctx, prmHead)
if err != nil {
return nil, err
}
res = append(res, obj)
}
return res, nil
}
type printer struct {
w io.Writer
err error
}
func newPrinter(w io.Writer) *printer {
return &printer{w: w}
}
func (p *printer) printf(format string, a ...interface{}) {
if p.err != nil {
return
}
if _, err := fmt.Fprintf(p.w, format, a...); err != nil {
p.err = err
}
}
func (p *printer) lastError() error {
return p.err
}
func (p *printer) printContainerInfo(cnrID cid.ID, cnr container.Container) {
p.printf("CID: %v\nOwner ID: %v", cnrID, cnr.Owner())
var timestamp time.Time
var attrs []string
cnr.IterateAttributes(func(key string, value string) {
attrs = append(attrs, fmt.Sprintf(" %v=%v", key, value))
if key == object.AttributeTimestamp {
val, err := strconv.ParseInt(value, 10, 64)
if err == nil {
timestamp = time.Unix(val, 0)
}
}
})
if !timestamp.IsZero() {
p.printf("\nCreated: %v", timestamp)
}
if len(attrs) > 0 {
p.printf("\nAttributes:\n%s", strings.Join(attrs, "\n"))
}
s := bytes.NewBufferString("")
if err := cnr.PlacementPolicy().WriteStringTo(s); err != nil {
return
}
p.printf("\nPlacement policy:\n%s", s.String())
}
func (p *printer) printChecksum(name string, recv func() (checksum.Checksum, bool)) {
var strVal string
cs, csSet := recv()
if csSet {
strVal = hex.EncodeToString(cs.Value())
} else {
strVal = "<empty>"
}
p.printf("\n%s: %s", name, strVal)
}
func (p *printer) printObject(obj *object.Object) {
objIDStr := "<empty>"
cnrIDStr := objIDStr
if objID, ok := obj.ID(); ok {
objIDStr = objID.String()
}
if cnrID, ok := obj.ContainerID(); ok {
cnrIDStr = cnrID.String()
}
p.printf("\nID: %v", objIDStr)
p.printf("\nCID: %v", cnrIDStr)
p.printf("\nOwner: %s", obj.OwnerID())
p.printf("\nCreatedAt: %d", obj.CreationEpoch())
p.printf("\nSize: %d", obj.PayloadSize())
p.printChecksum("HomoHash", obj.PayloadHomomorphicHash)
p.printChecksum("Checksum", obj.PayloadChecksum)
p.printf("\nType: %s", obj.Type())
p.printf("\nAttributes:")
for _, attr := range obj.Attributes() {
if attr.Key() == object.AttributeTimestamp {
var strVal string
val, err := strconv.ParseInt(attr.Value(), 10, 64)
if err == nil {
strVal = time.Unix(val, 0).String()
} else {
strVal = "malformed"
}
p.printf("\n %s=%s (%s)",
attr.Key(),
attr.Value(),
strVal)
continue
}
p.printf("\n %s=%s", attr.Key(), attr.Value())
}
if signature := obj.Signature(); signature != nil {
p.printf("\nID signature:")
var sigV2 refs.Signature
signature.WriteToV2(&sigV2)
p.printf("\n public key: %s", hex.EncodeToString(sigV2.GetKey()))
p.printf("\n signature: %s", hex.EncodeToString(sigV2.GetSign()))
}
if ecHeader := obj.ECHeader(); ecHeader != nil {
p.printf("\nEC header:")
p.printf("\n parent object ID: %s", ecHeader.Parent().EncodeToString())
p.printf("\n index: %d", ecHeader.Index())
p.printf("\n total: %d", ecHeader.Total())
p.printf("\n header length: %d", ecHeader.HeaderLength())
}
p.printSplitHeader(obj)
}
func (p *printer) printSplitHeader(obj *object.Object) {
if splitID := obj.SplitID(); splitID != nil {
p.printf("Split ID: %s\n", splitID)
}
if objID, ok := obj.ParentID(); ok {
p.printf("Split ParentID: %s\n", objID)
}
if prev, ok := obj.PreviousID(); ok {
p.printf("\nSplit PreviousID: %s", prev)
}
for _, child := range obj.Children() {
p.printf("\nSplit ChildID: %s", child.String())
}
parent := obj.Parent()
if parent != nil {
p.printf("\n\nSplit Parent Header:")
p.printObject(parent)
}
}
func formattedInfoOutput(format string, cnrID cid.ID, objHeads []object.Object) (string, error) {
format = strings.ReplaceAll(format, "{cid}", cnrID.String())
objIDStr := "<empty>"
if len(objHeads) > 0 {
objID, ok := objHeads[0].ID()
if ok {
objIDStr = objID.String()
}
}
return strings.ReplaceAll(format, "{oid}", objIDStr), nil
}
func (f *Fs) infoCmd(ctx context.Context, arg []string, opt map[string]string) (out interface{}, err error) {
var cnrID cid.ID
if cnrID, err = f.resolveContainerID(ctx, f.rootContainer); err != nil {
return nil, err
}
var format string
for k, v := range opt {
switch k {
case "format":
format = v
default:
return nil, fmt.Errorf("unknown option \"%s\"", k)
}
}
var objIDs []oid.ID
var filePath string
if len(arg) > 0 {
filePath = strings.TrimPrefix(arg[0], "/")
if f.rootDirectory != "" {
filePath = f.rootDirectory + "/" + filePath
}
if objIDs, err = f.findObjectsFilePath(ctx, cnrID, filePath); err != nil {
return
}
}
cnr, err := f.containerInfo(ctx, cnrID)
if err != nil {
return
}
var objHeads []object.Object
if objHeads, err = f.getObjectsHead(ctx, cnrID, objIDs); err != nil {
return
}
if format != "" {
return formattedInfoOutput(format, cnrID, objHeads)
}
w := bytes.NewBufferString("")
p := newPrinter(w)
p.printf(" --- Container info ---\n")
p.printContainerInfo(cnrID, cnr)
if len(arg) > 0 {
p.printf("\n\n --- Object info ---")
if len(objHeads) > 0 {
// Print info about the first object only
p.printObject(&objHeads[0])
} else {
p.printf("\nNo object with \"%s\" file path was found", filePath)
}
}
if err := p.lastError(); err != nil {
return nil, err
}
return w.String(), nil
}
// Shutdown the backend, closing any background tasks and any
// cached connections.
func (f *Fs) Shutdown(_ context.Context) error {