[#13] Add info FrostFS backend cmd
Signed-off-by: Aleksey Kravchenko <al.kravchenko@yadro.com>
This commit is contained in:
parent
12b572e62a
commit
141fbb783e
1 changed files with 322 additions and 0 deletions
|
@ -16,6 +16,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
"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"
|
sdkClient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
@ -40,6 +42,7 @@ func init() {
|
||||||
Name: "frostfs",
|
Name: "frostfs",
|
||||||
Description: "Distributed, decentralized object storage FrostFS",
|
Description: "Distributed, decentralized object storage FrostFS",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
|
CommandHelp: commandHelp,
|
||||||
Options: []fs.Option{
|
Options: []fs.Option{
|
||||||
{
|
{
|
||||||
Name: "endpoint",
|
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
|
// Options defines the configuration for this backend
|
||||||
type Options struct {
|
type Options struct {
|
||||||
FrostfsEndpoint string `config:"endpoint"`
|
FrostfsEndpoint string `config:"endpoint"`
|
||||||
|
@ -210,6 +266,272 @@ type Object struct {
|
||||||
timestamp time.Time
|
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
|
// Shutdown the backend, closing any background tasks and any
|
||||||
// cached connections.
|
// cached connections.
|
||||||
func (f *Fs) Shutdown(_ context.Context) error {
|
func (f *Fs) Shutdown(_ context.Context) error {
|
||||||
|
|
Loading…
Add table
Reference in a new issue