Move to frostfs-node
Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
This commit is contained in:
parent
42554a9298
commit
923f84722a
934 changed files with 3470 additions and 3451 deletions
75
cmd/frostfs-cli/docs/sessions.md
Normal file
75
cmd/frostfs-cli/docs/sessions.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# How NeoFS CLI uses session mechanism of the NeoFS
|
||||
|
||||
## Overview
|
||||
|
||||
NeoFS sessions implement a mechanism for issuing a power of attorney by one
|
||||
party to another. A trusted party can provide a so-called session token as
|
||||
proof of the right to act on behalf of another member of the network. The
|
||||
client of operations carried out with such a token will be the user who opened
|
||||
the session. The token contains information which limits power of attorney like
|
||||
action context or lifetime.
|
||||
|
||||
The client confirms trust in a third party by signing its public (session) key
|
||||
with his private key. Any operation signed using private session key with
|
||||
attached session token is treated as performed by the original client.
|
||||
|
||||
## Types
|
||||
|
||||
NeoFS CLI supports two ways to execute operation within a session depending on
|
||||
whether the user of the command application is an original user (1) or a trusted
|
||||
one (2).
|
||||
|
||||
### Dynamic
|
||||
|
||||
For case (1) CLI user can only open dynamic sessions. Protocol call
|
||||
`SessionService.Create` is used for this purpose. As a result of the call, a
|
||||
private session key will be generated on the server, thus making the remote
|
||||
server trusted. This type of session is useful when the client needs to
|
||||
transfer part of the responsibility for the formation of strict system elements
|
||||
to the trusted server. At the moment, the approach is applicable only to
|
||||
creating objects.
|
||||
|
||||
```shell
|
||||
$ frostfs-cli session create --rpc-endpoint <server_ip> --out ./blank_token
|
||||
```
|
||||
After this example command remote node holds session private key while its
|
||||
public part is written into the session token encoded into the output file.
|
||||
Later this token can be attached to the operations which support dynamic
|
||||
sessions. Then the token will be finally formed and signed by CLI itself.
|
||||
|
||||
### Static
|
||||
|
||||
For case (2) CLI user can act on behalf of the person who issued the session
|
||||
token to him. Unlike (1) the token must be fully prepared on the side of the
|
||||
original client, and the CLI uses it only for reading. Ready token MUST have:
|
||||
- correct context (object, container, etc.)
|
||||
- valid lifetime
|
||||
- public session key corresponding to the CLI key
|
||||
- valid client signature
|
||||
|
||||
To sign the session token, exec:
|
||||
```shell
|
||||
$ frostfs-cli --wallet <client_wallet> util sign session-token --from ./blank_token --to ./token
|
||||
```
|
||||
Once the token is signed, it MUST NOT be modified.
|
||||
|
||||
## Commands
|
||||
|
||||
### Object
|
||||
|
||||
Here are sub-commands of `object` command which support only dynamic sessions (1):
|
||||
- `put`
|
||||
- `delete`
|
||||
- `lock`
|
||||
|
||||
These commands accept blank token of the dynamically opened session or open
|
||||
session internally if it has not been opened yet.
|
||||
|
||||
All other `object` sub-commands support only static sessions (2).
|
||||
|
||||
### Container
|
||||
|
||||
List of commands supporting sessions (static only):
|
||||
- `create`
|
||||
- `delete`
|
||||
- `set-eacl`
|
892
cmd/frostfs-cli/internal/client/client.go
Normal file
892
cmd/frostfs-cli/internal/client/client.go
Normal file
|
@ -0,0 +1,892 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
containerSDK "github.com/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/version"
|
||||
)
|
||||
|
||||
// BalanceOfPrm groups parameters of BalanceOf operation.
|
||||
type BalanceOfPrm struct {
|
||||
commonPrm
|
||||
client.PrmBalanceGet
|
||||
}
|
||||
|
||||
// BalanceOfRes groups the resulting values of BalanceOf operation.
|
||||
type BalanceOfRes struct {
|
||||
cliRes *client.ResBalanceGet
|
||||
}
|
||||
|
||||
// Balance returns the current balance.
|
||||
func (x BalanceOfRes) Balance() accounting.Decimal {
|
||||
return x.cliRes.Amount()
|
||||
}
|
||||
|
||||
// BalanceOf requests the current balance of a NeoFS user.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func BalanceOf(prm BalanceOfPrm) (res BalanceOfRes, err error) {
|
||||
res.cliRes, err = prm.cli.BalanceGet(context.Background(), prm.PrmBalanceGet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ListContainersPrm groups parameters of ListContainers operation.
|
||||
type ListContainersPrm struct {
|
||||
commonPrm
|
||||
client.PrmContainerList
|
||||
}
|
||||
|
||||
// ListContainersRes groups the resulting values of ListContainers operation.
|
||||
type ListContainersRes struct {
|
||||
cliRes *client.ResContainerList
|
||||
}
|
||||
|
||||
// IDList returns list of identifiers of user's containers.
|
||||
func (x ListContainersRes) IDList() []cid.ID {
|
||||
return x.cliRes.Containers()
|
||||
}
|
||||
|
||||
// ListContainers requests a list of NeoFS user's containers.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func ListContainers(prm ListContainersPrm) (res ListContainersRes, err error) {
|
||||
res.cliRes, err = prm.cli.ContainerList(context.Background(), prm.PrmContainerList)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PutContainerPrm groups parameters of PutContainer operation.
|
||||
type PutContainerPrm struct {
|
||||
commonPrm
|
||||
client.PrmContainerPut
|
||||
}
|
||||
|
||||
// PutContainerRes groups the resulting values of PutContainer operation.
|
||||
type PutContainerRes struct {
|
||||
cnr cid.ID
|
||||
}
|
||||
|
||||
// ID returns identifier of the created container.
|
||||
func (x PutContainerRes) ID() cid.ID {
|
||||
return x.cnr
|
||||
}
|
||||
|
||||
// PutContainer sends a request to save the container in NeoFS.
|
||||
//
|
||||
// Operation is asynchronous and not guaranteed even in the absence of errors.
|
||||
// The required time is also not predictable.
|
||||
//
|
||||
// Success can be verified by reading by identifier.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func PutContainer(prm PutContainerPrm) (res PutContainerRes, err error) {
|
||||
cliRes, err := prm.cli.ContainerPut(context.Background(), prm.PrmContainerPut)
|
||||
if err == nil {
|
||||
res.cnr = cliRes.ID()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetContainerPrm groups parameters of GetContainer operation.
|
||||
type GetContainerPrm struct {
|
||||
commonPrm
|
||||
cliPrm client.PrmContainerGet
|
||||
}
|
||||
|
||||
// SetContainer sets identifier of the container to be read.
|
||||
func (x *GetContainerPrm) SetContainer(id cid.ID) {
|
||||
x.cliPrm.SetContainer(id)
|
||||
}
|
||||
|
||||
// GetContainerRes groups the resulting values of GetContainer operation.
|
||||
type GetContainerRes struct {
|
||||
cliRes *client.ResContainerGet
|
||||
}
|
||||
|
||||
// Container returns structured of the requested container.
|
||||
func (x GetContainerRes) Container() containerSDK.Container {
|
||||
return x.cliRes.Container()
|
||||
}
|
||||
|
||||
// GetContainer reads a container from NeoFS by ID.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func GetContainer(prm GetContainerPrm) (res GetContainerRes, err error) {
|
||||
res.cliRes, err = prm.cli.ContainerGet(context.Background(), prm.cliPrm)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsACLExtendable checks if ACL of the container referenced by the given identifier
|
||||
// can be extended. Client connection MUST BE correctly established in advance.
|
||||
func IsACLExtendable(c *client.Client, cnr cid.ID) (bool, error) {
|
||||
var prm GetContainerPrm
|
||||
prm.SetClient(c)
|
||||
prm.SetContainer(cnr)
|
||||
|
||||
res, err := GetContainer(prm)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("get container from the NeoFS: %w", err)
|
||||
}
|
||||
|
||||
return res.Container().BasicACL().Extendable(), nil
|
||||
}
|
||||
|
||||
// DeleteContainerPrm groups parameters of DeleteContainerPrm operation.
|
||||
type DeleteContainerPrm struct {
|
||||
commonPrm
|
||||
client.PrmContainerDelete
|
||||
}
|
||||
|
||||
// DeleteContainerRes groups the resulting values of DeleteContainer operation.
|
||||
type DeleteContainerRes struct{}
|
||||
|
||||
// DeleteContainer sends a request to remove a container from NeoFS by ID.
|
||||
//
|
||||
// Operation is asynchronous and not guaranteed even in the absence of errors.
|
||||
// The required time is also not predictable.
|
||||
//
|
||||
// Success can be verified by reading by identifier.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func DeleteContainer(prm DeleteContainerPrm) (res DeleteContainerRes, err error) {
|
||||
_, err = prm.cli.ContainerDelete(context.Background(), prm.PrmContainerDelete)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// EACLPrm groups parameters of EACL operation.
|
||||
type EACLPrm struct {
|
||||
commonPrm
|
||||
client.PrmContainerEACL
|
||||
}
|
||||
|
||||
// EACLRes groups the resulting values of EACL operation.
|
||||
type EACLRes struct {
|
||||
cliRes *client.ResContainerEACL
|
||||
}
|
||||
|
||||
// EACL returns requested eACL table.
|
||||
func (x EACLRes) EACL() eacl.Table {
|
||||
return x.cliRes.Table()
|
||||
}
|
||||
|
||||
// EACL reads eACL table from NeoFS by container ID.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func EACL(prm EACLPrm) (res EACLRes, err error) {
|
||||
res.cliRes, err = prm.cli.ContainerEACL(context.Background(), prm.PrmContainerEACL)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetEACLPrm groups parameters of SetEACL operation.
|
||||
type SetEACLPrm struct {
|
||||
commonPrm
|
||||
client.PrmContainerSetEACL
|
||||
}
|
||||
|
||||
// SetEACLRes groups the resulting values of SetEACL operation.
|
||||
type SetEACLRes struct{}
|
||||
|
||||
// SetEACL requests to save an eACL table in NeoFS.
|
||||
//
|
||||
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
||||
// The required time is also not predictable.
|
||||
//
|
||||
// Success can be verified by reading by container identifier.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func SetEACL(prm SetEACLPrm) (res SetEACLRes, err error) {
|
||||
_, err = prm.cli.ContainerSetEACL(context.Background(), prm.PrmContainerSetEACL)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NetworkInfoPrm groups parameters of NetworkInfo operation.
|
||||
type NetworkInfoPrm struct {
|
||||
commonPrm
|
||||
client.PrmNetworkInfo
|
||||
}
|
||||
|
||||
// NetworkInfoRes groups the resulting values of NetworkInfo operation.
|
||||
type NetworkInfoRes struct {
|
||||
cliRes *client.ResNetworkInfo
|
||||
}
|
||||
|
||||
// NetworkInfo returns structured information about the NeoFS network.
|
||||
func (x NetworkInfoRes) NetworkInfo() netmap.NetworkInfo {
|
||||
return x.cliRes.Info()
|
||||
}
|
||||
|
||||
// NetworkInfo reads information about the NeoFS network.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func NetworkInfo(prm NetworkInfoPrm) (res NetworkInfoRes, err error) {
|
||||
res.cliRes, err = prm.cli.NetworkInfo(context.Background(), prm.PrmNetworkInfo)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NodeInfoPrm groups parameters of NodeInfo operation.
|
||||
type NodeInfoPrm struct {
|
||||
commonPrm
|
||||
client.PrmEndpointInfo
|
||||
}
|
||||
|
||||
// NodeInfoRes groups the resulting values of NodeInfo operation.
|
||||
type NodeInfoRes struct {
|
||||
cliRes *client.ResEndpointInfo
|
||||
}
|
||||
|
||||
// NodeInfo returns information about the node from netmap.
|
||||
func (x NodeInfoRes) NodeInfo() netmap.NodeInfo {
|
||||
return x.cliRes.NodeInfo()
|
||||
}
|
||||
|
||||
// LatestVersion returns the latest NeoFS API version in use.
|
||||
func (x NodeInfoRes) LatestVersion() version.Version {
|
||||
return x.cliRes.LatestVersion()
|
||||
}
|
||||
|
||||
// NodeInfo requests information about the remote server from NeoFS netmap.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func NodeInfo(prm NodeInfoPrm) (res NodeInfoRes, err error) {
|
||||
res.cliRes, err = prm.cli.EndpointInfo(context.Background(), prm.PrmEndpointInfo)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NetMapSnapshotPrm groups parameters of NetMapSnapshot operation.
|
||||
type NetMapSnapshotPrm struct {
|
||||
commonPrm
|
||||
}
|
||||
|
||||
// NetMapSnapshotRes groups the resulting values of NetMapSnapshot operation.
|
||||
type NetMapSnapshotRes struct {
|
||||
cliRes *client.ResNetMapSnapshot
|
||||
}
|
||||
|
||||
// NetMap returns current local snapshot of the NeoFS network map.
|
||||
func (x NetMapSnapshotRes) NetMap() netmap.NetMap {
|
||||
return x.cliRes.NetMap()
|
||||
}
|
||||
|
||||
// NetMapSnapshot requests current network view of the remote server.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func NetMapSnapshot(prm NetMapSnapshotPrm) (res NetMapSnapshotRes, err error) {
|
||||
res.cliRes, err = prm.cli.NetMapSnapshot(context.Background(), client.PrmNetMapSnapshot{})
|
||||
return
|
||||
}
|
||||
|
||||
// CreateSessionPrm groups parameters of CreateSession operation.
|
||||
type CreateSessionPrm struct {
|
||||
commonPrm
|
||||
client.PrmSessionCreate
|
||||
}
|
||||
|
||||
// CreateSessionRes groups the resulting values of CreateSession operation.
|
||||
type CreateSessionRes struct {
|
||||
cliRes *client.ResSessionCreate
|
||||
}
|
||||
|
||||
// ID returns session identifier.
|
||||
func (x CreateSessionRes) ID() []byte {
|
||||
return x.cliRes.ID()
|
||||
}
|
||||
|
||||
// SessionKey returns public session key in a binary format.
|
||||
func (x CreateSessionRes) SessionKey() []byte {
|
||||
return x.cliRes.PublicKey()
|
||||
}
|
||||
|
||||
// CreateSession opens a new unlimited session with the remote node.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func CreateSession(prm CreateSessionPrm) (res CreateSessionRes, err error) {
|
||||
res.cliRes, err = prm.cli.SessionCreate(context.Background(), prm.PrmSessionCreate)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PutObjectPrm groups parameters of PutObject operation.
|
||||
type PutObjectPrm struct {
|
||||
commonObjectPrm
|
||||
|
||||
hdr *object.Object
|
||||
|
||||
rdr io.Reader
|
||||
|
||||
headerCallback func(*object.Object)
|
||||
}
|
||||
|
||||
// SetHeader sets object header.
|
||||
func (x *PutObjectPrm) SetHeader(hdr *object.Object) {
|
||||
x.hdr = hdr
|
||||
}
|
||||
|
||||
// SetPayloadReader sets reader of the object payload.
|
||||
func (x *PutObjectPrm) SetPayloadReader(rdr io.Reader) {
|
||||
x.rdr = rdr
|
||||
}
|
||||
|
||||
// SetHeaderCallback sets callback which is called on the object after the header is received
|
||||
// but before the payload is written.
|
||||
func (x *PutObjectPrm) SetHeaderCallback(f func(*object.Object)) {
|
||||
x.headerCallback = f
|
||||
}
|
||||
|
||||
// PutObjectRes groups the resulting values of PutObject operation.
|
||||
type PutObjectRes struct {
|
||||
id oid.ID
|
||||
}
|
||||
|
||||
// ID returns identifier of the created object.
|
||||
func (x PutObjectRes) ID() oid.ID {
|
||||
return x.id
|
||||
}
|
||||
|
||||
// PutObject saves the object in NeoFS network.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func PutObject(prm PutObjectPrm) (*PutObjectRes, error) {
|
||||
var putPrm client.PrmObjectPutInit
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
putPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
putPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
if prm.local {
|
||||
putPrm.MarkLocal()
|
||||
}
|
||||
|
||||
putPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
wrt, err := prm.cli.ObjectPutInit(context.Background(), putPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init object writing: %w", err)
|
||||
}
|
||||
|
||||
if wrt.WriteHeader(*prm.hdr) {
|
||||
if prm.headerCallback != nil {
|
||||
prm.headerCallback(prm.hdr)
|
||||
}
|
||||
|
||||
sz := prm.hdr.PayloadSize()
|
||||
|
||||
if data := prm.hdr.Payload(); len(data) > 0 {
|
||||
if prm.rdr != nil {
|
||||
prm.rdr = io.MultiReader(bytes.NewReader(data), prm.rdr)
|
||||
} else {
|
||||
prm.rdr = bytes.NewReader(data)
|
||||
sz = uint64(len(data))
|
||||
}
|
||||
}
|
||||
|
||||
if prm.rdr != nil {
|
||||
// TODO: (neofs-node#1198) explore better values or configure it
|
||||
const defaultBufferSizePut = 4096
|
||||
|
||||
if sz == 0 || sz > defaultBufferSizePut {
|
||||
sz = defaultBufferSizePut
|
||||
}
|
||||
|
||||
buf := make([]byte, sz)
|
||||
|
||||
var n int
|
||||
|
||||
for {
|
||||
n, err = prm.rdr.Read(buf)
|
||||
if n > 0 {
|
||||
if !wrt.WritePayloadChunk(buf[:n]) {
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("read payload: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cliRes, err := wrt.Close()
|
||||
if err != nil { // here err already carries both status and client errors
|
||||
return nil, fmt.Errorf("client failure: %w", err)
|
||||
}
|
||||
|
||||
return &PutObjectRes{
|
||||
id: cliRes.StoredObjectID(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteObjectPrm groups parameters of DeleteObject operation.
|
||||
type DeleteObjectPrm struct {
|
||||
commonObjectPrm
|
||||
objectAddressPrm
|
||||
}
|
||||
|
||||
// DeleteObjectRes groups the resulting values of DeleteObject operation.
|
||||
type DeleteObjectRes struct {
|
||||
tomb oid.ID
|
||||
}
|
||||
|
||||
// Tombstone returns the ID of the created object with tombstone.
|
||||
func (x DeleteObjectRes) Tombstone() oid.ID {
|
||||
return x.tomb
|
||||
}
|
||||
|
||||
// DeleteObject marks an object to be removed from NeoFS through tombstone placement.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func DeleteObject(prm DeleteObjectPrm) (*DeleteObjectRes, error) {
|
||||
var delPrm client.PrmObjectDelete
|
||||
delPrm.FromContainer(prm.objAddr.Container())
|
||||
delPrm.ByID(prm.objAddr.Object())
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
delPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
delPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
delPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
cliRes, err := prm.cli.ObjectDelete(context.Background(), delPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("remove object via client: %w", err)
|
||||
}
|
||||
|
||||
return &DeleteObjectRes{
|
||||
tomb: cliRes.Tombstone(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetObjectPrm groups parameters of GetObject operation.
|
||||
type GetObjectPrm struct {
|
||||
commonObjectPrm
|
||||
objectAddressPrm
|
||||
rawPrm
|
||||
payloadWriterPrm
|
||||
headerCallback func(*object.Object)
|
||||
}
|
||||
|
||||
// SetHeaderCallback sets callback which is called on the object after the header is received
|
||||
// but before the payload is written.
|
||||
func (p *GetObjectPrm) SetHeaderCallback(f func(*object.Object)) {
|
||||
p.headerCallback = f
|
||||
}
|
||||
|
||||
// GetObjectRes groups the resulting values of GetObject operation.
|
||||
type GetObjectRes struct {
|
||||
hdr *object.Object
|
||||
}
|
||||
|
||||
// Header returns the header of the request object.
|
||||
func (x GetObjectRes) Header() *object.Object {
|
||||
return x.hdr
|
||||
}
|
||||
|
||||
// GetObject reads an object by address.
|
||||
//
|
||||
// Interrupts on any writer error. If successful, payload is written to the writer.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
||||
func GetObject(prm GetObjectPrm) (*GetObjectRes, error) {
|
||||
var getPrm client.PrmObjectGet
|
||||
getPrm.FromContainer(prm.objAddr.Container())
|
||||
getPrm.ByID(prm.objAddr.Object())
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
getPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
getPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
if prm.raw {
|
||||
getPrm.MarkRaw()
|
||||
}
|
||||
|
||||
if prm.local {
|
||||
getPrm.MarkLocal()
|
||||
}
|
||||
|
||||
getPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
rdr, err := prm.cli.ObjectGetInit(context.Background(), getPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init object reading on client: %w", err)
|
||||
}
|
||||
|
||||
var hdr object.Object
|
||||
|
||||
if !rdr.ReadHeader(&hdr) {
|
||||
_, err = rdr.Close()
|
||||
return nil, fmt.Errorf("read object header: %w", err)
|
||||
}
|
||||
if prm.headerCallback != nil {
|
||||
prm.headerCallback(&hdr)
|
||||
}
|
||||
|
||||
_, err = io.Copy(prm.wrt, rdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copy payload: %w", err)
|
||||
}
|
||||
|
||||
return &GetObjectRes{
|
||||
hdr: &hdr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HeadObjectPrm groups parameters of HeadObject operation.
|
||||
type HeadObjectPrm struct {
|
||||
commonObjectPrm
|
||||
objectAddressPrm
|
||||
rawPrm
|
||||
|
||||
mainOnly bool
|
||||
}
|
||||
|
||||
// SetMainOnlyFlag sets flag to get only main fields of an object header in terms of NeoFS API.
|
||||
func (x *HeadObjectPrm) SetMainOnlyFlag(v bool) {
|
||||
x.mainOnly = v
|
||||
}
|
||||
|
||||
// HeadObjectRes groups the resulting values of HeadObject operation.
|
||||
type HeadObjectRes struct {
|
||||
hdr *object.Object
|
||||
}
|
||||
|
||||
// Header returns the requested object header.
|
||||
func (x HeadObjectRes) Header() *object.Object {
|
||||
return x.hdr
|
||||
}
|
||||
|
||||
// HeadObject reads an object header by address.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
||||
func HeadObject(prm HeadObjectPrm) (*HeadObjectRes, error) {
|
||||
var cliPrm client.PrmObjectHead
|
||||
cliPrm.FromContainer(prm.objAddr.Container())
|
||||
cliPrm.ByID(prm.objAddr.Object())
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
cliPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
if prm.raw {
|
||||
cliPrm.MarkRaw()
|
||||
}
|
||||
|
||||
if prm.local {
|
||||
cliPrm.MarkLocal()
|
||||
}
|
||||
|
||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
res, err := prm.cli.ObjectHead(context.Background(), cliPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read object header via client: %w", err)
|
||||
}
|
||||
|
||||
var hdr object.Object
|
||||
|
||||
if !res.ReadHeader(&hdr) {
|
||||
return nil, fmt.Errorf("missing header in response")
|
||||
}
|
||||
|
||||
return &HeadObjectRes{
|
||||
hdr: &hdr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchObjectsPrm groups parameters of SearchObjects operation.
|
||||
type SearchObjectsPrm struct {
|
||||
commonObjectPrm
|
||||
containerIDPrm
|
||||
|
||||
filters object.SearchFilters
|
||||
}
|
||||
|
||||
// SetFilters sets search filters.
|
||||
func (x *SearchObjectsPrm) SetFilters(filters object.SearchFilters) {
|
||||
x.filters = filters
|
||||
}
|
||||
|
||||
// SearchObjectsRes groups the resulting values of SearchObjects operation.
|
||||
type SearchObjectsRes struct {
|
||||
ids []oid.ID
|
||||
}
|
||||
|
||||
// IDList returns identifiers of the matched objects.
|
||||
func (x SearchObjectsRes) IDList() []oid.ID {
|
||||
return x.ids
|
||||
}
|
||||
|
||||
// SearchObjects selects objects from the container which match the filters.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
func SearchObjects(prm SearchObjectsPrm) (*SearchObjectsRes, error) {
|
||||
var cliPrm client.PrmObjectSearch
|
||||
cliPrm.InContainer(prm.cnrID)
|
||||
cliPrm.SetFilters(prm.filters)
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
cliPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
if prm.local {
|
||||
cliPrm.MarkLocal()
|
||||
}
|
||||
|
||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
rdr, err := prm.cli.ObjectSearchInit(context.Background(), cliPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init object search: %w", err)
|
||||
}
|
||||
|
||||
buf := make([]oid.ID, 10)
|
||||
var list []oid.ID
|
||||
var n int
|
||||
var ok bool
|
||||
|
||||
for {
|
||||
n, ok = rdr.Read(buf)
|
||||
for i := 0; i < n; i++ {
|
||||
list = append(list, buf[i])
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_, err = rdr.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read object list: %w", err)
|
||||
}
|
||||
|
||||
return &SearchObjectsRes{
|
||||
ids: list,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HashPayloadRangesPrm groups parameters of HashPayloadRanges operation.
|
||||
type HashPayloadRangesPrm struct {
|
||||
commonObjectPrm
|
||||
objectAddressPrm
|
||||
|
||||
tz bool
|
||||
|
||||
rngs []*object.Range
|
||||
|
||||
salt []byte
|
||||
}
|
||||
|
||||
// TZ sets flag to request Tillich-Zemor hashes.
|
||||
func (x *HashPayloadRangesPrm) TZ() {
|
||||
x.tz = true
|
||||
}
|
||||
|
||||
// SetRanges sets a list of payload ranges to hash.
|
||||
func (x *HashPayloadRangesPrm) SetRanges(rngs []*object.Range) {
|
||||
x.rngs = rngs
|
||||
}
|
||||
|
||||
// SetSalt sets data for each range to be XOR'ed with.
|
||||
func (x *HashPayloadRangesPrm) SetSalt(salt []byte) {
|
||||
x.salt = salt
|
||||
}
|
||||
|
||||
// HashPayloadRangesRes groups the resulting values of HashPayloadRanges operation.
|
||||
type HashPayloadRangesRes struct {
|
||||
cliRes *client.ResObjectHash
|
||||
}
|
||||
|
||||
// HashList returns a list of hashes of the payload ranges keeping order.
|
||||
func (x HashPayloadRangesRes) HashList() [][]byte {
|
||||
return x.cliRes.Checksums()
|
||||
}
|
||||
|
||||
// HashPayloadRanges requests hashes (by default SHA256) of the object payload ranges.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
// Returns an error if number of received hashes differs with the number of requested ranges.
|
||||
func HashPayloadRanges(prm HashPayloadRangesPrm) (*HashPayloadRangesRes, error) {
|
||||
var cliPrm client.PrmObjectHash
|
||||
cliPrm.FromContainer(prm.objAddr.Container())
|
||||
cliPrm.ByID(prm.objAddr.Object())
|
||||
|
||||
if prm.local {
|
||||
cliPrm.MarkLocal()
|
||||
}
|
||||
|
||||
cliPrm.UseSalt(prm.salt)
|
||||
|
||||
rngs := make([]uint64, 2*len(prm.rngs))
|
||||
|
||||
for i := range prm.rngs {
|
||||
rngs[2*i] = prm.rngs[i].GetOffset()
|
||||
rngs[2*i+1] = prm.rngs[i].GetLength()
|
||||
}
|
||||
|
||||
cliPrm.SetRangeList(rngs...)
|
||||
|
||||
if prm.tz {
|
||||
cliPrm.TillichZemorAlgo()
|
||||
}
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
cliPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
res, err := prm.cli.ObjectHash(context.Background(), cliPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read payload hashes via client: %w", err)
|
||||
}
|
||||
|
||||
return &HashPayloadRangesRes{
|
||||
cliRes: res,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PayloadRangePrm groups parameters of PayloadRange operation.
|
||||
type PayloadRangePrm struct {
|
||||
commonObjectPrm
|
||||
objectAddressPrm
|
||||
rawPrm
|
||||
payloadWriterPrm
|
||||
|
||||
rng *object.Range
|
||||
}
|
||||
|
||||
// SetRange sets payload range to read.
|
||||
func (x *PayloadRangePrm) SetRange(rng *object.Range) {
|
||||
x.rng = rng
|
||||
}
|
||||
|
||||
// PayloadRangeRes groups the resulting values of PayloadRange operation.
|
||||
type PayloadRangeRes struct{}
|
||||
|
||||
// PayloadRange reads object payload range from NeoFS and writes it to the specified writer.
|
||||
//
|
||||
// Interrupts on any writer error.
|
||||
//
|
||||
// Returns any error which prevented the operation from completing correctly in error return.
|
||||
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
||||
func PayloadRange(prm PayloadRangePrm) (*PayloadRangeRes, error) {
|
||||
var cliPrm client.PrmObjectRange
|
||||
cliPrm.FromContainer(prm.objAddr.Container())
|
||||
cliPrm.ByID(prm.objAddr.Object())
|
||||
|
||||
if prm.sessionToken != nil {
|
||||
cliPrm.WithinSession(*prm.sessionToken)
|
||||
}
|
||||
|
||||
if prm.bearerToken != nil {
|
||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||
}
|
||||
|
||||
if prm.raw {
|
||||
cliPrm.MarkRaw()
|
||||
}
|
||||
|
||||
if prm.local {
|
||||
cliPrm.MarkLocal()
|
||||
}
|
||||
|
||||
cliPrm.SetOffset(prm.rng.GetOffset())
|
||||
cliPrm.SetLength(prm.rng.GetLength())
|
||||
|
||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||
|
||||
rdr, err := prm.cli.ObjectRangeInit(context.Background(), cliPrm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init payload reading: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(prm.wrt, rdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copy payload: %w", err)
|
||||
}
|
||||
|
||||
return new(PayloadRangeRes), nil
|
||||
}
|
||||
|
||||
// SyncContainerPrm groups parameters of SyncContainerSettings operation.
|
||||
type SyncContainerPrm struct {
|
||||
commonPrm
|
||||
c *containerSDK.Container
|
||||
}
|
||||
|
||||
// SetContainer sets a container that is required to be synced.
|
||||
func (s *SyncContainerPrm) SetContainer(c *containerSDK.Container) {
|
||||
s.c = c
|
||||
}
|
||||
|
||||
// SyncContainerRes groups resulting values of SyncContainerSettings
|
||||
// operation.
|
||||
type SyncContainerRes struct{}
|
||||
|
||||
// SyncContainerSettings reads global network config from NeoFS and
|
||||
// syncs container settings with it.
|
||||
//
|
||||
// Interrupts on any writer error.
|
||||
//
|
||||
// Panics if a container passed as a parameter is nil.
|
||||
func SyncContainerSettings(prm SyncContainerPrm) (*SyncContainerRes, error) {
|
||||
if prm.c == nil {
|
||||
panic("sync container settings with the network: nil container")
|
||||
}
|
||||
|
||||
err := client.SyncContainerWithNetwork(context.Background(), prm.c, prm.cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return new(SyncContainerRes), nil
|
||||
}
|
12
cmd/frostfs-cli/internal/client/doc.go
Normal file
12
cmd/frostfs-cli/internal/client/doc.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Package internal provides functionality for NeoFS CLI application communication with NeoFS network.
|
||||
//
|
||||
// The base client for accessing remote nodes via NeoFS API is a NeoFS SDK Go API client.
|
||||
// However, although it encapsulates a useful piece of business logic (e.g. the signature mechanism),
|
||||
// the NeoFS CLI application does not fully use the client's flexible interface.
|
||||
//
|
||||
// In this regard, this package provides functions over base API client necessary for the application.
|
||||
// This allows you to concentrate the entire spectrum of the client's use in one place (this will be convenient
|
||||
// both when updating the base client and for evaluating the UX of SDK library). So it is expected that all
|
||||
// application packages will be limited to this package for the development of functionality requiring
|
||||
// NeoFS API communication.
|
||||
package internal
|
92
cmd/frostfs-cli/internal/client/prm.go
Normal file
92
cmd/frostfs-cli/internal/client/prm.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
||||
)
|
||||
|
||||
// here are small structures with public setters to share between parameter structures
|
||||
|
||||
type commonPrm struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
// SetClient sets the base client for NeoFS API communication.
|
||||
func (x *commonPrm) SetClient(cli *client.Client) {
|
||||
x.cli = cli
|
||||
}
|
||||
|
||||
type containerIDPrm struct {
|
||||
cnrID cid.ID
|
||||
}
|
||||
|
||||
// SetContainerID sets the container identifier.
|
||||
func (x *containerIDPrm) SetContainerID(id cid.ID) {
|
||||
x.cnrID = id
|
||||
}
|
||||
|
||||
type bearerTokenPrm struct {
|
||||
bearerToken *bearer.Token
|
||||
}
|
||||
|
||||
// SetBearerToken sets the bearer token to be attached to the request.
|
||||
func (x *bearerTokenPrm) SetBearerToken(tok *bearer.Token) {
|
||||
x.bearerToken = tok
|
||||
}
|
||||
|
||||
type objectAddressPrm struct {
|
||||
objAddr oid.Address
|
||||
}
|
||||
|
||||
func (x *objectAddressPrm) SetAddress(addr oid.Address) {
|
||||
x.objAddr = addr
|
||||
}
|
||||
|
||||
type rawPrm struct {
|
||||
raw bool
|
||||
}
|
||||
|
||||
// SetRawFlag sets flag of raw request.
|
||||
func (x *rawPrm) SetRawFlag(raw bool) {
|
||||
x.raw = raw
|
||||
}
|
||||
|
||||
type payloadWriterPrm struct {
|
||||
wrt io.Writer
|
||||
}
|
||||
|
||||
// SetPayloadWriter sets the writer of the object payload.
|
||||
func (x *payloadWriterPrm) SetPayloadWriter(wrt io.Writer) {
|
||||
x.wrt = wrt
|
||||
}
|
||||
|
||||
type commonObjectPrm struct {
|
||||
commonPrm
|
||||
bearerTokenPrm
|
||||
|
||||
sessionToken *session.Object
|
||||
|
||||
local bool
|
||||
|
||||
xHeaders []string
|
||||
}
|
||||
|
||||
// SetTTL sets request TTL value.
|
||||
func (x *commonObjectPrm) SetTTL(ttl uint32) {
|
||||
x.local = ttl < 2
|
||||
}
|
||||
|
||||
// SetXHeaders sets request X-Headers.
|
||||
func (x *commonObjectPrm) SetXHeaders(hs []string) {
|
||||
x.xHeaders = hs
|
||||
}
|
||||
|
||||
// SetSessionToken sets the token of the session within which the request should be sent.
|
||||
func (x *commonObjectPrm) SetSessionToken(tok *session.Object) {
|
||||
x.sessionToken = tok
|
||||
}
|
95
cmd/frostfs-cli/internal/client/sdk.go
Normal file
95
cmd/frostfs-cli/internal/client/sdk.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/network"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var errInvalidEndpoint = errors.New("provided RPC endpoint is incorrect")
|
||||
|
||||
// GetSDKClientByFlag returns default frostfs-sdk-go client using the specified flag for the address.
|
||||
// On error, outputs to stderr of cmd and exits with non-zero code.
|
||||
func GetSDKClientByFlag(cmd *cobra.Command, key *ecdsa.PrivateKey, endpointFlag string) *client.Client {
|
||||
cli, err := getSDKClientByFlag(key, endpointFlag)
|
||||
if err != nil {
|
||||
common.ExitOnErr(cmd, "can't create API client: %w", err)
|
||||
}
|
||||
return cli
|
||||
}
|
||||
|
||||
func getSDKClientByFlag(key *ecdsa.PrivateKey, endpointFlag string) (*client.Client, error) {
|
||||
var addr network.Address
|
||||
|
||||
err := addr.FromString(viper.GetString(endpointFlag))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", errInvalidEndpoint, err)
|
||||
}
|
||||
return GetSDKClient(key, addr)
|
||||
}
|
||||
|
||||
// GetSDKClient returns default frostfs-sdk-go client.
|
||||
func GetSDKClient(key *ecdsa.PrivateKey, addr network.Address) (*client.Client, error) {
|
||||
var (
|
||||
c client.Client
|
||||
prmInit client.PrmInit
|
||||
prmDial client.PrmDial
|
||||
)
|
||||
|
||||
prmInit.SetDefaultPrivateKey(*key)
|
||||
prmInit.ResolveNeoFSFailures()
|
||||
prmDial.SetServerURI(addr.URIAddr())
|
||||
if timeout := viper.GetDuration(commonflags.Timeout); timeout > 0 {
|
||||
// In CLI we can only set a timeout for the whole operation.
|
||||
// By also setting stream timeout we ensure that no operation hands
|
||||
// for too long.
|
||||
prmDial.SetTimeout(timeout)
|
||||
prmDial.SetStreamTimeout(timeout)
|
||||
|
||||
common.PrintVerbose("Set request timeout to %s.", timeout)
|
||||
}
|
||||
|
||||
c.Init(prmInit)
|
||||
|
||||
if err := c.Dial(prmDial); err != nil {
|
||||
return nil, fmt.Errorf("can't init SDK client: %w", err)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// GetCurrentEpoch returns current epoch.
|
||||
func GetCurrentEpoch(ctx context.Context, endpoint string) (uint64, error) {
|
||||
var addr network.Address
|
||||
|
||||
if err := addr.FromString(endpoint); err != nil {
|
||||
return 0, fmt.Errorf("can't parse RPC endpoint: %w", err)
|
||||
}
|
||||
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("can't generate key to sign query: %w", err)
|
||||
}
|
||||
|
||||
c, err := GetSDKClient(key, addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ni, err := c.NetworkInfo(ctx, client.PrmNetworkInfo{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ni.Info().CurrentEpoch(), nil
|
||||
}
|
49
cmd/frostfs-cli/internal/common/eacl.go
Normal file
49
cmd/frostfs-cli/internal/common/eacl.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/core/version"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
versionSDK "github.com/TrueCloudLab/frostfs-sdk-go/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var errUnsupportedEACLFormat = errors.New("unsupported eACL format")
|
||||
|
||||
// ReadEACL reads extended ACL table from eaclPath.
|
||||
func ReadEACL(cmd *cobra.Command, eaclPath string) *eacl.Table {
|
||||
_, err := os.Stat(eaclPath) // check if `eaclPath` is an existing file
|
||||
if err != nil {
|
||||
ExitOnErr(cmd, "", errors.New("incorrect path to file with EACL"))
|
||||
}
|
||||
|
||||
PrintVerbose("Reading EACL from file: %s", eaclPath)
|
||||
|
||||
data, err := os.ReadFile(eaclPath)
|
||||
ExitOnErr(cmd, "can't read file with EACL: %w", err)
|
||||
|
||||
table := eacl.NewTable()
|
||||
|
||||
if err = table.UnmarshalJSON(data); err == nil {
|
||||
validateAndFixEACLVersion(table)
|
||||
PrintVerbose("Parsed JSON encoded EACL table")
|
||||
return table
|
||||
}
|
||||
|
||||
if err = table.Unmarshal(data); err == nil {
|
||||
validateAndFixEACLVersion(table)
|
||||
PrintVerbose("Parsed binary encoded EACL table")
|
||||
return table
|
||||
}
|
||||
|
||||
ExitOnErr(cmd, "", errUnsupportedEACLFormat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAndFixEACLVersion(table *eacl.Table) {
|
||||
if !version.IsValid(table.Version()) {
|
||||
table.SetVersion(versionSDK.Current())
|
||||
}
|
||||
}
|
28
cmd/frostfs-cli/internal/common/epoch.go
Normal file
28
cmd/frostfs-cli/internal/common/epoch.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ParseEpoch parses epoch argument. Second return value is true if
|
||||
// the specified epoch is relative, and false otherwise.
|
||||
func ParseEpoch(cmd *cobra.Command, flag string) (uint64, bool, error) {
|
||||
s, _ := cmd.Flags().GetString(flag)
|
||||
if len(s) == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
relative := s[0] == '+'
|
||||
if relative {
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
epoch, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, relative, fmt.Errorf("can't parse epoch for %s argument: %w", flag, err)
|
||||
}
|
||||
return epoch, relative, nil
|
||||
}
|
50
cmd/frostfs-cli/internal/common/exit.go
Normal file
50
cmd/frostfs-cli/internal/common/exit.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
sdkstatus "github.com/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ExitOnErr prints error and exits with a code that matches
|
||||
// one of the common errors from sdk library. If no errors
|
||||
// found, exits with 1 code.
|
||||
// Does nothing if passed error in nil.
|
||||
func ExitOnErr(cmd *cobra.Command, errFmt string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if errFmt != "" {
|
||||
err = fmt.Errorf(errFmt, err)
|
||||
}
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
internal
|
||||
aclDenied
|
||||
)
|
||||
|
||||
var (
|
||||
code int
|
||||
|
||||
internalErr = new(sdkstatus.ServerInternal)
|
||||
accessErr = new(sdkstatus.ObjectAccessDenied)
|
||||
)
|
||||
|
||||
switch {
|
||||
case errors.As(err, &internalErr):
|
||||
code = internal
|
||||
case errors.As(err, &accessErr):
|
||||
code = aclDenied
|
||||
err = fmt.Errorf("%w: %s", err, accessErr.Reason())
|
||||
default:
|
||||
code = internal
|
||||
}
|
||||
|
||||
cmd.PrintErrln(err)
|
||||
os.Exit(code)
|
||||
}
|
23
cmd/frostfs-cli/internal/common/json.go
Normal file
23
cmd/frostfs-cli/internal/common/json.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PrettyPrintJSON prints m as an indented JSON to the cmd output.
|
||||
func PrettyPrintJSON(cmd *cobra.Command, m json.Marshaler, entity string) {
|
||||
data, err := m.MarshalJSON()
|
||||
if err != nil {
|
||||
PrintVerbose("Can't convert %s to json: %w", entity, err)
|
||||
return
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := json.Indent(buf, data, "", " "); err != nil {
|
||||
PrintVerbose("Can't pretty print json: %w", err)
|
||||
return
|
||||
}
|
||||
cmd.Println(buf)
|
||||
}
|
49
cmd/frostfs-cli/internal/common/netmap.go
Normal file
49
cmd/frostfs-cli/internal/common/netmap.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PrettyPrintNodeInfo print information about network node with given indent and index.
|
||||
// To avoid printing attribute list use short parameter.
|
||||
func PrettyPrintNodeInfo(cmd *cobra.Command, node netmap.NodeInfo,
|
||||
index int, indent string, short bool) {
|
||||
var strState string
|
||||
|
||||
switch {
|
||||
default:
|
||||
strState = "STATE_UNSUPPORTED"
|
||||
case node.IsOnline():
|
||||
strState = "ONLINE"
|
||||
case node.IsOffline():
|
||||
strState = "OFFLINE"
|
||||
case node.IsMaintenance():
|
||||
strState = "MAINTENANCE"
|
||||
}
|
||||
|
||||
cmd.Printf("%sNode %d: %s %s ", indent, index+1, hex.EncodeToString(node.PublicKey()), strState)
|
||||
|
||||
netmap.IterateNetworkEndpoints(node, func(endpoint string) {
|
||||
cmd.Printf("%s ", endpoint)
|
||||
})
|
||||
cmd.Println()
|
||||
|
||||
if !short {
|
||||
node.IterateAttributes(func(key, value string) {
|
||||
cmd.Printf("%s\t%s: %s\n", indent, key, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// PrettyPrintNetMap print information about network map.
|
||||
func PrettyPrintNetMap(cmd *cobra.Command, nm netmap.NetMap) {
|
||||
cmd.Println("Epoch:", nm.Epoch())
|
||||
|
||||
nodes := nm.Nodes()
|
||||
for i := range nodes {
|
||||
PrettyPrintNodeInfo(cmd, nodes[i], i, "", false)
|
||||
}
|
||||
}
|
66
cmd/frostfs-cli/internal/common/token.go
Normal file
66
cmd/frostfs-cli/internal/common/token.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ReadBearerToken reads bearer token from the path provided in a specified flag.
|
||||
func ReadBearerToken(cmd *cobra.Command, flagname string) *bearer.Token {
|
||||
path, err := cmd.Flags().GetString(flagname)
|
||||
ExitOnErr(cmd, "", err)
|
||||
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
PrintVerbose("Reading bearer token from file [%s]...", path)
|
||||
|
||||
var tok bearer.Token
|
||||
|
||||
err = ReadBinaryOrJSON(&tok, path)
|
||||
ExitOnErr(cmd, "invalid bearer token: %v", err)
|
||||
|
||||
return &tok
|
||||
}
|
||||
|
||||
// BinaryOrJSON is an interface of entities which provide json.Unmarshaler
|
||||
// and NeoFS binary decoder.
|
||||
type BinaryOrJSON interface {
|
||||
Unmarshal([]byte) error
|
||||
json.Unmarshaler
|
||||
}
|
||||
|
||||
// ReadBinaryOrJSON reads file data using provided path and decodes
|
||||
// BinaryOrJSON from the data.
|
||||
func ReadBinaryOrJSON(dst BinaryOrJSON, fPath string) error {
|
||||
PrintVerbose("Reading file [%s]...", fPath)
|
||||
|
||||
// try to read session token from file
|
||||
data, err := os.ReadFile(fPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read file <%s>: %w", fPath, err)
|
||||
}
|
||||
|
||||
PrintVerbose("Trying to decode binary...")
|
||||
|
||||
err = dst.Unmarshal(data)
|
||||
if err != nil {
|
||||
PrintVerbose("Failed to decode binary: %v", err)
|
||||
|
||||
PrintVerbose("Trying to decode JSON...")
|
||||
|
||||
err = dst.UnmarshalJSON(data)
|
||||
if err != nil {
|
||||
PrintVerbose("Failed to decode JSON: %v", err)
|
||||
return errors.New("invalid format")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
47
cmd/frostfs-cli/internal/common/verbose.go
Normal file
47
cmd/frostfs-cli/internal/common/verbose.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// PrintVerbose prints to the stdout if the commonflags.Verbose flag is on.
|
||||
func PrintVerbose(format string, a ...interface{}) {
|
||||
if viper.GetBool(commonflags.Verbose) {
|
||||
fmt.Printf(format+"\n", a...)
|
||||
}
|
||||
}
|
||||
|
||||
// PrettyPrintUnixTime interprets s as unix timestamp and prints it as
|
||||
// a date. Is s is invalid, "malformed" is returned.
|
||||
func PrettyPrintUnixTime(s string) string {
|
||||
unixTime, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return "malformed"
|
||||
}
|
||||
|
||||
timestamp := time.Unix(unixTime, 0)
|
||||
|
||||
return timestamp.String()
|
||||
}
|
||||
|
||||
// PrintChecksum prints checksum.
|
||||
func PrintChecksum(cmd *cobra.Command, name string, recv func() (checksum.Checksum, bool)) {
|
||||
var strVal string
|
||||
|
||||
cs, csSet := recv()
|
||||
if csSet {
|
||||
strVal = hex.EncodeToString(cs.Value())
|
||||
} else {
|
||||
strVal = "<empty>"
|
||||
}
|
||||
|
||||
cmd.Printf("%s: %s\n", name, strVal)
|
||||
}
|
33
cmd/frostfs-cli/internal/commonflags/api.go
Normal file
33
cmd/frostfs-cli/internal/commonflags/api.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package commonflags
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
TTL = "ttl"
|
||||
TTLShorthand = ""
|
||||
TTLDefault = 2
|
||||
TTLUsage = "TTL value in request meta header"
|
||||
|
||||
XHeadersKey = "xhdr"
|
||||
XHeadersShorthand = "x"
|
||||
XHeadersUsage = "Request X-Headers in form of Key=Value"
|
||||
)
|
||||
|
||||
// InitAPI inits common flags for storage node services.
|
||||
func InitAPI(cmd *cobra.Command) {
|
||||
ff := cmd.Flags()
|
||||
|
||||
ff.StringSliceP(XHeadersKey, XHeadersShorthand, []string{}, XHeadersUsage)
|
||||
ff.Uint32P(TTL, TTLShorthand, TTLDefault, TTLUsage)
|
||||
}
|
||||
|
||||
// BindAPI binds API flags of storage node services to the viper.
|
||||
func BindAPI(cmd *cobra.Command) {
|
||||
ff := cmd.Flags()
|
||||
|
||||
_ = viper.BindPFlag(TTL, ff.Lookup(TTL))
|
||||
_ = viper.BindPFlag(XHeadersKey, ff.Lookup(XHeadersKey))
|
||||
}
|
9
cmd/frostfs-cli/internal/commonflags/expiration.go
Normal file
9
cmd/frostfs-cli/internal/commonflags/expiration.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package commonflags
|
||||
|
||||
const (
|
||||
// ExpireAt is a flag for setting last epoch of an object or a token.
|
||||
ExpireAt = "expire-at"
|
||||
// Lifetime is a flag for setting the lifetime of an object or a token,
|
||||
// starting from the current epoch.
|
||||
Lifetime = "lifetime"
|
||||
)
|
84
cmd/frostfs-cli/internal/commonflags/flags.go
Normal file
84
cmd/frostfs-cli/internal/commonflags/flags.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package commonflags
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Common CLI flag keys, shorthands, default
|
||||
// values and their usage descriptions.
|
||||
const (
|
||||
GenerateKey = "generate-key"
|
||||
generateKeyShorthand = "g"
|
||||
generateKeyDefault = false
|
||||
generateKeyUsage = "Generate new private key"
|
||||
|
||||
WalletPath = "wallet"
|
||||
WalletPathShorthand = "w"
|
||||
WalletPathDefault = ""
|
||||
WalletPathUsage = "Path to the wallet or binary key"
|
||||
|
||||
Account = "address"
|
||||
AccountShorthand = ""
|
||||
AccountDefault = ""
|
||||
AccountUsage = "Address of wallet account"
|
||||
|
||||
RPC = "rpc-endpoint"
|
||||
RPCShorthand = "r"
|
||||
RPCDefault = ""
|
||||
RPCUsage = "Remote node address (as 'multiaddr' or '<host>:<port>')"
|
||||
|
||||
Timeout = "timeout"
|
||||
TimeoutShorthand = "t"
|
||||
TimeoutDefault = 15 * time.Second
|
||||
TimeoutUsage = "Timeout for an operation"
|
||||
|
||||
Verbose = "verbose"
|
||||
VerboseShorthand = "v"
|
||||
VerboseUsage = "Verbose output"
|
||||
|
||||
ForceFlag = "force"
|
||||
ForceFlagShorthand = "f"
|
||||
|
||||
CIDFlag = "cid"
|
||||
CIDFlagUsage = "Container ID."
|
||||
|
||||
OIDFlag = "oid"
|
||||
OIDFlagUsage = "Object ID."
|
||||
)
|
||||
|
||||
// Init adds common flags to the command:
|
||||
// - GenerateKey,
|
||||
// - WalletPath,
|
||||
// - Account,
|
||||
// - RPC,
|
||||
// - Timeout.
|
||||
func Init(cmd *cobra.Command) {
|
||||
InitWithoutRPC(cmd)
|
||||
|
||||
ff := cmd.Flags()
|
||||
ff.StringP(RPC, RPCShorthand, RPCDefault, RPCUsage)
|
||||
ff.DurationP(Timeout, TimeoutShorthand, TimeoutDefault, TimeoutUsage)
|
||||
}
|
||||
|
||||
// InitWithoutRPC is similar to Init but doesn't create the RPC flag.
|
||||
func InitWithoutRPC(cmd *cobra.Command) {
|
||||
ff := cmd.Flags()
|
||||
|
||||
ff.BoolP(GenerateKey, generateKeyShorthand, generateKeyDefault, generateKeyUsage)
|
||||
ff.StringP(WalletPath, WalletPathShorthand, WalletPathDefault, WalletPathUsage)
|
||||
ff.StringP(Account, AccountShorthand, AccountDefault, AccountUsage)
|
||||
}
|
||||
|
||||
// Bind binds common command flags to the viper.
|
||||
func Bind(cmd *cobra.Command) {
|
||||
ff := cmd.Flags()
|
||||
|
||||
_ = viper.BindPFlag(GenerateKey, ff.Lookup(GenerateKey))
|
||||
_ = viper.BindPFlag(WalletPath, ff.Lookup(WalletPath))
|
||||
_ = viper.BindPFlag(Account, ff.Lookup(Account))
|
||||
_ = viper.BindPFlag(RPC, ff.Lookup(RPC))
|
||||
_ = viper.BindPFlag(Timeout, ff.Lookup(Timeout))
|
||||
}
|
3
cmd/frostfs-cli/internal/commonflags/json.go
Normal file
3
cmd/frostfs-cli/internal/commonflags/json.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package commonflags
|
||||
|
||||
const JSON = "json"
|
19
cmd/frostfs-cli/internal/commonflags/session.go
Normal file
19
cmd/frostfs-cli/internal/commonflags/session.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package commonflags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const SessionToken = "session"
|
||||
|
||||
// InitSession registers SessionToken flag representing filepath to the token
|
||||
// of the session with the given name. Supports NeoFS-binary and JSON files.
|
||||
func InitSession(cmd *cobra.Command, name string) {
|
||||
cmd.Flags().String(
|
||||
SessionToken,
|
||||
"",
|
||||
fmt.Sprintf("Filepath to a JSON- or binary-encoded token of the %s session", name),
|
||||
)
|
||||
}
|
119
cmd/frostfs-cli/internal/key/key_test.go
Normal file
119
cmd/frostfs-cli/internal/key/key_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/nspcc-dev/neo-go/cli/input"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func Test_getOrGenerate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
wallPath := filepath.Join(dir, "wallet.json")
|
||||
w, err := wallet.NewWallet(wallPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
badWallPath := filepath.Join(dir, "bad_wallet.json")
|
||||
require.NoError(t, os.WriteFile(badWallPath, []byte("bad content"), os.ModePerm))
|
||||
|
||||
acc1, err := wallet.NewAccount()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, acc1.Encrypt("pass", keys.NEP2ScryptParams()))
|
||||
w.AddAccount(acc1)
|
||||
|
||||
acc2, err := wallet.NewAccount()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, acc2.Encrypt("pass", keys.NEP2ScryptParams()))
|
||||
acc2.Default = true
|
||||
w.AddAccount(acc2)
|
||||
require.NoError(t, w.Save())
|
||||
|
||||
keyPath := filepath.Join(dir, "binary.key")
|
||||
rawKey, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(keyPath, rawKey.Bytes(), os.ModePerm))
|
||||
|
||||
wifKey, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
nep2Key, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
nep2, err := keys.NEP2Encrypt(nep2Key, "pass", keys.NEP2ScryptParams())
|
||||
require.NoError(t, err)
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
input.Terminal = term.NewTerminal(input.ReadWriter{
|
||||
Reader: in,
|
||||
Writer: io.Discard,
|
||||
}, "")
|
||||
|
||||
checkKeyError(t, filepath.Join(dir, "badfile"), ErrFs)
|
||||
checkKeyError(t, badWallPath, ErrInvalidKey)
|
||||
|
||||
t.Run("wallet", func(t *testing.T) {
|
||||
checkKeyError(t, wallPath, ErrInvalidPassword)
|
||||
|
||||
in.WriteString("invalid\r")
|
||||
checkKeyError(t, wallPath, ErrInvalidPassword)
|
||||
|
||||
in.WriteString("pass\r")
|
||||
checkKey(t, wallPath, acc2.PrivateKey()) // default account
|
||||
|
||||
viper.Set(commonflags.Account, acc1.Address)
|
||||
in.WriteString("pass\r")
|
||||
checkKey(t, wallPath, acc1.PrivateKey())
|
||||
|
||||
viper.Set(commonflags.Account, "not an address")
|
||||
checkKeyError(t, wallPath, ErrInvalidAddress)
|
||||
|
||||
acc, err := wallet.NewAccount()
|
||||
require.NoError(t, err)
|
||||
viper.Set(commonflags.Account, acc.Address)
|
||||
checkKeyError(t, wallPath, ErrInvalidAddress)
|
||||
})
|
||||
|
||||
t.Run("WIF", func(t *testing.T) {
|
||||
checkKeyError(t, wifKey.WIF(), ErrFs)
|
||||
})
|
||||
|
||||
t.Run("NEP-2", func(t *testing.T) {
|
||||
checkKeyError(t, nep2, ErrFs)
|
||||
})
|
||||
|
||||
t.Run("raw key", func(t *testing.T) {
|
||||
checkKey(t, keyPath, rawKey)
|
||||
})
|
||||
|
||||
t.Run("generate", func(t *testing.T) {
|
||||
viper.Set(commonflags.GenerateKey, true)
|
||||
actual, err := getOrGenerate()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, actual)
|
||||
for _, p := range []*keys.PrivateKey{nep2Key, rawKey, wifKey, acc1.PrivateKey(), acc2.PrivateKey()} {
|
||||
require.NotEqual(t, p, actual, "expected new key to be generated")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func checkKeyError(t *testing.T, desc string, err error) {
|
||||
viper.Set(commonflags.WalletPath, desc)
|
||||
_, actualErr := getOrGenerate()
|
||||
require.ErrorIs(t, actualErr, err)
|
||||
}
|
||||
|
||||
func checkKey(t *testing.T, desc string, expected *keys.PrivateKey) {
|
||||
viper.Set(commonflags.WalletPath, desc)
|
||||
actual, err := getOrGenerate()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &expected.PrivateKey, actual)
|
||||
}
|
62
cmd/frostfs-cli/internal/key/raw.go
Normal file
62
cmd/frostfs-cli/internal/key/raw.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var errCantGenerateKey = errors.New("can't generate new private key")
|
||||
|
||||
// Get returns private key from wallet or binary file.
|
||||
// Ideally we want to touch file-system on the last step.
|
||||
// This function assumes that all flags were bind to viper in a `PersistentPreRun`.
|
||||
func Get(cmd *cobra.Command) *ecdsa.PrivateKey {
|
||||
pk, err := get()
|
||||
common.ExitOnErr(cmd, "can't fetch private key: %w", err)
|
||||
return pk
|
||||
}
|
||||
|
||||
func get() (*ecdsa.PrivateKey, error) {
|
||||
keyDesc := viper.GetString(commonflags.WalletPath)
|
||||
data, err := os.ReadFile(keyDesc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrFs, err)
|
||||
}
|
||||
|
||||
priv, err := keys.NewPrivateKeyFromBytes(data)
|
||||
if err != nil {
|
||||
w, err := wallet.NewWalletFromFile(keyDesc)
|
||||
if err == nil {
|
||||
return FromWallet(w, viper.GetString(commonflags.Account))
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err)
|
||||
}
|
||||
return &priv.PrivateKey, nil
|
||||
}
|
||||
|
||||
// GetOrGenerate is similar to get but generates a new key if commonflags.GenerateKey is set.
|
||||
func GetOrGenerate(cmd *cobra.Command) *ecdsa.PrivateKey {
|
||||
pk, err := getOrGenerate()
|
||||
common.ExitOnErr(cmd, "can't fetch private key: %w", err)
|
||||
return pk
|
||||
}
|
||||
|
||||
func getOrGenerate() (*ecdsa.PrivateKey, error) {
|
||||
if viper.GetBool(commonflags.GenerateKey) {
|
||||
priv, err := keys.NewPrivateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errCantGenerateKey, err)
|
||||
}
|
||||
return &priv.PrivateKey, nil
|
||||
}
|
||||
return get()
|
||||
}
|
75
cmd/frostfs-cli/internal/key/wallet.go
Normal file
75
cmd/frostfs-cli/internal/key/wallet.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/cli/flags"
|
||||
"github.com/nspcc-dev/neo-go/cli/input"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Key-related errors.
|
||||
var (
|
||||
ErrFs = errors.New("unable to read file from given path")
|
||||
ErrInvalidKey = errors.New("provided key is incorrect, only wallet or binary key supported")
|
||||
ErrInvalidAddress = errors.New("--address option must be specified and valid")
|
||||
ErrInvalidPassword = errors.New("invalid password for the encrypted key")
|
||||
)
|
||||
|
||||
// FromWallet returns private key of the wallet account.
|
||||
func FromWallet(w *wallet.Wallet, addrStr string) (*ecdsa.PrivateKey, error) {
|
||||
var (
|
||||
addr util.Uint160
|
||||
err error
|
||||
)
|
||||
|
||||
if addrStr == "" {
|
||||
printVerbose("Using default wallet address")
|
||||
addr = w.GetChangeAddress()
|
||||
} else {
|
||||
addr, err = flags.ParseAddress(addrStr)
|
||||
if err != nil {
|
||||
printVerbose("Can't parse address: %s", addrStr)
|
||||
return nil, ErrInvalidAddress
|
||||
}
|
||||
}
|
||||
|
||||
acc := w.GetAccount(addr)
|
||||
if acc == nil {
|
||||
printVerbose("Can't find wallet account for %s", addrStr)
|
||||
return nil, ErrInvalidAddress
|
||||
}
|
||||
|
||||
pass, err := getPassword()
|
||||
if err != nil {
|
||||
printVerbose("Can't read password: %v", err)
|
||||
return nil, ErrInvalidPassword
|
||||
}
|
||||
|
||||
if err := acc.Decrypt(pass, keys.NEP2ScryptParams()); err != nil {
|
||||
printVerbose("Can't decrypt account: %v", err)
|
||||
return nil, ErrInvalidPassword
|
||||
}
|
||||
|
||||
return &acc.PrivateKey().PrivateKey, nil
|
||||
}
|
||||
|
||||
func getPassword() (string, error) {
|
||||
// this check allows empty passwords
|
||||
if viper.IsSet("password") {
|
||||
return viper.GetString("password"), nil
|
||||
}
|
||||
|
||||
return input.ReadPassword("Enter password > ")
|
||||
}
|
||||
|
||||
func printVerbose(format string, a ...interface{}) {
|
||||
if viper.GetBool("verbose") {
|
||||
fmt.Printf(format+"\n", a...)
|
||||
}
|
||||
}
|
7
cmd/frostfs-cli/main.go
Normal file
7
cmd/frostfs-cli/main.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import cmd "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
70
cmd/frostfs-cli/modules/accounting/balance.go
Normal file
70
cmd/frostfs-cli/modules/accounting/balance.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/util/precision"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
ownerFlag = "owner"
|
||||
)
|
||||
|
||||
var accountingBalanceCmd = &cobra.Command{
|
||||
Use: "balance",
|
||||
Short: "Get internal balance of NeoFS account",
|
||||
Long: `Get internal balance of NeoFS account`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var idUser user.ID
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
balanceOwner, _ := cmd.Flags().GetString(ownerFlag)
|
||||
if balanceOwner == "" {
|
||||
user.IDFromKey(&idUser, pk.PublicKey)
|
||||
} else {
|
||||
common.ExitOnErr(cmd, "can't decode owner ID wallet address: %w", idUser.DecodeString(balanceOwner))
|
||||
}
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.BalanceOfPrm
|
||||
prm.SetClient(cli)
|
||||
prm.SetAccount(idUser)
|
||||
|
||||
res, err := internalclient.BalanceOf(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
// print to stdout
|
||||
prettyPrintDecimal(cmd, res.Balance())
|
||||
},
|
||||
}
|
||||
|
||||
func initAccountingBalanceCmd() {
|
||||
ff := accountingBalanceCmd.Flags()
|
||||
|
||||
ff.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||
ff.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
||||
ff.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
||||
ff.String(ownerFlag, "", "owner of balance account (omit to use owner from private key)")
|
||||
}
|
||||
|
||||
func prettyPrintDecimal(cmd *cobra.Command, decimal accounting.Decimal) {
|
||||
if viper.GetBool(commonflags.Verbose) {
|
||||
cmd.Println("value:", decimal.Value())
|
||||
cmd.Println("precision:", decimal.Precision())
|
||||
} else {
|
||||
amountF8 := precision.Convert(decimal.Precision(), 8, big.NewInt(decimal.Value()))
|
||||
|
||||
cmd.Println(fixedn.ToString(amountF8, 8))
|
||||
}
|
||||
}
|
27
cmd/frostfs-cli/modules/accounting/root.go
Normal file
27
cmd/frostfs-cli/modules/accounting/root.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Cmd represents the accounting command.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "accounting",
|
||||
Short: "Operations with accounts and balances",
|
||||
Long: `Operations with accounts and balances`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
flags := cmd.Flags()
|
||||
|
||||
_ = viper.BindPFlag(commonflags.WalletPath, flags.Lookup(commonflags.WalletPath))
|
||||
_ = viper.BindPFlag(commonflags.Account, flags.Lookup(commonflags.Account))
|
||||
_ = viper.BindPFlag(commonflags.RPC, flags.Lookup(commonflags.RPC))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(accountingBalanceCmd)
|
||||
|
||||
initAccountingBalanceCmd()
|
||||
}
|
28
cmd/frostfs-cli/modules/acl/basic/print.go
Normal file
28
cmd/frostfs-cli/modules/acl/basic/print.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package basic
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var printACLCmd = &cobra.Command{
|
||||
Use: "print",
|
||||
Short: "Pretty print basic ACL from the HEX representation",
|
||||
Example: `frostfs-cli acl basic print 0x1C8C8CCC`,
|
||||
Long: `Pretty print basic ACL from the HEX representation.
|
||||
Few roles have exclusive default access to set of operation, even if particular bit deny it.
|
||||
Container have access to the operations of the data replication mechanism:
|
||||
Get, Head, Put, Search, Hash.
|
||||
InnerRing members are allowed to data audit ops only:
|
||||
Get, Head, Hash, Search.`,
|
||||
Run: printACL,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func printACL(cmd *cobra.Command, args []string) {
|
||||
var bacl acl.Basic
|
||||
common.ExitOnErr(cmd, "unable to parse basic acl: %w", bacl.DecodeString(args[0]))
|
||||
util.PrettyPrintTableBACL(cmd, &bacl)
|
||||
}
|
14
cmd/frostfs-cli/modules/acl/basic/root.go
Normal file
14
cmd/frostfs-cli/modules/acl/basic/root.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package basic
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "basic",
|
||||
Short: "Operations with Basic Access Control Lists",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(printACLCmd)
|
||||
}
|
127
cmd/frostfs-cli/modules/acl/extended/create.go
Normal file
127
cmd/frostfs-cli/modules/acl/extended/create.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package extended
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create extended ACL from the text representation",
|
||||
Long: `Create extended ACL from the text representation.
|
||||
|
||||
Rule consist of these blocks: <action> <operation> [<filter1> ...] [<target1> ...]
|
||||
|
||||
Action is 'allow' or 'deny'.
|
||||
|
||||
Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'.
|
||||
|
||||
Filter consists of <typ>:<key><match><value>
|
||||
Typ is 'obj' for object applied filter or 'req' for request applied filter.
|
||||
Key is a valid unicode string corresponding to object or request header key.
|
||||
Well-known system object headers start with '$Object:' prefix.
|
||||
User defined headers start without prefix.
|
||||
Read more about filter keys at github.com/TrueCloudLab/frostfs-api/blob/master/proto-docs/acl.md#message-eaclrecordfilter
|
||||
Match is '=' for matching and '!=' for non-matching filter.
|
||||
Value is a valid unicode string corresponding to object or request header value.
|
||||
|
||||
Target is
|
||||
'user' for container owner,
|
||||
'system' for Storage nodes in container and Inner Ring nodes,
|
||||
'others' for all other request senders,
|
||||
'pubkey:<key1>,<key2>,...' for exact request sender, where <key> is a hex-encoded 33-byte public key.
|
||||
|
||||
When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting extended ACL table.
|
||||
`,
|
||||
Example: `frostfs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -f rules.txt --out table.json
|
||||
frostfs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others'`,
|
||||
Run: createEACL,
|
||||
}
|
||||
|
||||
func init() {
|
||||
createCmd.Flags().StringArrayP("rule", "r", nil, "Extended ACL table record to apply")
|
||||
createCmd.Flags().StringP("file", "f", "", "Read list of extended ACL table records from text file")
|
||||
createCmd.Flags().StringP("out", "o", "", "Save JSON formatted extended ACL table in file")
|
||||
createCmd.Flags().StringP(commonflags.CIDFlag, "", "", commonflags.CIDFlagUsage)
|
||||
|
||||
_ = cobra.MarkFlagFilename(createCmd.Flags(), "file")
|
||||
_ = cobra.MarkFlagFilename(createCmd.Flags(), "out")
|
||||
}
|
||||
|
||||
func createEACL(cmd *cobra.Command, _ []string) {
|
||||
rules, _ := cmd.Flags().GetStringArray("rule")
|
||||
fileArg, _ := cmd.Flags().GetString("file")
|
||||
outArg, _ := cmd.Flags().GetString("out")
|
||||
cidArg, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
|
||||
var containerID cid.ID
|
||||
if cidArg != "" {
|
||||
if err := containerID.DecodeString(cidArg); err != nil {
|
||||
cmd.PrintErrf("invalid container ID: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
rulesFile, err := getRulesFromFile(fileArg)
|
||||
if err != nil {
|
||||
cmd.PrintErrf("can't read rules from file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rules = append(rules, rulesFile...)
|
||||
if len(rules) == 0 {
|
||||
cmd.PrintErrln("no extended ACL rules has been provided")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tb := eacl.NewTable()
|
||||
common.ExitOnErr(cmd, "unable to parse provided rules: %w", util.ParseEACLRules(tb, rules))
|
||||
|
||||
tb.SetCID(containerID)
|
||||
|
||||
data, err := tb.MarshalJSON()
|
||||
if err != nil {
|
||||
cmd.PrintErrln(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = json.Indent(buf, data, "", " ")
|
||||
if err != nil {
|
||||
cmd.PrintErrln(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(outArg) == 0 {
|
||||
cmd.Println(buf)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(outArg, buf.Bytes(), 0644)
|
||||
if err != nil {
|
||||
cmd.PrintErrln(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getRulesFromFile(filename string) ([]string, error) {
|
||||
if len(filename) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strings.Split(strings.TrimSpace(string(data)), "\n"), nil
|
||||
}
|
90
cmd/frostfs-cli/modules/acl/extended/create_test.go
Normal file
90
cmd/frostfs-cli/modules/acl/extended/create_test.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package extended
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseTable(t *testing.T) {
|
||||
tests := [...]struct {
|
||||
name string // test name
|
||||
rule string // input extended ACL rule
|
||||
jsonRecord string // produced record after successfull parsing
|
||||
}{
|
||||
{
|
||||
name: "valid rule with multiple filters",
|
||||
rule: "deny get obj:a=b req:c=d others",
|
||||
jsonRecord: `{"operation":"GET","action":"DENY","filters":[{"headerType":"OBJECT","matchType":"STRING_EQUAL","key":"a","value":"b"},{"headerType":"REQUEST","matchType":"STRING_EQUAL","key":"c","value":"d"}],"targets":[{"role":"OTHERS","keys":[]}]}`,
|
||||
},
|
||||
{
|
||||
name: "valid rule without filters",
|
||||
rule: "allow put user",
|
||||
jsonRecord: `{"operation":"PUT","action":"ALLOW","filters":[],"targets":[{"role":"USER","keys":[]}]}`,
|
||||
},
|
||||
{
|
||||
name: "valid rule with public key",
|
||||
rule: "deny getrange pubkey:036410abb260bbbda89f61c0cad65a4fa15ac5cb83b3c3abf8aee403856fcf65ed",
|
||||
jsonRecord: `{"operation":"GETRANGE","action":"DENY","filters":[],"targets":[{"role":"ROLE_UNSPECIFIED","keys":["A2QQq7Jgu72on2HAytZaT6FaxcuDs8Or+K7kA4Vvz2Xt"]}]}`,
|
||||
},
|
||||
{
|
||||
name: "missing action",
|
||||
rule: "get obj:a=b others",
|
||||
},
|
||||
{
|
||||
name: "invalid action",
|
||||
rule: "permit get obj:a=b others",
|
||||
},
|
||||
{
|
||||
name: "missing op",
|
||||
rule: "deny obj:a=b others",
|
||||
},
|
||||
{
|
||||
name: "invalid op action",
|
||||
rule: "deny look obj:a=b others",
|
||||
},
|
||||
{
|
||||
name: "invalid filter type",
|
||||
rule: "deny get invalid:a=b others",
|
||||
},
|
||||
{
|
||||
name: "invalid target group",
|
||||
rule: "deny get obj:a=b helpers",
|
||||
},
|
||||
{
|
||||
name: "invalid public key",
|
||||
rule: "deny get obj:a=b pubkey:0123",
|
||||
},
|
||||
}
|
||||
|
||||
eaclTable := eacl.NewTable()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := util.ParseEACLRule(eaclTable, test.rule)
|
||||
ok := len(test.jsonRecord) > 0
|
||||
require.Equal(t, ok, err == nil, err)
|
||||
if ok {
|
||||
expectedRecord := eacl.NewRecord()
|
||||
err = expectedRecord.UnmarshalJSON([]byte(test.jsonRecord))
|
||||
require.NoError(t, err)
|
||||
|
||||
actualRecord := eaclTable.Records()[len(eaclTable.Records())-1]
|
||||
|
||||
equalRecords(t, expectedRecord, &actualRecord)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func equalRecords(t *testing.T, r1, r2 *eacl.Record) {
|
||||
d1, err := r1.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
d2, err := r2.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, d1, d2)
|
||||
}
|
38
cmd/frostfs-cli/modules/acl/extended/print.go
Normal file
38
cmd/frostfs-cli/modules/acl/extended/print.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package extended
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var printEACLCmd = &cobra.Command{
|
||||
Use: "print",
|
||||
Short: "Pretty print extended ACL from the file(in text or json format) or for given container.",
|
||||
Run: printEACL,
|
||||
}
|
||||
|
||||
func init() {
|
||||
flags := printEACLCmd.Flags()
|
||||
flags.StringP("file", "f", "",
|
||||
"Read list of extended ACL table records from text or json file")
|
||||
_ = printEACLCmd.MarkFlagRequired("file")
|
||||
}
|
||||
|
||||
func printEACL(cmd *cobra.Command, _ []string) {
|
||||
file, _ := cmd.Flags().GetString("file")
|
||||
eaclTable := new(eacl.Table)
|
||||
data, err := os.ReadFile(file)
|
||||
common.ExitOnErr(cmd, "can't read file with EACL: %w", err)
|
||||
if strings.HasSuffix(file, ".json") {
|
||||
common.ExitOnErr(cmd, "unable to parse json: %w", eaclTable.UnmarshalJSON(data))
|
||||
} else {
|
||||
rules := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
common.ExitOnErr(cmd, "can't parse file with EACL: %w", util.ParseEACLRules(eaclTable, rules))
|
||||
}
|
||||
util.PrettyPrintTableEACL(cmd, eaclTable)
|
||||
}
|
15
cmd/frostfs-cli/modules/acl/extended/root.go
Normal file
15
cmd/frostfs-cli/modules/acl/extended/root.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package extended
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "extended",
|
||||
Short: "Operations with Extended Access Control Lists",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(createCmd)
|
||||
Cmd.AddCommand(printEACLCmd)
|
||||
}
|
17
cmd/frostfs-cli/modules/acl/root.go
Normal file
17
cmd/frostfs-cli/modules/acl/root.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/acl/basic"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/acl/extended"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "acl",
|
||||
Short: "Operations with Access Control Lists",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(extended.Cmd)
|
||||
Cmd.AddCommand(basic.Cmd)
|
||||
}
|
125
cmd/frostfs-cli/modules/bearer/create.go
Normal file
125
cmd/frostfs-cli/modules/bearer/create.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package bearer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
eaclSDK "github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
eaclFlag = "eacl"
|
||||
issuedAtFlag = "issued-at"
|
||||
notValidBeforeFlag = "not-valid-before"
|
||||
ownerFlag = "owner"
|
||||
outFlag = "out"
|
||||
jsonFlag = commonflags.JSON
|
||||
)
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create bearer token",
|
||||
Long: `Create bearer token.
|
||||
|
||||
All epoch flags can be specified relative to the current epoch with the +n syntax.
|
||||
In this case --` + commonflags.RPC + ` flag should be specified and the epoch in bearer token
|
||||
is set to current epoch + n.
|
||||
`,
|
||||
Run: createToken,
|
||||
}
|
||||
|
||||
func init() {
|
||||
createCmd.Flags().StringP(eaclFlag, "e", "", "Path to the extended ACL table")
|
||||
createCmd.Flags().StringP(issuedAtFlag, "i", "", "Epoch to issue token at")
|
||||
createCmd.Flags().StringP(notValidBeforeFlag, "n", "", "Not valid before epoch")
|
||||
createCmd.Flags().StringP(commonflags.ExpireAt, "x", "", "Expiration epoch")
|
||||
createCmd.Flags().StringP(ownerFlag, "o", "", "Token owner")
|
||||
createCmd.Flags().String(outFlag, "", "File to write token to")
|
||||
createCmd.Flags().Bool(jsonFlag, false, "Output token in JSON")
|
||||
createCmd.Flags().StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
||||
|
||||
_ = cobra.MarkFlagFilename(createCmd.Flags(), eaclFlag)
|
||||
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), issuedAtFlag)
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), notValidBeforeFlag)
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), commonflags.ExpireAt)
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), ownerFlag)
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), outFlag)
|
||||
}
|
||||
|
||||
func createToken(cmd *cobra.Command, _ []string) {
|
||||
iat, iatRelative, err := common.ParseEpoch(cmd, issuedAtFlag)
|
||||
common.ExitOnErr(cmd, "can't parse --"+issuedAtFlag+" flag: %w", err)
|
||||
|
||||
exp, expRelative, err := common.ParseEpoch(cmd, commonflags.ExpireAt)
|
||||
common.ExitOnErr(cmd, "can't parse --"+commonflags.ExpireAt+" flag: %w", err)
|
||||
|
||||
nvb, nvbRelative, err := common.ParseEpoch(cmd, notValidBeforeFlag)
|
||||
common.ExitOnErr(cmd, "can't parse --"+notValidBeforeFlag+" flag: %w", err)
|
||||
|
||||
if iatRelative || expRelative || nvbRelative {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
endpoint, _ := cmd.Flags().GetString(commonflags.RPC)
|
||||
currEpoch, err := internalclient.GetCurrentEpoch(ctx, endpoint)
|
||||
common.ExitOnErr(cmd, "can't fetch current epoch: %w", err)
|
||||
|
||||
if iatRelative {
|
||||
iat += currEpoch
|
||||
}
|
||||
if expRelative {
|
||||
exp += currEpoch
|
||||
}
|
||||
if nvbRelative {
|
||||
nvb += currEpoch
|
||||
}
|
||||
}
|
||||
if exp < nvb {
|
||||
common.ExitOnErr(cmd, "",
|
||||
fmt.Errorf("expiration epoch is less than not-valid-before epoch: %d < %d", exp, nvb))
|
||||
}
|
||||
|
||||
ownerStr, _ := cmd.Flags().GetString(ownerFlag)
|
||||
|
||||
var ownerID user.ID
|
||||
common.ExitOnErr(cmd, "can't parse recipient: %w", ownerID.DecodeString(ownerStr))
|
||||
|
||||
var b bearer.Token
|
||||
b.SetExp(exp)
|
||||
b.SetNbf(nvb)
|
||||
b.SetIat(iat)
|
||||
b.ForUser(ownerID)
|
||||
|
||||
eaclPath, _ := cmd.Flags().GetString(eaclFlag)
|
||||
if eaclPath != "" {
|
||||
table := eaclSDK.NewTable()
|
||||
raw, err := os.ReadFile(eaclPath)
|
||||
common.ExitOnErr(cmd, "can't read extended ACL file: %w", err)
|
||||
common.ExitOnErr(cmd, "can't parse extended ACL: %w", json.Unmarshal(raw, table))
|
||||
b.SetEACLTable(*table)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
|
||||
toJSON, _ := cmd.Flags().GetBool(jsonFlag)
|
||||
if toJSON {
|
||||
data, err = json.Marshal(b)
|
||||
common.ExitOnErr(cmd, "can't mashal token to JSON: %w", err)
|
||||
} else {
|
||||
data = b.Marshal()
|
||||
}
|
||||
|
||||
out, _ := cmd.Flags().GetString(outFlag)
|
||||
err = os.WriteFile(out, data, 0644)
|
||||
common.ExitOnErr(cmd, "can't write token to file: %w", err)
|
||||
}
|
14
cmd/frostfs-cli/modules/bearer/root.go
Normal file
14
cmd/frostfs-cli/modules/bearer/root.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package bearer
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "bearer",
|
||||
Short: "Operations with bearer token",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(createCmd)
|
||||
}
|
9
cmd/frostfs-cli/modules/completion.go
Normal file
9
cmd/frostfs-cli/modules/completion.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/util/autocomplete"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(autocomplete.Command("frostfs-cli"))
|
||||
}
|
216
cmd/frostfs-cli/modules/container/create.go
Normal file
216
cmd/frostfs-cli/modules/container/create.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
subnetid "github.com/TrueCloudLab/frostfs-sdk-go/subnet/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
containerACL string
|
||||
containerPolicy string
|
||||
containerAttributes []string
|
||||
containerAwait bool
|
||||
containerName string
|
||||
containerNoTimestamp bool
|
||||
containerSubnet string
|
||||
force bool
|
||||
)
|
||||
|
||||
var createContainerCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create new container",
|
||||
Long: `Create new container and register it in the NeoFS.
|
||||
It will be stored in sidechain when inner ring will accepts it.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
placementPolicy, err := parseContainerPolicy(containerPolicy)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
key := key.Get(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, key, commonflags.RPC)
|
||||
|
||||
if !force {
|
||||
var prm internalclient.NetMapSnapshotPrm
|
||||
prm.SetClient(cli)
|
||||
|
||||
resmap, err := internalclient.NetMapSnapshot(prm)
|
||||
common.ExitOnErr(cmd, "unable to get netmap snapshot to validate container placement, "+
|
||||
"use --force option to skip this check: %w", err)
|
||||
|
||||
nodesByRep, err := resmap.NetMap().ContainerNodes(*placementPolicy, nil)
|
||||
common.ExitOnErr(cmd, "could not build container nodes based on given placement policy, "+
|
||||
"use --force option to skip this check: %w", err)
|
||||
|
||||
for i, nodes := range nodesByRep {
|
||||
if placementPolicy.ReplicaNumberByIndex(i) > uint32(len(nodes)) {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf(
|
||||
"the number of nodes '%d' in selector is not enough for the number of replicas '%d', "+
|
||||
"use --force option to skip this check",
|
||||
len(nodes),
|
||||
placementPolicy.ReplicaNumberByIndex(i),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if containerSubnet != "" {
|
||||
var subnetID subnetid.ID
|
||||
|
||||
err = subnetID.DecodeString(containerSubnet)
|
||||
common.ExitOnErr(cmd, "could not parse subnetID: %w", err)
|
||||
|
||||
placementPolicy.RestrictSubnet(subnetID)
|
||||
}
|
||||
|
||||
var cnr container.Container
|
||||
cnr.Init()
|
||||
|
||||
err = parseAttributes(&cnr, containerAttributes)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
var basicACL acl.Basic
|
||||
common.ExitOnErr(cmd, "decode basic ACL string: %w", basicACL.DecodeString(containerACL))
|
||||
|
||||
tok := getSession(cmd)
|
||||
|
||||
if tok != nil {
|
||||
issuer := tok.Issuer()
|
||||
cnr.SetOwner(issuer)
|
||||
} else {
|
||||
var idOwner user.ID
|
||||
user.IDFromKey(&idOwner, key.PublicKey)
|
||||
|
||||
cnr.SetOwner(idOwner)
|
||||
}
|
||||
|
||||
cnr.SetPlacementPolicy(*placementPolicy)
|
||||
cnr.SetBasicACL(basicACL)
|
||||
|
||||
var syncContainerPrm internalclient.SyncContainerPrm
|
||||
syncContainerPrm.SetClient(cli)
|
||||
syncContainerPrm.SetContainer(&cnr)
|
||||
|
||||
_, err = internalclient.SyncContainerSettings(syncContainerPrm)
|
||||
common.ExitOnErr(cmd, "syncing container's settings rpc error: %w", err)
|
||||
|
||||
var putPrm internalclient.PutContainerPrm
|
||||
putPrm.SetClient(cli)
|
||||
putPrm.SetContainer(cnr)
|
||||
|
||||
if tok != nil {
|
||||
putPrm.WithinSession(*tok)
|
||||
}
|
||||
|
||||
res, err := internalclient.PutContainer(putPrm)
|
||||
common.ExitOnErr(cmd, "put container rpc error: %w", err)
|
||||
|
||||
id := res.ID()
|
||||
|
||||
cmd.Println("container ID:", id)
|
||||
|
||||
if containerAwait {
|
||||
cmd.Println("awaiting...")
|
||||
|
||||
var getPrm internalclient.GetContainerPrm
|
||||
getPrm.SetClient(cli)
|
||||
getPrm.SetContainer(id)
|
||||
|
||||
for i := 0; i < awaitTimeout; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
_, err := internalclient.GetContainer(getPrm)
|
||||
if err == nil {
|
||||
cmd.Println("container has been persisted on sidechain")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "", errCreateTimeout)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerCreateCmd() {
|
||||
flags := createContainerCmd.Flags()
|
||||
|
||||
// Init common flags
|
||||
flags.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
||||
flags.DurationP(commonflags.Timeout, commonflags.TimeoutShorthand, commonflags.TimeoutDefault, commonflags.TimeoutUsage)
|
||||
flags.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||
flags.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
||||
|
||||
flags.StringVar(&containerACL, "basic-acl", acl.NamePrivate, fmt.Sprintf("HEX encoded basic ACL value or keywords like '%s', '%s', '%s'",
|
||||
acl.NamePublicRW, acl.NamePrivate, acl.NamePublicROExtended,
|
||||
))
|
||||
flags.StringVarP(&containerPolicy, "policy", "p", "", "QL-encoded or JSON-encoded placement policy or path to file with it")
|
||||
flags.StringSliceVarP(&containerAttributes, "attributes", "a", nil, "Comma separated pairs of container attributes in form of Key1=Value1,Key2=Value2")
|
||||
flags.BoolVar(&containerAwait, "await", false, "Block execution until container is persisted")
|
||||
flags.StringVar(&containerName, "name", "", "Container name attribute")
|
||||
flags.BoolVar(&containerNoTimestamp, "disable-timestamp", false, "Disable timestamp container attribute")
|
||||
flags.StringVar(&containerSubnet, "subnet", "", "String representation of container subnetwork")
|
||||
flags.BoolVarP(&force, commonflags.ForceFlag, commonflags.ForceFlagShorthand, false,
|
||||
"Skip placement validity check")
|
||||
}
|
||||
|
||||
func parseContainerPolicy(policyString string) (*netmap.PlacementPolicy, error) {
|
||||
_, err := os.Stat(policyString) // check if `policyString` is a path to file with placement policy
|
||||
if err == nil {
|
||||
common.PrintVerbose("Reading placement policy from file: %s", policyString)
|
||||
|
||||
data, err := os.ReadFile(policyString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read file with placement policy: %w", err)
|
||||
}
|
||||
|
||||
policyString = string(data)
|
||||
}
|
||||
|
||||
var result netmap.PlacementPolicy
|
||||
|
||||
err = result.DecodeString(policyString)
|
||||
if err == nil {
|
||||
common.PrintVerbose("Parsed QL encoded policy")
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
if err = result.UnmarshalJSON([]byte(policyString)); err == nil {
|
||||
common.PrintVerbose("Parsed JSON encoded policy")
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("can't parse placement policy")
|
||||
}
|
||||
|
||||
func parseAttributes(dst *container.Container, attributes []string) error {
|
||||
for i := range attributes {
|
||||
kvPair := strings.Split(attributes[i], attributeDelimiter)
|
||||
if len(kvPair) != 2 {
|
||||
return errors.New("invalid container attribute")
|
||||
}
|
||||
|
||||
dst.SetAttribute(kvPair[0], kvPair[1])
|
||||
}
|
||||
|
||||
if !containerNoTimestamp {
|
||||
container.SetCreationTime(dst, time.Now())
|
||||
}
|
||||
|
||||
if containerName != "" {
|
||||
container.SetName(dst, containerName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
95
cmd/frostfs-cli/modules/container/delete.go
Normal file
95
cmd/frostfs-cli/modules/container/delete.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
objectSDK "github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var deleteContainerCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete existing container",
|
||||
Long: `Delete existing container.
|
||||
Only owner of the container has a permission to remove container.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
id := parseContainerID(cmd)
|
||||
|
||||
tok := getSession(cmd)
|
||||
|
||||
pk := key.Get(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
if force, _ := cmd.Flags().GetBool(commonflags.ForceFlag); !force {
|
||||
fs := objectSDK.NewSearchFilters()
|
||||
fs.AddTypeFilter(objectSDK.MatchStringEqual, objectSDK.TypeLock)
|
||||
|
||||
var searchPrm internalclient.SearchObjectsPrm
|
||||
searchPrm.SetClient(cli)
|
||||
searchPrm.SetContainerID(id)
|
||||
searchPrm.SetFilters(fs)
|
||||
searchPrm.SetTTL(2)
|
||||
|
||||
common.PrintVerbose("Searching for LOCK objects...")
|
||||
|
||||
res, err := internalclient.SearchObjects(searchPrm)
|
||||
common.ExitOnErr(cmd, "can't search for LOCK objects: %w", err)
|
||||
|
||||
if len(res.IDList()) != 0 {
|
||||
common.ExitOnErr(cmd, "",
|
||||
fmt.Errorf("Container wasn't removed because LOCK objects were found.\n"+
|
||||
"Use --%s flag to remove anyway.", commonflags.ForceFlag))
|
||||
}
|
||||
}
|
||||
|
||||
var delPrm internalclient.DeleteContainerPrm
|
||||
delPrm.SetClient(cli)
|
||||
delPrm.SetContainer(id)
|
||||
|
||||
if tok != nil {
|
||||
delPrm.WithinSession(*tok)
|
||||
}
|
||||
|
||||
_, err := internalclient.DeleteContainer(delPrm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
cmd.Println("container delete method invoked")
|
||||
|
||||
if containerAwait {
|
||||
cmd.Println("awaiting...")
|
||||
|
||||
var getPrm internalclient.GetContainerPrm
|
||||
getPrm.SetClient(cli)
|
||||
getPrm.SetContainer(id)
|
||||
|
||||
for i := 0; i < awaitTimeout; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
_, err := internalclient.GetContainer(getPrm)
|
||||
if err != nil {
|
||||
cmd.Println("container has been removed:", containerID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "", errDeleteTimeout)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerDeleteCmd() {
|
||||
flags := deleteContainerCmd.Flags()
|
||||
|
||||
flags.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||
flags.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
||||
flags.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
||||
|
||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.BoolVar(&containerAwait, "await", false, "Block execution until container is removed")
|
||||
flags.BoolP(commonflags.ForceFlag, commonflags.ForceFlagShorthand, false, "Do not check whether container contains locks and remove immediately")
|
||||
}
|
172
cmd/frostfs-cli/modules/container/get.go
Normal file
172
cmd/frostfs-cli/modules/container/get.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
fromFlag = "from"
|
||||
fromFlagUsage = "Path to file with encoded container"
|
||||
)
|
||||
|
||||
var (
|
||||
containerID string
|
||||
containerPathFrom string
|
||||
containerPathTo string
|
||||
containerJSON bool
|
||||
)
|
||||
|
||||
var getContainerInfoCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get container field info",
|
||||
Long: `Get container field info`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cnr, _ := getContainer(cmd)
|
||||
|
||||
prettyPrintContainer(cmd, cnr, containerJSON)
|
||||
|
||||
if containerPathTo != "" {
|
||||
var (
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if containerJSON {
|
||||
data, err = cnr.MarshalJSON()
|
||||
common.ExitOnErr(cmd, "can't JSON encode container: %w", err)
|
||||
} else {
|
||||
data = cnr.Marshal()
|
||||
}
|
||||
|
||||
err = os.WriteFile(containerPathTo, data, 0644)
|
||||
common.ExitOnErr(cmd, "can't write container to file: %w", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerInfoCmd() {
|
||||
commonflags.Init(getContainerInfoCmd)
|
||||
|
||||
flags := getContainerInfoCmd.Flags()
|
||||
|
||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.StringVar(&containerPathTo, "to", "", "Path to dump encoded container")
|
||||
flags.StringVar(&containerPathFrom, fromFlag, "", fromFlagUsage)
|
||||
flags.BoolVar(&containerJSON, commonflags.JSON, false, "Print or dump container in JSON format")
|
||||
}
|
||||
|
||||
type stringWriter cobra.Command
|
||||
|
||||
func (x *stringWriter) WriteString(s string) (n int, err error) {
|
||||
(*cobra.Command)(x).Print(s)
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func prettyPrintContainer(cmd *cobra.Command, cnr container.Container, jsonEncoding bool) {
|
||||
if jsonEncoding {
|
||||
data, err := cnr.MarshalJSON()
|
||||
if err != nil {
|
||||
common.PrintVerbose("Can't convert container to json: %w", err)
|
||||
return
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := json.Indent(buf, data, "", " "); err != nil {
|
||||
common.PrintVerbose("Can't pretty print json: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println(buf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var id cid.ID
|
||||
container.CalculateID(&id, cnr)
|
||||
cmd.Println("container ID:", id)
|
||||
|
||||
cmd.Println("owner ID:", cnr.Owner())
|
||||
|
||||
basicACL := cnr.BasicACL()
|
||||
prettyPrintBasicACL(cmd, basicACL)
|
||||
|
||||
cmd.Println("created:", container.CreatedAt(cnr))
|
||||
|
||||
cmd.Println("attributes:")
|
||||
cnr.IterateAttributes(func(key, val string) {
|
||||
cmd.Printf("\t%s=%s\n", key, val)
|
||||
})
|
||||
|
||||
cmd.Println("placement policy:")
|
||||
common.ExitOnErr(cmd, "write policy: %w", cnr.PlacementPolicy().WriteStringTo((*stringWriter)(cmd)))
|
||||
cmd.Println()
|
||||
}
|
||||
|
||||
func prettyPrintBasicACL(cmd *cobra.Command, basicACL acl.Basic) {
|
||||
cmd.Printf("basic ACL: %s", basicACL.EncodeToString())
|
||||
|
||||
var prettyName string
|
||||
|
||||
switch basicACL {
|
||||
case acl.Private:
|
||||
prettyName = acl.NamePrivate
|
||||
case acl.PrivateExtended:
|
||||
prettyName = acl.NamePrivateExtended
|
||||
case acl.PublicRO:
|
||||
prettyName = acl.NamePublicRO
|
||||
case acl.PublicROExtended:
|
||||
prettyName = acl.NamePublicROExtended
|
||||
case acl.PublicRW:
|
||||
prettyName = acl.NamePublicRW
|
||||
case acl.PublicRWExtended:
|
||||
prettyName = acl.NamePublicRWExtended
|
||||
case acl.PublicAppend:
|
||||
prettyName = acl.NamePublicAppend
|
||||
case acl.PublicAppendExtended:
|
||||
prettyName = acl.NamePublicAppendExtended
|
||||
}
|
||||
|
||||
if prettyName != "" {
|
||||
cmd.Printf(" (%s)", prettyName)
|
||||
}
|
||||
|
||||
cmd.Println()
|
||||
util.PrettyPrintTableBACL(cmd, &basicACL)
|
||||
}
|
||||
|
||||
func getContainer(cmd *cobra.Command) (container.Container, *ecdsa.PrivateKey) {
|
||||
var cnr container.Container
|
||||
var pk *ecdsa.PrivateKey
|
||||
if containerPathFrom != "" {
|
||||
data, err := os.ReadFile(containerPathFrom)
|
||||
common.ExitOnErr(cmd, "can't read file: %w", err)
|
||||
|
||||
err = cnr.Unmarshal(data)
|
||||
common.ExitOnErr(cmd, "can't unmarshal container: %w", err)
|
||||
} else {
|
||||
id := parseContainerID(cmd)
|
||||
pk = key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.GetContainerPrm
|
||||
prm.SetClient(cli)
|
||||
prm.SetContainer(id)
|
||||
|
||||
res, err := internalclient.GetContainer(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
cnr = res.Container()
|
||||
}
|
||||
return cnr, pk
|
||||
}
|
63
cmd/frostfs-cli/modules/container/get_eacl.go
Normal file
63
cmd/frostfs-cli/modules/container/get_eacl.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getExtendedACLCmd = &cobra.Command{
|
||||
Use: "get-eacl",
|
||||
Short: "Get extended ACL table of container",
|
||||
Long: `Get extended ACL table of container`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
id := parseContainerID(cmd)
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var eaclPrm internalclient.EACLPrm
|
||||
eaclPrm.SetClient(cli)
|
||||
eaclPrm.SetContainer(id)
|
||||
|
||||
res, err := internalclient.EACL(eaclPrm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
eaclTable := res.EACL()
|
||||
|
||||
if containerPathTo == "" {
|
||||
cmd.Println("eACL: ")
|
||||
common.PrettyPrintJSON(cmd, &eaclTable, "eACL")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var data []byte
|
||||
|
||||
if containerJSON {
|
||||
data, err = eaclTable.MarshalJSON()
|
||||
common.ExitOnErr(cmd, "can't encode to JSON: %w", err)
|
||||
} else {
|
||||
data, err = eaclTable.Marshal()
|
||||
common.ExitOnErr(cmd, "can't encode to binary: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println("dumping data to file:", containerPathTo)
|
||||
|
||||
err = os.WriteFile(containerPathTo, data, 0644)
|
||||
common.ExitOnErr(cmd, "could not write eACL to file: %w", err)
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerGetEACLCmd() {
|
||||
commonflags.Init(getExtendedACLCmd)
|
||||
|
||||
flags := getExtendedACLCmd.Flags()
|
||||
|
||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.StringVar(&containerPathTo, "to", "", "Path to dump encoded container (default: binary encoded)")
|
||||
flags.BoolVar(&containerJSON, commonflags.JSON, false, "Encode EACL table in json format")
|
||||
}
|
89
cmd/frostfs-cli/modules/container/list.go
Normal file
89
cmd/frostfs-cli/modules/container/list.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// flags of list command.
|
||||
const (
|
||||
flagListPrintAttr = "with-attr"
|
||||
flagListContainerOwner = "owner"
|
||||
)
|
||||
|
||||
// flag vars of list command.
|
||||
var (
|
||||
flagVarListPrintAttr bool
|
||||
flagVarListContainerOwner string
|
||||
)
|
||||
|
||||
var listContainersCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all created containers",
|
||||
Long: "List all created containers",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var idUser user.ID
|
||||
|
||||
key := key.GetOrGenerate(cmd)
|
||||
|
||||
if flagVarListContainerOwner == "" {
|
||||
user.IDFromKey(&idUser, key.PublicKey)
|
||||
} else {
|
||||
err := idUser.DecodeString(flagVarListContainerOwner)
|
||||
common.ExitOnErr(cmd, "invalid user ID: %w", err)
|
||||
}
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, key, commonflags.RPC)
|
||||
|
||||
var prm internalclient.ListContainersPrm
|
||||
prm.SetClient(cli)
|
||||
prm.SetAccount(idUser)
|
||||
|
||||
res, err := internalclient.ListContainers(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
var prmGet internalclient.GetContainerPrm
|
||||
prmGet.SetClient(cli)
|
||||
|
||||
list := res.IDList()
|
||||
for i := range list {
|
||||
cmd.Println(list[i].String())
|
||||
|
||||
if flagVarListPrintAttr {
|
||||
prmGet.SetContainer(list[i])
|
||||
|
||||
res, err := internalclient.GetContainer(prmGet)
|
||||
if err == nil {
|
||||
res.Container().IterateAttributes(func(key, val string) {
|
||||
if !strings.HasPrefix(key, container.SysAttributePrefix) {
|
||||
// FIXME(@cthulhu-rider): neofs-sdk-go#314 use dedicated method to skip system attributes
|
||||
cmd.Printf(" %s: %s\n", key, val)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cmd.Printf(" failed to read attributes: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerListContainersCmd() {
|
||||
commonflags.Init(listContainersCmd)
|
||||
|
||||
flags := listContainersCmd.Flags()
|
||||
|
||||
flags.StringVar(&flagVarListContainerOwner, flagListContainerOwner, "",
|
||||
"Owner of containers (omit to use owner from private key)",
|
||||
)
|
||||
flags.BoolVar(&flagVarListPrintAttr, flagListPrintAttr, false,
|
||||
"Request and print attributes of each container",
|
||||
)
|
||||
}
|
96
cmd/frostfs-cli/modules/container/list_objects.go
Normal file
96
cmd/frostfs-cli/modules/container/list_objects.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
v2object "github.com/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// flags of list-object command.
|
||||
const (
|
||||
flagListObjectPrintAttr = "with-attr"
|
||||
)
|
||||
|
||||
// flag vars of list-objects command.
|
||||
var (
|
||||
flagVarListObjectsPrintAttr bool
|
||||
)
|
||||
|
||||
var listContainerObjectsCmd = &cobra.Command{
|
||||
Use: "list-objects",
|
||||
Short: "List existing objects in container",
|
||||
Long: `List existing objects in container`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
id := parseContainerID(cmd)
|
||||
|
||||
filters := new(object.SearchFilters)
|
||||
filters.AddRootFilter() // search only user created objects
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, key.GetOrGenerate(cmd), commonflags.RPC)
|
||||
|
||||
var prmSearch internalclient.SearchObjectsPrm
|
||||
var prmHead internalclient.HeadObjectPrm
|
||||
|
||||
prmSearch.SetClient(cli)
|
||||
|
||||
if flagVarListObjectsPrintAttr {
|
||||
prmHead.SetClient(cli)
|
||||
objectCli.Prepare(cmd, &prmSearch, &prmHead)
|
||||
} else {
|
||||
objectCli.Prepare(cmd, &prmSearch)
|
||||
}
|
||||
|
||||
prmSearch.SetContainerID(id)
|
||||
prmSearch.SetFilters(*filters)
|
||||
|
||||
res, err := internalclient.SearchObjects(prmSearch)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
objectIDs := res.IDList()
|
||||
|
||||
for i := range objectIDs {
|
||||
cmd.Println(objectIDs[i].String())
|
||||
|
||||
if flagVarListObjectsPrintAttr {
|
||||
var addr oid.Address
|
||||
addr.SetContainer(id)
|
||||
addr.SetObject(objectIDs[i])
|
||||
prmHead.SetAddress(addr)
|
||||
|
||||
resHead, err := internalclient.HeadObject(prmHead)
|
||||
if err == nil {
|
||||
attrs := resHead.Header().Attributes()
|
||||
for i := range attrs {
|
||||
attrKey := attrs[i].Key()
|
||||
if !strings.HasPrefix(attrKey, v2object.SysAttributePrefix) {
|
||||
// FIXME(@cthulhu-rider): neofs-sdk-go#226 use dedicated method to skip system attributes
|
||||
cmd.Printf(" %s: %s\n", attrKey, attrs[i].Value())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd.Printf(" failed to read attributes: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerListObjectsCmd() {
|
||||
commonflags.Init(listContainerObjectsCmd)
|
||||
objectCli.InitBearer(listContainerObjectsCmd)
|
||||
|
||||
flags := listContainerObjectsCmd.Flags()
|
||||
|
||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.BoolVar(&flagVarListObjectsPrintAttr, flagListObjectPrintAttr, false,
|
||||
"Request and print user attributes of each object",
|
||||
)
|
||||
}
|
64
cmd/frostfs-cli/modules/container/nodes.go
Normal file
64
cmd/frostfs-cli/modules/container/nodes.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
containerAPI "github.com/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var short bool
|
||||
|
||||
var containerNodesCmd = &cobra.Command{
|
||||
Use: "nodes",
|
||||
Short: "Show nodes for container",
|
||||
Long: "Show nodes taking part in a container at the current epoch.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var cnr, pkey = getContainer(cmd)
|
||||
|
||||
if pkey == nil {
|
||||
pkey = key.GetOrGenerate(cmd)
|
||||
}
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pkey, commonflags.RPC)
|
||||
|
||||
var prm internalclient.NetMapSnapshotPrm
|
||||
prm.SetClient(cli)
|
||||
|
||||
resmap, err := internalclient.NetMapSnapshot(prm)
|
||||
common.ExitOnErr(cmd, "unable to get netmap snapshot", err)
|
||||
|
||||
var id cid.ID
|
||||
containerAPI.CalculateID(&id, cnr)
|
||||
binCnr := make([]byte, sha256.Size)
|
||||
id.Encode(binCnr)
|
||||
|
||||
policy := cnr.PlacementPolicy()
|
||||
|
||||
var cnrNodes [][]netmap.NodeInfo
|
||||
cnrNodes, err = resmap.NetMap().ContainerNodes(policy, binCnr)
|
||||
common.ExitOnErr(cmd, "could not build container nodes for given container: %w", err)
|
||||
|
||||
for i := range cnrNodes {
|
||||
cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i))
|
||||
for j := range cnrNodes[i] {
|
||||
common.PrettyPrintNodeInfo(cmd, cnrNodes[i][j], j, "\t", short)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerNodesCmd() {
|
||||
commonflags.Init(containerNodesCmd)
|
||||
|
||||
flags := containerNodesCmd.Flags()
|
||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.StringVar(&containerPathFrom, fromFlag, "", fromFlagUsage)
|
||||
flags.BoolVar(&short, "short", false, "Shortens output of node info")
|
||||
}
|
58
cmd/frostfs-cli/modules/container/root.go
Normal file
58
cmd/frostfs-cli/modules/container/root.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Cmd represents the container command.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "container",
|
||||
Short: "Operations with containers",
|
||||
Long: "Operations with containers",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// bind exactly that cmd's flags to
|
||||
// the viper before execution
|
||||
commonflags.Bind(cmd)
|
||||
commonflags.BindAPI(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
containerChildCommand := []*cobra.Command{
|
||||
listContainersCmd,
|
||||
createContainerCmd,
|
||||
deleteContainerCmd,
|
||||
listContainerObjectsCmd,
|
||||
getContainerInfoCmd,
|
||||
getExtendedACLCmd,
|
||||
setExtendedACLCmd,
|
||||
containerNodesCmd,
|
||||
}
|
||||
|
||||
Cmd.AddCommand(containerChildCommand...)
|
||||
|
||||
initContainerListContainersCmd()
|
||||
initContainerCreateCmd()
|
||||
initContainerDeleteCmd()
|
||||
initContainerListObjectsCmd()
|
||||
initContainerInfoCmd()
|
||||
initContainerGetEACLCmd()
|
||||
initContainerSetEACLCmd()
|
||||
initContainerNodesCmd()
|
||||
|
||||
for _, containerCommand := range containerChildCommand {
|
||||
commonflags.InitAPI(containerCommand)
|
||||
}
|
||||
|
||||
for _, el := range []struct {
|
||||
cmd *cobra.Command
|
||||
verb string
|
||||
}{
|
||||
{createContainerCmd, "PUT"},
|
||||
{deleteContainerCmd, "DELETE"},
|
||||
{setExtendedACLCmd, "SETEACL"},
|
||||
} {
|
||||
commonflags.InitSession(el.cmd, "container "+el.verb)
|
||||
}
|
||||
}
|
103
cmd/frostfs-cli/modules/container/set_eacl.go
Normal file
103
cmd/frostfs-cli/modules/container/set_eacl.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagVarsSetEACL struct {
|
||||
noPreCheck bool
|
||||
|
||||
srcPath string
|
||||
}
|
||||
|
||||
var setExtendedACLCmd = &cobra.Command{
|
||||
Use: "set-eacl",
|
||||
Short: "Set new extended ACL table for container",
|
||||
Long: `Set new extended ACL table for container.
|
||||
Container ID in EACL table will be substituted with ID from the CLI.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
id := parseContainerID(cmd)
|
||||
eaclTable := common.ReadEACL(cmd, flagVarsSetEACL.srcPath)
|
||||
|
||||
tok := getSession(cmd)
|
||||
|
||||
eaclTable.SetCID(id)
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
if !flagVarsSetEACL.noPreCheck {
|
||||
cmd.Println("Checking the ability to modify access rights in the container...")
|
||||
|
||||
extendable, err := internalclient.IsACLExtendable(cli, id)
|
||||
common.ExitOnErr(cmd, "Extensibility check failure: %w", err)
|
||||
|
||||
if !extendable {
|
||||
common.ExitOnErr(cmd, "", errors.New("container ACL is immutable"))
|
||||
}
|
||||
|
||||
cmd.Println("ACL extension is enabled in the container, continue processing.")
|
||||
}
|
||||
|
||||
var setEACLPrm internalclient.SetEACLPrm
|
||||
setEACLPrm.SetClient(cli)
|
||||
setEACLPrm.SetTable(*eaclTable)
|
||||
|
||||
if tok != nil {
|
||||
setEACLPrm.WithinSession(*tok)
|
||||
}
|
||||
|
||||
_, err := internalclient.SetEACL(setEACLPrm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
if containerAwait {
|
||||
exp, err := eaclTable.Marshal()
|
||||
common.ExitOnErr(cmd, "broken EACL table: %w", err)
|
||||
|
||||
cmd.Println("awaiting...")
|
||||
|
||||
var getEACLPrm internalclient.EACLPrm
|
||||
getEACLPrm.SetClient(cli)
|
||||
getEACLPrm.SetContainer(id)
|
||||
|
||||
for i := 0; i < awaitTimeout; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
res, err := internalclient.EACL(getEACLPrm)
|
||||
if err == nil {
|
||||
// compare binary values because EACL could have been set already
|
||||
table := res.EACL()
|
||||
got, err := table.Marshal()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(exp, got) {
|
||||
cmd.Println("EACL has been persisted on sidechain")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "", errSetEACLTimeout)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func initContainerSetEACLCmd() {
|
||||
commonflags.Init(setExtendedACLCmd)
|
||||
|
||||
flags := setExtendedACLCmd.Flags()
|
||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.StringVar(&flagVarsSetEACL.srcPath, "table", "", "path to file with JSON or binary encoded EACL table")
|
||||
flags.BoolVar(&containerAwait, "await", false, "block execution until EACL is persisted")
|
||||
flags.BoolVar(&flagVarsSetEACL.noPreCheck, "no-precheck", false, "do not pre-check the extensibility of the container ACL")
|
||||
}
|
57
cmd/frostfs-cli/modules/container/util.go
Normal file
57
cmd/frostfs-cli/modules/container/util.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
attributeDelimiter = "="
|
||||
|
||||
awaitTimeout = 120 // in seconds
|
||||
)
|
||||
|
||||
var (
|
||||
errCreateTimeout = errors.New("timeout: container has not been persisted on sidechain")
|
||||
errDeleteTimeout = errors.New("timeout: container has not been removed from sidechain")
|
||||
errSetEACLTimeout = errors.New("timeout: EACL has not been persisted on sidechain")
|
||||
)
|
||||
|
||||
func parseContainerID(cmd *cobra.Command) cid.ID {
|
||||
if containerID == "" {
|
||||
common.ExitOnErr(cmd, "", errors.New("container ID is not set"))
|
||||
}
|
||||
|
||||
var id cid.ID
|
||||
err := id.DecodeString(containerID)
|
||||
common.ExitOnErr(cmd, "can't decode container ID value: %w", err)
|
||||
return id
|
||||
}
|
||||
|
||||
// decodes session.Container from the file by path provided in
|
||||
// commonflags.SessionToken flag. Returns nil if the path is not specified.
|
||||
func getSession(cmd *cobra.Command) *session.Container {
|
||||
common.PrintVerbose("Reading container session...")
|
||||
|
||||
path, _ := cmd.Flags().GetString(commonflags.SessionToken)
|
||||
if path == "" {
|
||||
common.PrintVerbose("Session not provided.")
|
||||
return nil
|
||||
}
|
||||
|
||||
common.PrintVerbose("Reading container session from the file [%s]...", path)
|
||||
|
||||
var res session.Container
|
||||
|
||||
err := common.ReadBinaryOrJSON(&res, path)
|
||||
common.ExitOnErr(cmd, "read container session: %v", err)
|
||||
|
||||
common.PrintVerbose("Session successfully read.")
|
||||
|
||||
return &res
|
||||
}
|
59
cmd/frostfs-cli/modules/control/drop_objects.go
Normal file
59
cmd/frostfs-cli/modules/control/drop_objects.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
rawclient "github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const dropObjectsFlag = "objects"
|
||||
|
||||
var dropObjectsCmd = &cobra.Command{
|
||||
Use: "drop-objects",
|
||||
Short: "Drop objects from the node's local storage",
|
||||
Long: "Drop objects from the node's local storage",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
dropObjectsList, _ := cmd.Flags().GetStringSlice(dropObjectsFlag)
|
||||
binAddrList := make([][]byte, len(dropObjectsList))
|
||||
|
||||
for i := range dropObjectsList {
|
||||
binAddrList[i] = []byte(dropObjectsList[i])
|
||||
}
|
||||
|
||||
body := new(control.DropObjectsRequest_Body)
|
||||
body.SetAddressList(binAddrList)
|
||||
|
||||
req := new(control.DropObjectsRequest)
|
||||
req.SetBody(body)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.DropObjectsResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = control.DropObjects(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Objects were successfully marked to be removed.")
|
||||
},
|
||||
}
|
||||
|
||||
func initControlDropObjectsCmd() {
|
||||
initControlFlags(dropObjectsCmd)
|
||||
|
||||
flags := dropObjectsCmd.Flags()
|
||||
flags.StringSliceP(dropObjectsFlag, "o", nil,
|
||||
"List of object addresses to be removed in string format")
|
||||
|
||||
_ = dropObjectsCmd.MarkFlagRequired(dropObjectsFlag)
|
||||
}
|
53
cmd/frostfs-cli/modules/control/evacuate_shard.go
Normal file
53
cmd/frostfs-cli/modules/control/evacuate_shard.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var evacuateShardCmd = &cobra.Command{
|
||||
Use: "evacuate",
|
||||
Short: "Evacuate objects from shard",
|
||||
Long: "Evacuate objects from shard to other shards",
|
||||
Run: evacuateShard,
|
||||
}
|
||||
|
||||
func evacuateShard(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
req := &control.EvacuateShardRequest{Body: new(control.EvacuateShardRequest_Body)}
|
||||
req.Body.Shard_ID = getShardIDList(cmd)
|
||||
req.Body.IgnoreErrors, _ = cmd.Flags().GetBool(dumpIgnoreErrorsFlag)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.EvacuateShardResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.EvacuateShard(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
cmd.Printf("Objects moved: %d\n", resp.GetBody().GetCount())
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Shard has successfully been evacuated.")
|
||||
}
|
||||
|
||||
func initControlEvacuateShardCmd() {
|
||||
initControlFlags(evacuateShardCmd)
|
||||
|
||||
flags := evacuateShardCmd.Flags()
|
||||
flags.StringSlice(shardIDFlag, nil, "List of shard IDs in base58 encoding")
|
||||
flags.Bool(shardAllFlag, false, "Process all shards")
|
||||
flags.Bool(dumpIgnoreErrorsFlag, false, "Skip invalid/unreadable objects")
|
||||
|
||||
evacuateShardCmd.MarkFlagsMutuallyExclusive(shardIDFlag, shardAllFlag)
|
||||
}
|
49
cmd/frostfs-cli/modules/control/flush_cache.go
Normal file
49
cmd/frostfs-cli/modules/control/flush_cache.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flushCacheCmd = &cobra.Command{
|
||||
Use: "flush-cache",
|
||||
Short: "Flush objects from the write-cache to the main storage",
|
||||
Long: "Flush objects from the write-cache to the main storage",
|
||||
Run: flushCache,
|
||||
}
|
||||
|
||||
func flushCache(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
req := &control.FlushCacheRequest{Body: new(control.FlushCacheRequest_Body)}
|
||||
req.Body.Shard_ID = getShardIDList(cmd)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.FlushCacheResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.FlushCache(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Write-cache has been flushed.")
|
||||
}
|
||||
|
||||
func initControlFlushCacheCmd() {
|
||||
initControlFlags(flushCacheCmd)
|
||||
|
||||
ff := flushCacheCmd.Flags()
|
||||
ff.StringSlice(shardIDFlag, nil, "List of shard IDs in base58 encoding")
|
||||
ff.Bool(shardAllFlag, false, "Process all shards")
|
||||
|
||||
flushCacheCmd.MarkFlagsMutuallyExclusive(shardIDFlag, shardAllFlag)
|
||||
}
|
81
cmd/frostfs-cli/modules/control/healthcheck.go
Normal file
81
cmd/frostfs-cli/modules/control/healthcheck.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
|
||||
rawclient "github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
ircontrol "github.com/TrueCloudLab/frostfs-node/pkg/services/control/ir"
|
||||
ircontrolsrv "github.com/TrueCloudLab/frostfs-node/pkg/services/control/ir/server"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
healthcheckIRFlag = "ir"
|
||||
)
|
||||
|
||||
var healthCheckCmd = &cobra.Command{
|
||||
Use: "healthcheck",
|
||||
Short: "Health check of the NeoFS node",
|
||||
Long: "Health check of the NeoFS node. Checks storage node by default, use --ir flag to work with Inner Ring.",
|
||||
Run: healthCheck,
|
||||
}
|
||||
|
||||
func initControlHealthCheckCmd() {
|
||||
initControlFlags(healthCheckCmd)
|
||||
|
||||
flags := healthCheckCmd.Flags()
|
||||
flags.Bool(healthcheckIRFlag, false, "Communicate with IR node")
|
||||
}
|
||||
|
||||
func healthCheck(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
if isIR, _ := cmd.Flags().GetBool(healthcheckIRFlag); isIR {
|
||||
healthCheckIR(cmd, pk, cli)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(control.HealthCheckRequest)
|
||||
req.SetBody(new(control.HealthCheckRequest_Body))
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
var resp *control.HealthCheckResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = control.HealthCheck(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Printf("Network status: %s\n", resp.GetBody().GetNetmapStatus())
|
||||
cmd.Printf("Health status: %s\n", resp.GetBody().GetHealthStatus())
|
||||
}
|
||||
|
||||
func healthCheckIR(cmd *cobra.Command, key *ecdsa.PrivateKey, c *client.Client) {
|
||||
req := new(ircontrol.HealthCheckRequest)
|
||||
|
||||
req.SetBody(new(ircontrol.HealthCheckRequest_Body))
|
||||
|
||||
err := ircontrolsrv.SignMessage(key, req)
|
||||
common.ExitOnErr(cmd, "could not sign request: %w", err)
|
||||
|
||||
var resp *ircontrol.HealthCheckResponse
|
||||
err = c.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = ircontrol.HealthCheck(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Printf("Health status: %s\n", resp.GetBody().GetHealthStatus())
|
||||
}
|
43
cmd/frostfs-cli/modules/control/root.go
Normal file
43
cmd/frostfs-cli/modules/control/root.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "control",
|
||||
Short: "Operations with storage node",
|
||||
Long: `Operations with storage node`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
ff := cmd.Flags()
|
||||
|
||||
_ = viper.BindPFlag(commonflags.WalletPath, ff.Lookup(commonflags.WalletPath))
|
||||
_ = viper.BindPFlag(commonflags.Account, ff.Lookup(commonflags.Account))
|
||||
_ = viper.BindPFlag(controlRPC, ff.Lookup(controlRPC))
|
||||
_ = viper.BindPFlag(commonflags.Timeout, ff.Lookup(commonflags.Timeout))
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
controlRPC = "endpoint"
|
||||
controlRPCDefault = ""
|
||||
controlRPCUsage = "Remote node control address (as 'multiaddr' or '<host>:<port>')"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(
|
||||
healthCheckCmd,
|
||||
setNetmapStatusCmd,
|
||||
dropObjectsCmd,
|
||||
shardsCmd,
|
||||
synchronizeTreeCmd,
|
||||
)
|
||||
|
||||
initControlHealthCheckCmd()
|
||||
initControlSetNetmapStatusCmd()
|
||||
initControlDropObjectsCmd()
|
||||
initControlShardsCmd()
|
||||
initControlSynchronizeTreeCmd()
|
||||
}
|
94
cmd/frostfs-cli/modules/control/set_netmap_status.go
Normal file
94
cmd/frostfs-cli/modules/control/set_netmap_status.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
rawclient "github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
netmapStatusFlag = "status"
|
||||
|
||||
netmapStatusOnline = "online"
|
||||
netmapStatusOffline = "offline"
|
||||
netmapStatusMaintenance = "maintenance"
|
||||
)
|
||||
|
||||
var setNetmapStatusCmd = &cobra.Command{
|
||||
Use: "set-status",
|
||||
Short: "Set status of the storage node in NeoFS network map",
|
||||
Long: "Set status of the storage node in NeoFS network map",
|
||||
Run: setNetmapStatus,
|
||||
}
|
||||
|
||||
func initControlSetNetmapStatusCmd() {
|
||||
initControlFlags(setNetmapStatusCmd)
|
||||
|
||||
flags := setNetmapStatusCmd.Flags()
|
||||
flags.String(netmapStatusFlag, "",
|
||||
fmt.Sprintf("New netmap status keyword ('%s', '%s', '%s')",
|
||||
netmapStatusOnline,
|
||||
netmapStatusOffline,
|
||||
netmapStatusMaintenance,
|
||||
),
|
||||
)
|
||||
|
||||
_ = setNetmapStatusCmd.MarkFlagRequired(netmapStatusFlag)
|
||||
|
||||
flags.BoolP(commonflags.ForceFlag, commonflags.ForceFlagShorthand, false,
|
||||
"Force turning to local maintenance")
|
||||
}
|
||||
|
||||
func setNetmapStatus(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
body := new(control.SetNetmapStatusRequest_Body)
|
||||
force, _ := cmd.Flags().GetBool(commonflags.ForceFlag)
|
||||
|
||||
printIgnoreForce := func(st control.NetmapStatus) {
|
||||
if force {
|
||||
common.PrintVerbose("Ignore --%s flag for %s state.", commonflags.ForceFlag, st)
|
||||
}
|
||||
}
|
||||
|
||||
switch st, _ := cmd.Flags().GetString(netmapStatusFlag); st {
|
||||
default:
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("unsupported status %s", st))
|
||||
case netmapStatusOnline:
|
||||
body.SetStatus(control.NetmapStatus_ONLINE)
|
||||
printIgnoreForce(control.NetmapStatus_ONLINE)
|
||||
case netmapStatusOffline:
|
||||
body.SetStatus(control.NetmapStatus_OFFLINE)
|
||||
printIgnoreForce(control.NetmapStatus_OFFLINE)
|
||||
case netmapStatusMaintenance:
|
||||
body.SetStatus(control.NetmapStatus_MAINTENANCE)
|
||||
|
||||
if force {
|
||||
body.SetForceMaintenance()
|
||||
common.PrintVerbose("Local maintenance will be forced.")
|
||||
}
|
||||
}
|
||||
|
||||
req := new(control.SetNetmapStatusRequest)
|
||||
req.SetBody(body)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.SetNetmapStatusResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = control.SetNetmapStatus(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Network status update request successfully sent.")
|
||||
}
|
27
cmd/frostfs-cli/modules/control/shards.go
Normal file
27
cmd/frostfs-cli/modules/control/shards.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var shardsCmd = &cobra.Command{
|
||||
Use: "shards",
|
||||
Short: "Operations with storage node's shards",
|
||||
Long: "Operations with storage node's shards",
|
||||
}
|
||||
|
||||
func initControlShardsCmd() {
|
||||
shardsCmd.AddCommand(listShardsCmd)
|
||||
shardsCmd.AddCommand(setShardModeCmd)
|
||||
shardsCmd.AddCommand(dumpShardCmd)
|
||||
shardsCmd.AddCommand(restoreShardCmd)
|
||||
shardsCmd.AddCommand(evacuateShardCmd)
|
||||
shardsCmd.AddCommand(flushCacheCmd)
|
||||
|
||||
initControlShardsListCmd()
|
||||
initControlSetShardModeCmd()
|
||||
initControlDumpShardCmd()
|
||||
initControlRestoreShardCmd()
|
||||
initControlEvacuateShardCmd()
|
||||
initControlFlushCacheCmd()
|
||||
}
|
66
cmd/frostfs-cli/modules/control/shards_dump.go
Normal file
66
cmd/frostfs-cli/modules/control/shards_dump.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
dumpFilepathFlag = "path"
|
||||
dumpIgnoreErrorsFlag = "no-errors"
|
||||
)
|
||||
|
||||
var dumpShardCmd = &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: "Dump objects from shard",
|
||||
Long: "Dump objects from shard to a file",
|
||||
Run: dumpShard,
|
||||
}
|
||||
|
||||
func dumpShard(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
body := new(control.DumpShardRequest_Body)
|
||||
body.SetShardID(getShardID(cmd))
|
||||
|
||||
p, _ := cmd.Flags().GetString(dumpFilepathFlag)
|
||||
body.SetFilepath(p)
|
||||
|
||||
ignore, _ := cmd.Flags().GetBool(dumpIgnoreErrorsFlag)
|
||||
body.SetIgnoreErrors(ignore)
|
||||
|
||||
req := new(control.DumpShardRequest)
|
||||
req.SetBody(body)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.DumpShardResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.DumpShard(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Shard has been dumped successfully.")
|
||||
}
|
||||
|
||||
func initControlDumpShardCmd() {
|
||||
initControlFlags(dumpShardCmd)
|
||||
|
||||
flags := dumpShardCmd.Flags()
|
||||
flags.String(shardIDFlag, "", "Shard ID in base58 encoding")
|
||||
flags.String(dumpFilepathFlag, "", "File to write objects to")
|
||||
flags.Bool(dumpIgnoreErrorsFlag, false, "Skip invalid/unreadable objects")
|
||||
|
||||
_ = dumpShardCmd.MarkFlagRequired(shardIDFlag)
|
||||
_ = dumpShardCmd.MarkFlagRequired(dumpFilepathFlag)
|
||||
_ = dumpShardCmd.MarkFlagRequired(controlRPC)
|
||||
}
|
117
cmd/frostfs-cli/modules/control/shards_list.go
Normal file
117
cmd/frostfs-cli/modules/control/shards_list.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
rawclient "github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/mr-tron/base58"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var listShardsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List shards of the storage node",
|
||||
Long: "List shards of the storage node",
|
||||
Run: listShards,
|
||||
}
|
||||
|
||||
func initControlShardsListCmd() {
|
||||
initControlFlags(listShardsCmd)
|
||||
|
||||
flags := listShardsCmd.Flags()
|
||||
flags.Bool(commonflags.JSON, false, "Print shard info as a JSON array")
|
||||
}
|
||||
|
||||
func listShards(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
req := new(control.ListShardsRequest)
|
||||
req.SetBody(new(control.ListShardsRequest_Body))
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.ListShardsResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = control.ListShards(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
isJSON, _ := cmd.Flags().GetBool(commonflags.JSON)
|
||||
if isJSON {
|
||||
prettyPrintShardsJSON(cmd, resp.GetBody().GetShards())
|
||||
} else {
|
||||
prettyPrintShards(cmd, resp.GetBody().GetShards())
|
||||
}
|
||||
}
|
||||
|
||||
func prettyPrintShardsJSON(cmd *cobra.Command, ii []*control.ShardInfo) {
|
||||
out := make([]map[string]interface{}, 0, len(ii))
|
||||
for _, i := range ii {
|
||||
out = append(out, map[string]interface{}{
|
||||
"shard_id": base58.Encode(i.Shard_ID),
|
||||
"mode": shardModeToString(i.GetMode()),
|
||||
"metabase": i.GetMetabasePath(),
|
||||
"blobstor": i.GetBlobstor(),
|
||||
"writecache": i.GetWritecachePath(),
|
||||
"error_count": i.GetErrorCount(),
|
||||
})
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetIndent("", " ")
|
||||
common.ExitOnErr(cmd, "cannot shard info to JSON: %w", enc.Encode(out))
|
||||
|
||||
cmd.Print(buf.String()) // pretty printer emits newline, to no need for Println
|
||||
}
|
||||
|
||||
func prettyPrintShards(cmd *cobra.Command, ii []*control.ShardInfo) {
|
||||
for _, i := range ii {
|
||||
pathPrinter := func(name, path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %s\n", name, path)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Blobstor:\n")
|
||||
for j, info := range i.GetBlobstor() {
|
||||
sb.WriteString(fmt.Sprintf("\tPath %d: %s\n\tType %d: %s\n",
|
||||
j, info.GetPath(), j, info.GetType()))
|
||||
}
|
||||
|
||||
cmd.Printf("Shard %s:\nMode: %s\n"+
|
||||
pathPrinter("Metabase", i.GetMetabasePath())+
|
||||
sb.String()+
|
||||
pathPrinter("Write-cache", i.GetWritecachePath())+
|
||||
pathPrinter("Pilorama", i.GetPiloramaPath())+
|
||||
fmt.Sprintf("Error count: %d\n", i.GetErrorCount()),
|
||||
base58.Encode(i.Shard_ID),
|
||||
shardModeToString(i.GetMode()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func shardModeToString(m control.ShardMode) string {
|
||||
strMode, ok := lookUpShardModeString(m)
|
||||
if ok {
|
||||
return strMode
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
66
cmd/frostfs-cli/modules/control/shards_restore.go
Normal file
66
cmd/frostfs-cli/modules/control/shards_restore.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
restoreFilepathFlag = "path"
|
||||
restoreIgnoreErrorsFlag = "no-errors"
|
||||
)
|
||||
|
||||
var restoreShardCmd = &cobra.Command{
|
||||
Use: "restore",
|
||||
Short: "Restore objects from shard",
|
||||
Long: "Restore objects from shard to a file",
|
||||
Run: restoreShard,
|
||||
}
|
||||
|
||||
func restoreShard(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
body := new(control.RestoreShardRequest_Body)
|
||||
body.SetShardID(getShardID(cmd))
|
||||
|
||||
p, _ := cmd.Flags().GetString(restoreFilepathFlag)
|
||||
body.SetFilepath(p)
|
||||
|
||||
ignore, _ := cmd.Flags().GetBool(restoreIgnoreErrorsFlag)
|
||||
body.SetIgnoreErrors(ignore)
|
||||
|
||||
req := new(control.RestoreShardRequest)
|
||||
req.SetBody(body)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.RestoreShardResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.RestoreShard(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Shard has been restored successfully.")
|
||||
}
|
||||
|
||||
func initControlRestoreShardCmd() {
|
||||
initControlFlags(restoreShardCmd)
|
||||
|
||||
flags := restoreShardCmd.Flags()
|
||||
flags.String(shardIDFlag, "", "Shard ID in base58 encoding")
|
||||
flags.String(restoreFilepathFlag, "", "File to read objects from")
|
||||
flags.Bool(restoreIgnoreErrorsFlag, false, "Skip invalid/unreadable objects")
|
||||
|
||||
_ = restoreShardCmd.MarkFlagRequired(shardIDFlag)
|
||||
_ = restoreShardCmd.MarkFlagRequired(restoreFilepathFlag)
|
||||
_ = restoreShardCmd.MarkFlagRequired(controlRPC)
|
||||
}
|
177
cmd/frostfs-cli/modules/control/shards_set_mode.go
Normal file
177
cmd/frostfs-cli/modules/control/shards_set_mode.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
rawclient "github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
"github.com/mr-tron/base58"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
shardModeFlag = "mode"
|
||||
shardIDFlag = "id"
|
||||
shardAllFlag = "all"
|
||||
shardClearErrorsFlag = "clear-errors"
|
||||
)
|
||||
|
||||
// maps string command input to control.ShardMode. To support new mode, it's
|
||||
// enough to add the map entry. Modes are automatically printed in command help
|
||||
// messages.
|
||||
var mShardModes = map[string]struct {
|
||||
val control.ShardMode
|
||||
|
||||
// flag to support shard mode implicitly without help message. The flag is set
|
||||
// for values which are not expected to be set by users but still supported
|
||||
// for developers.
|
||||
unsafe bool
|
||||
}{
|
||||
"read-only": {val: control.ShardMode_READ_ONLY},
|
||||
"read-write": {val: control.ShardMode_READ_WRITE},
|
||||
"degraded-read-write": {val: control.ShardMode_DEGRADED, unsafe: true},
|
||||
"degraded-read-only": {val: control.ShardMode_DEGRADED_READ_ONLY},
|
||||
}
|
||||
|
||||
// iterates over string representations of safe supported shard modes. Safe means
|
||||
// modes which are expected to be used by any user. All other supported modes
|
||||
// are for developers only.
|
||||
func iterateSafeShardModes(f func(string)) {
|
||||
for strMode, mode := range mShardModes {
|
||||
if !mode.unsafe {
|
||||
f(strMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// looks up for supported control.ShardMode represented by the given string.
|
||||
// Returns false if no corresponding mode exists.
|
||||
func lookUpShardModeFromString(str string) (control.ShardMode, bool) {
|
||||
mode, ok := mShardModes[str]
|
||||
if !ok {
|
||||
return control.ShardMode_SHARD_MODE_UNDEFINED, false
|
||||
}
|
||||
|
||||
return mode.val, true
|
||||
}
|
||||
|
||||
// looks up for string representation of supported shard mode. Returns false
|
||||
// if mode is not supported.
|
||||
func lookUpShardModeString(m control.ShardMode) (string, bool) {
|
||||
for strMode, mode := range mShardModes {
|
||||
if mode.val == m {
|
||||
return strMode, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
var setShardModeCmd = &cobra.Command{
|
||||
Use: "set-mode",
|
||||
Short: "Set work mode of the shard",
|
||||
Long: "Set work mode of the shard",
|
||||
Run: setShardMode,
|
||||
}
|
||||
|
||||
func initControlSetShardModeCmd() {
|
||||
initControlFlags(setShardModeCmd)
|
||||
|
||||
flags := setShardModeCmd.Flags()
|
||||
flags.StringSlice(shardIDFlag, nil, "List of shard IDs in base58 encoding")
|
||||
flags.Bool(shardAllFlag, false, "Process all shards")
|
||||
|
||||
modes := make([]string, 0)
|
||||
|
||||
iterateSafeShardModes(func(strMode string) {
|
||||
modes = append(modes, "'"+strMode+"'")
|
||||
})
|
||||
|
||||
flags.String(shardModeFlag, "",
|
||||
fmt.Sprintf("New shard mode (%s)", strings.Join(modes, ", ")),
|
||||
)
|
||||
flags.Bool(shardClearErrorsFlag, false, "Set shard error count to 0")
|
||||
|
||||
setShardModeCmd.MarkFlagsMutuallyExclusive(shardIDFlag, shardAllFlag)
|
||||
}
|
||||
|
||||
func setShardMode(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
strMode, _ := cmd.Flags().GetString(shardModeFlag)
|
||||
|
||||
mode, ok := lookUpShardModeFromString(strMode)
|
||||
if !ok {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("unsupported mode %s", strMode))
|
||||
}
|
||||
|
||||
req := new(control.SetShardModeRequest)
|
||||
|
||||
body := new(control.SetShardModeRequest_Body)
|
||||
req.SetBody(body)
|
||||
|
||||
body.SetMode(mode)
|
||||
body.SetShardIDList(getShardIDList(cmd))
|
||||
|
||||
reset, _ := cmd.Flags().GetBool(shardClearErrorsFlag)
|
||||
body.ClearErrorCounter(reset)
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.SetShardModeResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = control.SetShardMode(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Shard mode update request successfully sent.")
|
||||
}
|
||||
|
||||
func getShardID(cmd *cobra.Command) []byte {
|
||||
sid, _ := cmd.Flags().GetString(shardIDFlag)
|
||||
raw, err := base58.Decode(sid)
|
||||
common.ExitOnErr(cmd, "incorrect shard ID encoding: %w", err)
|
||||
return raw
|
||||
}
|
||||
|
||||
func getShardIDList(cmd *cobra.Command) [][]byte {
|
||||
all, _ := cmd.Flags().GetBool(shardAllFlag)
|
||||
if all {
|
||||
return nil
|
||||
}
|
||||
|
||||
sidList, _ := cmd.Flags().GetStringSlice(shardIDFlag)
|
||||
if len(sidList) == 0 {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("either --%s or --%s flag must be provided", shardIDFlag, shardAllFlag))
|
||||
}
|
||||
|
||||
// We can sort the ID list and perform this check without additional allocations,
|
||||
// but preserving the user order is a nice thing to have.
|
||||
// Also, this is a CLI, we don't care too much about this.
|
||||
seen := make(map[string]struct{})
|
||||
for i := range sidList {
|
||||
if _, ok := seen[sidList[i]]; ok {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("duplicated shard IDs: %s", sidList[i]))
|
||||
}
|
||||
seen[sidList[i]] = struct{}{}
|
||||
}
|
||||
|
||||
res := make([][]byte, 0, len(sidList))
|
||||
for i := range sidList {
|
||||
raw, err := base58.Decode(sidList[i])
|
||||
common.ExitOnErr(cmd, "incorrect shard ID encoding: %w", err)
|
||||
|
||||
res = append(res, raw)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
78
cmd/frostfs-cli/modules/control/synchronize_tree.go
Normal file
78
cmd/frostfs-cli/modules/control/synchronize_tree.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
|
||||
rawclient "github.com/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
controlSvc "github.com/TrueCloudLab/frostfs-node/pkg/services/control/server"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
synchronizeTreeIDFlag = "tree-id"
|
||||
synchronizeTreeHeightFlag = "height"
|
||||
)
|
||||
|
||||
var synchronizeTreeCmd = &cobra.Command{
|
||||
Use: "synchronize-tree",
|
||||
Short: "Synchronize log for the tree",
|
||||
Long: "Synchronize log for the tree in an object tree service.",
|
||||
Run: synchronizeTree,
|
||||
}
|
||||
|
||||
func initControlSynchronizeTreeCmd() {
|
||||
initControlFlags(synchronizeTreeCmd)
|
||||
|
||||
flags := synchronizeTreeCmd.Flags()
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.String(synchronizeTreeIDFlag, "", "Tree ID")
|
||||
flags.Uint64(synchronizeTreeHeightFlag, 0, "Starting height")
|
||||
}
|
||||
|
||||
func synchronizeTree(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
cidStr, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
common.ExitOnErr(cmd, "can't decode container ID: %w", cnr.DecodeString(cidStr))
|
||||
|
||||
treeID, _ := cmd.Flags().GetString("tree-id")
|
||||
if treeID == "" {
|
||||
common.ExitOnErr(cmd, "", errors.New("tree ID must not be empty"))
|
||||
}
|
||||
|
||||
height, _ := cmd.Flags().GetUint64("height")
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
req := &control.SynchronizeTreeRequest{
|
||||
Body: &control.SynchronizeTreeRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
TreeId: treeID,
|
||||
Height: height,
|
||||
},
|
||||
}
|
||||
|
||||
err := controlSvc.SignMessage(pk, req)
|
||||
common.ExitOnErr(cmd, "could not sign request: %w", err)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.SynchronizeTreeResponse
|
||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
||||
resp, err = control.SynchronizeTree(client, req)
|
||||
return err
|
||||
})
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Tree has been synchronized successfully.")
|
||||
}
|
59
cmd/frostfs-cli/modules/control/util.go
Normal file
59
cmd/frostfs-cli/modules/control/util.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
controlSvc "github.com/TrueCloudLab/frostfs-node/pkg/services/control/server"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
frostfscrypto "github.com/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func initControlFlags(cmd *cobra.Command) {
|
||||
ff := cmd.Flags()
|
||||
ff.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||
ff.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
||||
ff.String(controlRPC, controlRPCDefault, controlRPCUsage)
|
||||
ff.DurationP(commonflags.Timeout, commonflags.TimeoutShorthand, commonflags.TimeoutDefault, commonflags.TimeoutUsage)
|
||||
}
|
||||
|
||||
func signRequest(cmd *cobra.Command, pk *ecdsa.PrivateKey, req controlSvc.SignedMessage) {
|
||||
err := controlSvc.SignMessage(pk, req)
|
||||
common.ExitOnErr(cmd, "could not sign request: %w", err)
|
||||
}
|
||||
|
||||
func verifyResponse(cmd *cobra.Command,
|
||||
sigControl interface {
|
||||
GetKey() []byte
|
||||
GetSign() []byte
|
||||
},
|
||||
body interface {
|
||||
StableMarshal([]byte) []byte
|
||||
},
|
||||
) {
|
||||
if sigControl == nil {
|
||||
common.ExitOnErr(cmd, "", errors.New("missing response signature"))
|
||||
}
|
||||
|
||||
// TODO(@cthulhu-rider): #1387 use Signature message from NeoFS API to avoid conversion
|
||||
var sigV2 refs.Signature
|
||||
sigV2.SetScheme(refs.ECDSA_SHA512)
|
||||
sigV2.SetKey(sigControl.GetKey())
|
||||
sigV2.SetSign(sigControl.GetSign())
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
common.ExitOnErr(cmd, "can't read signature: %w", sig.ReadFromV2(sigV2))
|
||||
|
||||
if !sig.Verify(body.StableMarshal(nil)) {
|
||||
common.ExitOnErr(cmd, "", errors.New("invalid response signature"))
|
||||
}
|
||||
}
|
||||
|
||||
func getClient(cmd *cobra.Command, pk *ecdsa.PrivateKey) *client.Client {
|
||||
return internalclient.GetSDKClientByFlag(cmd, pk, controlRPC)
|
||||
}
|
34
cmd/frostfs-cli/modules/netmap/get_epoch.go
Normal file
34
cmd/frostfs-cli/modules/netmap/get_epoch.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getEpochCmd = &cobra.Command{
|
||||
Use: "epoch",
|
||||
Short: "Get current epoch number",
|
||||
Long: "Get current epoch number",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
p := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, p, commonflags.RPC)
|
||||
|
||||
var prm internalclient.NetworkInfoPrm
|
||||
prm.SetClient(cli)
|
||||
|
||||
res, err := internalclient.NetworkInfo(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
netInfo := res.NetworkInfo()
|
||||
|
||||
cmd.Println(netInfo.CurrentEpoch())
|
||||
},
|
||||
}
|
||||
|
||||
func initGetEpochCmd() {
|
||||
commonflags.Init(getEpochCmd)
|
||||
commonflags.InitAPI(getEpochCmd)
|
||||
}
|
63
cmd/frostfs-cli/modules/netmap/netinfo.go
Normal file
63
cmd/frostfs-cli/modules/netmap/netinfo.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var netInfoCmd = &cobra.Command{
|
||||
Use: "netinfo",
|
||||
Short: "Get information about NeoFS network",
|
||||
Long: "Get information about NeoFS network",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
p := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, p, commonflags.RPC)
|
||||
|
||||
var prm internalclient.NetworkInfoPrm
|
||||
prm.SetClient(cli)
|
||||
|
||||
res, err := internalclient.NetworkInfo(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
netInfo := res.NetworkInfo()
|
||||
|
||||
cmd.Printf("Epoch: %d\n", netInfo.CurrentEpoch())
|
||||
|
||||
magic := netInfo.MagicNumber()
|
||||
cmd.Printf("Network magic: [%s] %d\n", netmode.Magic(magic), magic)
|
||||
|
||||
cmd.Printf("Time per block: %s\n", time.Duration(netInfo.MsPerBlock())*time.Millisecond)
|
||||
|
||||
const format = " %s: %v\n"
|
||||
|
||||
cmd.Println("NeoFS network configuration (system)")
|
||||
cmd.Printf(format, "Audit fee", netInfo.AuditFee())
|
||||
cmd.Printf(format, "Storage price", netInfo.StoragePrice())
|
||||
cmd.Printf(format, "Container fee", netInfo.ContainerFee())
|
||||
cmd.Printf(format, "EigenTrust alpha", netInfo.EigenTrustAlpha())
|
||||
cmd.Printf(format, "Number of EigenTrust iterations", netInfo.NumberOfEigenTrustIterations())
|
||||
cmd.Printf(format, "Epoch duration", netInfo.EpochDuration())
|
||||
cmd.Printf(format, "Inner Ring candidate fee", netInfo.IRCandidateFee())
|
||||
cmd.Printf(format, "Maximum object size", netInfo.MaxObjectSize())
|
||||
cmd.Printf(format, "Withdrawal fee", netInfo.WithdrawalFee())
|
||||
cmd.Printf(format, "Homomorphic hashing disabled", netInfo.HomomorphicHashingDisabled())
|
||||
cmd.Printf(format, "Maintenance mode allowed", netInfo.MaintenanceModeAllowed())
|
||||
|
||||
cmd.Println("NeoFS network configuration (other)")
|
||||
netInfo.IterateRawNetworkParameters(func(name string, value []byte) {
|
||||
cmd.Printf(format, name, hex.EncodeToString(value))
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
func initNetInfoCmd() {
|
||||
commonflags.Init(netInfoCmd)
|
||||
commonflags.InitAPI(netInfoCmd)
|
||||
}
|
70
cmd/frostfs-cli/modules/netmap/nodeinfo.go
Normal file
70
cmd/frostfs-cli/modules/netmap/nodeinfo.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const nodeInfoJSONFlag = commonflags.JSON
|
||||
|
||||
var nodeInfoCmd = &cobra.Command{
|
||||
Use: "nodeinfo",
|
||||
Short: "Get target node info",
|
||||
Long: `Get target node info`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
p := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, p, commonflags.RPC)
|
||||
|
||||
var prm internalclient.NodeInfoPrm
|
||||
prm.SetClient(cli)
|
||||
|
||||
res, err := internalclient.NodeInfo(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
prettyPrintNodeInfo(cmd, res.NodeInfo())
|
||||
},
|
||||
}
|
||||
|
||||
func initNodeInfoCmd() {
|
||||
commonflags.Init(nodeInfoCmd)
|
||||
commonflags.InitAPI(nodeInfoCmd)
|
||||
nodeInfoCmd.Flags().Bool(nodeInfoJSONFlag, false, "Print node info in JSON format")
|
||||
}
|
||||
|
||||
func prettyPrintNodeInfo(cmd *cobra.Command, i netmap.NodeInfo) {
|
||||
isJSON, _ := cmd.Flags().GetBool(nodeInfoJSONFlag)
|
||||
if isJSON {
|
||||
common.PrettyPrintJSON(cmd, i, "node info")
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Println("key:", hex.EncodeToString(i.PublicKey()))
|
||||
|
||||
var stateWord string
|
||||
switch {
|
||||
default:
|
||||
stateWord = "<undefined>"
|
||||
case i.IsOnline():
|
||||
stateWord = "online"
|
||||
case i.IsOffline():
|
||||
stateWord = "offline"
|
||||
case i.IsMaintenance():
|
||||
stateWord = "maintenance"
|
||||
}
|
||||
|
||||
cmd.Println("state:", stateWord)
|
||||
|
||||
netmap.IterateNetworkEndpoints(i, func(s string) {
|
||||
cmd.Println("address:", s)
|
||||
})
|
||||
|
||||
i.IterateAttributes(func(key, value string) {
|
||||
cmd.Printf("attribute: %s=%s\n", key, value)
|
||||
})
|
||||
}
|
32
cmd/frostfs-cli/modules/netmap/root.go
Normal file
32
cmd/frostfs-cli/modules/netmap/root.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "netmap",
|
||||
Short: "Operations with Network Map",
|
||||
Long: `Operations with Network Map`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// bind exactly that cmd's flags to
|
||||
// the viper before execution
|
||||
commonflags.Bind(cmd)
|
||||
commonflags.BindAPI(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(
|
||||
getEpochCmd,
|
||||
nodeInfoCmd,
|
||||
netInfoCmd,
|
||||
snapshotCmd,
|
||||
)
|
||||
|
||||
initGetEpochCmd()
|
||||
initNetInfoCmd()
|
||||
initNodeInfoCmd()
|
||||
initSnapshotCmd()
|
||||
}
|
32
cmd/frostfs-cli/modules/netmap/snapshot.go
Normal file
32
cmd/frostfs-cli/modules/netmap/snapshot.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package netmap
|
||||
|
||||
import (
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var snapshotCmd = &cobra.Command{
|
||||
Use: "snapshot",
|
||||
Short: "Request current local snapshot of the network map",
|
||||
Long: `Request current local snapshot of the network map`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
p := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, p, commonflags.RPC)
|
||||
|
||||
var prm internalclient.NetMapSnapshotPrm
|
||||
prm.SetClient(cli)
|
||||
|
||||
res, err := internalclient.NetMapSnapshot(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
common.PrettyPrintNetMap(cmd, res.NetMap())
|
||||
},
|
||||
}
|
||||
|
||||
func initSnapshotCmd() {
|
||||
commonflags.Init(snapshotCmd)
|
||||
commonflags.InitAPI(snapshotCmd)
|
||||
}
|
75
cmd/frostfs-cli/modules/object/delete.go
Normal file
75
cmd/frostfs-cli/modules/object/delete.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var objectDelCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Aliases: []string{"del"},
|
||||
Short: "Delete object from NeoFS",
|
||||
Long: "Delete object from NeoFS",
|
||||
Run: deleteObject,
|
||||
}
|
||||
|
||||
func initObjectDeleteCmd() {
|
||||
commonflags.Init(objectDelCmd)
|
||||
initFlagSession(objectDelCmd, "DELETE")
|
||||
|
||||
flags := objectDelCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
|
||||
flags.Bool(binaryFlag, false, "Deserialize object structure from given file.")
|
||||
flags.String(fileFlag, "", "File with object payload")
|
||||
}
|
||||
|
||||
func deleteObject(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
var objAddr oid.Address
|
||||
|
||||
binary, _ := cmd.Flags().GetBool(binaryFlag)
|
||||
if binary {
|
||||
filename, _ := cmd.Flags().GetString(fileFlag)
|
||||
if filename == "" {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("required flag \"%s\" not set", fileFlag))
|
||||
}
|
||||
objAddr = readObjectAddressBin(cmd, &cnr, &obj, filename)
|
||||
} else {
|
||||
cidVal, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
if cidVal == "" {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("required flag \"%s\" not set", commonflags.CIDFlag))
|
||||
}
|
||||
|
||||
oidVal, _ := cmd.Flags().GetString(commonflags.OIDFlag)
|
||||
if oidVal == "" {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("required flag \"%s\" not set", commonflags.OIDFlag))
|
||||
}
|
||||
|
||||
objAddr = readObjectAddress(cmd, &cnr, &obj)
|
||||
}
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
var prm internalclient.DeleteObjectPrm
|
||||
ReadOrOpenSession(cmd, &prm, pk, cnr, &obj)
|
||||
Prepare(cmd, &prm)
|
||||
prm.SetAddress(objAddr)
|
||||
|
||||
res, err := internalclient.DeleteObject(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
tomb := res.Tombstone()
|
||||
|
||||
cmd.Println("Object removed successfully.")
|
||||
cmd.Printf(" ID: %s\n CID: %s\n", tomb, cnr)
|
||||
}
|
141
cmd/frostfs-cli/modules/object/get.go
Normal file
141
cmd/frostfs-cli/modules/object/get.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var objectGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get object from NeoFS",
|
||||
Long: "Get object from NeoFS",
|
||||
Run: getObject,
|
||||
}
|
||||
|
||||
func initObjectGetCmd() {
|
||||
commonflags.Init(objectGetCmd)
|
||||
initFlagSession(objectGetCmd, "GET")
|
||||
|
||||
flags := objectGetCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = objectGetCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
|
||||
_ = objectGetCmd.MarkFlagRequired(commonflags.OIDFlag)
|
||||
|
||||
flags.String(fileFlag, "", "File to write object payload to(with -b together with signature and header). Default: stdout.")
|
||||
flags.Bool(rawFlag, false, rawFlagDesc)
|
||||
flags.Bool(noProgressFlag, false, "Do not show progress bar")
|
||||
flags.Bool(binaryFlag, false, "Serialize whole object structure into given file(id + signature + header + payload).")
|
||||
}
|
||||
|
||||
func getObject(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
|
||||
objAddr := readObjectAddress(cmd, &cnr, &obj)
|
||||
|
||||
var out io.Writer
|
||||
filename := cmd.Flag(fileFlag).Value.String()
|
||||
if filename == "" {
|
||||
out = os.Stdout
|
||||
} else {
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err))
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
out = f
|
||||
}
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.GetObjectPrm
|
||||
prm.SetClient(cli)
|
||||
Prepare(cmd, &prm)
|
||||
readSession(cmd, &prm, pk, cnr, obj)
|
||||
|
||||
raw, _ := cmd.Flags().GetBool(rawFlag)
|
||||
prm.SetRawFlag(raw)
|
||||
prm.SetAddress(objAddr)
|
||||
|
||||
var p *pb.ProgressBar
|
||||
noProgress, _ := cmd.Flags().GetBool(noProgressFlag)
|
||||
|
||||
var payloadWriter io.Writer
|
||||
var payloadBuffer *bytes.Buffer
|
||||
binary, _ := cmd.Flags().GetBool(binaryFlag)
|
||||
if binary {
|
||||
payloadBuffer = new(bytes.Buffer)
|
||||
payloadWriter = payloadBuffer
|
||||
} else {
|
||||
payloadWriter = out
|
||||
}
|
||||
|
||||
if filename == "" || noProgress {
|
||||
prm.SetPayloadWriter(payloadWriter)
|
||||
} else {
|
||||
p = pb.New64(0)
|
||||
p.Output = cmd.OutOrStdout()
|
||||
prm.SetPayloadWriter(p.NewProxyWriter(payloadWriter))
|
||||
prm.SetHeaderCallback(func(o *object.Object) {
|
||||
p.SetTotal64(int64(o.PayloadSize()))
|
||||
p.Start()
|
||||
})
|
||||
}
|
||||
|
||||
res, err := internalclient.GetObject(prm)
|
||||
if p != nil {
|
||||
p.Finish()
|
||||
}
|
||||
if err != nil {
|
||||
if ok := printSplitInfoErr(cmd, err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
}
|
||||
|
||||
if binary {
|
||||
objToStore := res.Header()
|
||||
//TODO(@acid-ant): #1932 Use streams to marshal/unmarshal payload
|
||||
objToStore.SetPayload(payloadBuffer.Bytes())
|
||||
objBytes, err := objToStore.Marshal()
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
_, err = out.Write(objBytes)
|
||||
common.ExitOnErr(cmd, "unable to write binary object in out: %w ", err)
|
||||
}
|
||||
|
||||
if filename != "" && !strictOutput(cmd) {
|
||||
cmd.Printf("[%s] Object successfully saved\n", filename)
|
||||
}
|
||||
|
||||
// Print header only if file is not streamed to stdout.
|
||||
if filename != "" {
|
||||
err = printHeader(cmd, res.Header())
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
}
|
||||
}
|
||||
|
||||
func strictOutput(cmd *cobra.Command) bool {
|
||||
toJSON, _ := cmd.Flags().GetBool(commonflags.JSON)
|
||||
toProto, _ := cmd.Flags().GetBool("proto")
|
||||
return toJSON || toProto
|
||||
}
|
130
cmd/frostfs-cli/modules/object/hash.go
Normal file
130
cmd/frostfs-cli/modules/object/hash.go
Normal file
|
@ -0,0 +1,130 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const getRangeHashSaltFlag = "salt"
|
||||
|
||||
const (
|
||||
hashSha256 = "sha256"
|
||||
hashTz = "tz"
|
||||
rangeSep = ":"
|
||||
)
|
||||
|
||||
var objectHashCmd = &cobra.Command{
|
||||
Use: "hash",
|
||||
Short: "Get object hash",
|
||||
Long: "Get object hash",
|
||||
Run: getObjectHash,
|
||||
}
|
||||
|
||||
func initObjectHashCmd() {
|
||||
commonflags.Init(objectHashCmd)
|
||||
initFlagSession(objectHashCmd, "RANGEHASH")
|
||||
|
||||
flags := objectHashCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = objectHashCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
|
||||
_ = objectHashCmd.MarkFlagRequired(commonflags.OIDFlag)
|
||||
|
||||
flags.String("range", "", "Range to take hash from in the form offset1:length1,...")
|
||||
flags.String("type", hashSha256, "Hash type. Either 'sha256' or 'tz'")
|
||||
flags.String(getRangeHashSaltFlag, "", "Salt in hex format")
|
||||
}
|
||||
|
||||
func getObjectHash(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
|
||||
objAddr := readObjectAddress(cmd, &cnr, &obj)
|
||||
|
||||
ranges, err := getRangeList(cmd)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
typ, err := getHashType(cmd)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
strSalt := strings.TrimPrefix(cmd.Flag(getRangeHashSaltFlag).Value.String(), "0x")
|
||||
|
||||
salt, err := hex.DecodeString(strSalt)
|
||||
common.ExitOnErr(cmd, "could not decode salt: %w", err)
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
tz := typ == hashTz
|
||||
fullHash := len(ranges) == 0
|
||||
if fullHash {
|
||||
var headPrm internalclient.HeadObjectPrm
|
||||
headPrm.SetClient(cli)
|
||||
Prepare(cmd, &headPrm)
|
||||
headPrm.SetAddress(objAddr)
|
||||
|
||||
// get hash of full payload through HEAD (may be user can do it through dedicated command?)
|
||||
res, err := internalclient.HeadObject(headPrm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
var cs checksum.Checksum
|
||||
var csSet bool
|
||||
|
||||
if tz {
|
||||
cs, csSet = res.Header().PayloadHomomorphicHash()
|
||||
} else {
|
||||
cs, csSet = res.Header().PayloadChecksum()
|
||||
}
|
||||
|
||||
if csSet {
|
||||
cmd.Println(hex.EncodeToString(cs.Value()))
|
||||
} else {
|
||||
cmd.Println("Missing checksum in object header.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var hashPrm internalclient.HashPayloadRangesPrm
|
||||
hashPrm.SetClient(cli)
|
||||
Prepare(cmd, &hashPrm)
|
||||
readSession(cmd, &hashPrm, pk, cnr, obj)
|
||||
hashPrm.SetAddress(objAddr)
|
||||
hashPrm.SetSalt(salt)
|
||||
hashPrm.SetRanges(ranges)
|
||||
|
||||
if tz {
|
||||
hashPrm.TZ()
|
||||
}
|
||||
|
||||
res, err := internalclient.HashPayloadRanges(hashPrm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
hs := res.HashList()
|
||||
|
||||
for i := range hs {
|
||||
cmd.Printf("Offset=%d (Length=%d)\t: %s\n", ranges[i].GetOffset(), ranges[i].GetLength(),
|
||||
hex.EncodeToString(hs[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func getHashType(cmd *cobra.Command) (string, error) {
|
||||
rawType := cmd.Flag("type").Value.String()
|
||||
switch typ := strings.ToLower(rawType); typ {
|
||||
case hashSha256, hashTz:
|
||||
return typ, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid hash type: %s", typ)
|
||||
}
|
||||
}
|
202
cmd/frostfs-cli/modules/object/head.go
Normal file
202
cmd/frostfs-cli/modules/object/head.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
oidSDK "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var objectHeadCmd = &cobra.Command{
|
||||
Use: "head",
|
||||
Short: "Get object header",
|
||||
Long: "Get object header",
|
||||
Run: getObjectHeader,
|
||||
}
|
||||
|
||||
func initObjectHeadCmd() {
|
||||
commonflags.Init(objectHeadCmd)
|
||||
initFlagSession(objectHeadCmd, "HEAD")
|
||||
|
||||
flags := objectHeadCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = objectHeadCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
|
||||
_ = objectHeadCmd.MarkFlagRequired(commonflags.OIDFlag)
|
||||
|
||||
flags.String(fileFlag, "", "File to write header to. Default: stdout.")
|
||||
flags.Bool("main-only", false, "Return only main fields")
|
||||
flags.Bool(commonflags.JSON, false, "Marshal output in JSON")
|
||||
flags.Bool("proto", false, "Marshal output in Protobuf")
|
||||
flags.Bool(rawFlag, false, rawFlagDesc)
|
||||
}
|
||||
|
||||
func getObjectHeader(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
|
||||
objAddr := readObjectAddress(cmd, &cnr, &obj)
|
||||
mainOnly, _ := cmd.Flags().GetBool("main-only")
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.HeadObjectPrm
|
||||
prm.SetClient(cli)
|
||||
Prepare(cmd, &prm)
|
||||
readSession(cmd, &prm, pk, cnr, obj)
|
||||
|
||||
raw, _ := cmd.Flags().GetBool(rawFlag)
|
||||
prm.SetRawFlag(raw)
|
||||
prm.SetAddress(objAddr)
|
||||
prm.SetMainOnlyFlag(mainOnly)
|
||||
|
||||
res, err := internalclient.HeadObject(prm)
|
||||
if err != nil {
|
||||
if ok := printSplitInfoErr(cmd, err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
}
|
||||
|
||||
err = saveAndPrintHeader(cmd, res.Header(), cmd.Flag(fileFlag).Value.String())
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
}
|
||||
|
||||
func saveAndPrintHeader(cmd *cobra.Command, obj *object.Object, filename string) error {
|
||||
bs, err := marshalHeader(cmd, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal header: %w", err)
|
||||
}
|
||||
if len(bs) != 0 {
|
||||
if filename == "" {
|
||||
cmd.Println(string(bs))
|
||||
return nil
|
||||
}
|
||||
err = os.WriteFile(filename, bs, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write header to file: %w", err)
|
||||
}
|
||||
cmd.Printf("[%s] Header successfully saved.", filename)
|
||||
}
|
||||
|
||||
return printHeader(cmd, obj)
|
||||
}
|
||||
|
||||
func marshalHeader(cmd *cobra.Command, hdr *object.Object) ([]byte, error) {
|
||||
toJSON, _ := cmd.Flags().GetBool(commonflags.JSON)
|
||||
toProto, _ := cmd.Flags().GetBool("proto")
|
||||
switch {
|
||||
case toJSON && toProto:
|
||||
return nil, errors.New("'--json' and '--proto' flags are mutually exclusive")
|
||||
case toJSON:
|
||||
return hdr.MarshalJSON()
|
||||
case toProto:
|
||||
return hdr.Marshal()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func printObjectID(cmd *cobra.Command, recv func() (oidSDK.ID, bool)) {
|
||||
var strID string
|
||||
|
||||
id, ok := recv()
|
||||
if ok {
|
||||
strID = id.String()
|
||||
} else {
|
||||
strID = "<empty>"
|
||||
}
|
||||
|
||||
cmd.Printf("ID: %s\n", strID)
|
||||
}
|
||||
|
||||
func printContainerID(cmd *cobra.Command, recv func() (cid.ID, bool)) {
|
||||
var strID string
|
||||
|
||||
id, ok := recv()
|
||||
if ok {
|
||||
strID = id.String()
|
||||
} else {
|
||||
strID = "<empty>"
|
||||
}
|
||||
|
||||
cmd.Printf("CID: %s\n", strID)
|
||||
}
|
||||
|
||||
func printHeader(cmd *cobra.Command, obj *object.Object) error {
|
||||
printObjectID(cmd, obj.ID)
|
||||
printContainerID(cmd, obj.ContainerID)
|
||||
cmd.Printf("Owner: %s\n", obj.OwnerID())
|
||||
cmd.Printf("CreatedAt: %d\n", obj.CreationEpoch())
|
||||
cmd.Printf("Size: %d\n", obj.PayloadSize())
|
||||
common.PrintChecksum(cmd, "HomoHash", obj.PayloadHomomorphicHash)
|
||||
common.PrintChecksum(cmd, "Checksum", obj.PayloadChecksum)
|
||||
cmd.Printf("Type: %s\n", obj.Type())
|
||||
|
||||
cmd.Println("Attributes:")
|
||||
for _, attr := range obj.Attributes() {
|
||||
if attr.Key() == object.AttributeTimestamp {
|
||||
cmd.Printf(" %s=%s (%s)\n",
|
||||
attr.Key(),
|
||||
attr.Value(),
|
||||
common.PrettyPrintUnixTime(attr.Value()))
|
||||
continue
|
||||
}
|
||||
cmd.Printf(" %s=%s\n", attr.Key(), attr.Value())
|
||||
}
|
||||
|
||||
if signature := obj.Signature(); signature != nil {
|
||||
cmd.Print("ID signature:\n")
|
||||
|
||||
// TODO(@carpawell): #1387 implement and use another approach to avoid conversion
|
||||
var sigV2 refs.Signature
|
||||
signature.WriteToV2(&sigV2)
|
||||
|
||||
cmd.Printf(" public key: %s\n", hex.EncodeToString(sigV2.GetKey()))
|
||||
cmd.Printf(" signature: %s\n", hex.EncodeToString(sigV2.GetSign()))
|
||||
}
|
||||
|
||||
return printSplitHeader(cmd, obj)
|
||||
}
|
||||
|
||||
func printSplitHeader(cmd *cobra.Command, obj *object.Object) error {
|
||||
if splitID := obj.SplitID(); splitID != nil {
|
||||
cmd.Printf("Split ID: %s\n", splitID)
|
||||
}
|
||||
|
||||
if oid, ok := obj.ParentID(); ok {
|
||||
cmd.Printf("Split ParentID: %s\n", oid)
|
||||
}
|
||||
|
||||
if prev, ok := obj.PreviousID(); ok {
|
||||
cmd.Printf("Split PreviousID: %s\n", prev)
|
||||
}
|
||||
|
||||
for _, child := range obj.Children() {
|
||||
cmd.Printf("Split ChildID: %s\n", child.String())
|
||||
}
|
||||
|
||||
parent := obj.Parent()
|
||||
if parent != nil {
|
||||
cmd.Print("\nSplit Parent Header:\n")
|
||||
|
||||
return printHeader(cmd, parent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
111
cmd/frostfs-cli/modules/object/lock.go
Normal file
111
cmd/frostfs-cli/modules/object/lock.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
objectV2 "github.com/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
objectSDK "github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// object lock command.
|
||||
var objectLockCmd = &cobra.Command{
|
||||
Use: "lock",
|
||||
Short: "Lock object in container",
|
||||
Long: "Lock object in container",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
cidRaw, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
|
||||
var cnr cid.ID
|
||||
err := cnr.DecodeString(cidRaw)
|
||||
common.ExitOnErr(cmd, "Incorrect container arg: %v", err)
|
||||
|
||||
oidsRaw, _ := cmd.Flags().GetStringSlice(commonflags.OIDFlag)
|
||||
|
||||
lockList := make([]oid.ID, len(oidsRaw))
|
||||
|
||||
for i := range oidsRaw {
|
||||
err = lockList[i].DecodeString(oidsRaw[i])
|
||||
common.ExitOnErr(cmd, fmt.Sprintf("Incorrect object arg #%d: %%v", i+1), err)
|
||||
}
|
||||
|
||||
key := key.GetOrGenerate(cmd)
|
||||
|
||||
var idOwner user.ID
|
||||
user.IDFromKey(&idOwner, key.PublicKey)
|
||||
|
||||
var lock objectSDK.Lock
|
||||
lock.WriteMembers(lockList)
|
||||
|
||||
exp, _ := cmd.Flags().GetUint64(commonflags.ExpireAt)
|
||||
lifetime, _ := cmd.Flags().GetUint64(commonflags.Lifetime)
|
||||
if exp == 0 && lifetime == 0 { // mutual exclusion is ensured by cobra
|
||||
common.ExitOnErr(cmd, "", errors.New("either expiration epoch of a lifetime is required"))
|
||||
}
|
||||
|
||||
if lifetime != 0 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
endpoint, _ := cmd.Flags().GetString(commonflags.RPC)
|
||||
|
||||
currEpoch, err := internalclient.GetCurrentEpoch(ctx, endpoint)
|
||||
common.ExitOnErr(cmd, "Request current epoch: %w", err)
|
||||
|
||||
exp = currEpoch + lifetime
|
||||
}
|
||||
|
||||
common.PrintVerbose("Lock object will expire at %d epoch", exp)
|
||||
|
||||
var expirationAttr objectSDK.Attribute
|
||||
expirationAttr.SetKey(objectV2.SysAttributeExpEpoch)
|
||||
expirationAttr.SetValue(strconv.FormatUint(exp, 10))
|
||||
|
||||
obj := objectSDK.New()
|
||||
obj.SetContainerID(cnr)
|
||||
obj.SetOwnerID(&idOwner)
|
||||
obj.SetType(objectSDK.TypeLock)
|
||||
obj.SetAttributes(expirationAttr)
|
||||
obj.SetPayload(lock.Marshal())
|
||||
|
||||
var prm internalclient.PutObjectPrm
|
||||
ReadOrOpenSession(cmd, &prm, key, cnr, nil)
|
||||
Prepare(cmd, &prm)
|
||||
prm.SetHeader(obj)
|
||||
|
||||
res, err := internalclient.PutObject(prm)
|
||||
common.ExitOnErr(cmd, "Store lock object in NeoFS: %w", err)
|
||||
|
||||
cmd.Printf("Lock object ID: %s\n", res.ID())
|
||||
cmd.Println("Objects successfully locked.")
|
||||
},
|
||||
}
|
||||
|
||||
func initCommandObjectLock() {
|
||||
commonflags.Init(objectLockCmd)
|
||||
initFlagSession(objectLockCmd, "PUT")
|
||||
|
||||
ff := objectLockCmd.Flags()
|
||||
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = objectLockCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
ff.StringSlice(commonflags.OIDFlag, nil, commonflags.OIDFlagUsage)
|
||||
_ = objectLockCmd.MarkFlagRequired(commonflags.OIDFlag)
|
||||
|
||||
ff.Uint64P(commonflags.ExpireAt, "e", 0, "Lock expiration epoch")
|
||||
|
||||
ff.Uint64(commonflags.Lifetime, 0, "Lock lifetime")
|
||||
objectLockCmd.MarkFlagsMutuallyExclusive(commonflags.ExpireAt, commonflags.Lifetime)
|
||||
}
|
244
cmd/frostfs-cli/modules/object/put.go
Normal file
244
cmd/frostfs-cli/modules/object/put.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
objectV2 "github.com/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
noProgressFlag = "no-progress"
|
||||
notificationFlag = "notify"
|
||||
)
|
||||
|
||||
var putExpiredOn uint64
|
||||
|
||||
var objectPutCmd = &cobra.Command{
|
||||
Use: "put",
|
||||
Short: "Put object to NeoFS",
|
||||
Long: "Put object to NeoFS",
|
||||
Run: putObject,
|
||||
}
|
||||
|
||||
func initObjectPutCmd() {
|
||||
commonflags.Init(objectPutCmd)
|
||||
initFlagSession(objectPutCmd, "PUT")
|
||||
|
||||
flags := objectPutCmd.Flags()
|
||||
|
||||
flags.String(fileFlag, "", "File with object payload")
|
||||
_ = objectPutCmd.MarkFlagFilename(fileFlag)
|
||||
_ = objectPutCmd.MarkFlagRequired(fileFlag)
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
|
||||
flags.String("attributes", "", "User attributes in form of Key1=Value1,Key2=Value2")
|
||||
flags.Bool("disable-filename", false, "Do not set well-known filename attribute")
|
||||
flags.Bool("disable-timestamp", false, "Do not set well-known timestamp attribute")
|
||||
flags.Uint64VarP(&putExpiredOn, commonflags.ExpireAt, "e", 0, "Last epoch in the life of the object")
|
||||
flags.Bool(noProgressFlag, false, "Do not show progress bar")
|
||||
|
||||
flags.String(notificationFlag, "", "Object notification in the form of *epoch*:*topic*; '-' topic means using default")
|
||||
flags.Bool(binaryFlag, false, "Deserialize object structure from given file.")
|
||||
}
|
||||
|
||||
func putObject(cmd *cobra.Command, _ []string) {
|
||||
binary, _ := cmd.Flags().GetBool(binaryFlag)
|
||||
cidVal, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
|
||||
if !binary && cidVal == "" {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("required flag \"%s\" not set", commonflags.CIDFlag))
|
||||
}
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
var ownerID user.ID
|
||||
var cnr cid.ID
|
||||
|
||||
filename, _ := cmd.Flags().GetString(fileFlag)
|
||||
f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err))
|
||||
}
|
||||
var payloadReader io.Reader = f
|
||||
obj := object.New()
|
||||
|
||||
if binary {
|
||||
buf, err := os.ReadFile(filename)
|
||||
common.ExitOnErr(cmd, "unable to read given file: %w", err)
|
||||
objTemp := object.New()
|
||||
//TODO(@acid-ant): #1932 Use streams to marshal/unmarshal payload
|
||||
common.ExitOnErr(cmd, "can't unmarshal object from given file: %w", objTemp.Unmarshal(buf))
|
||||
payloadReader = bytes.NewReader(objTemp.Payload())
|
||||
cnr, _ = objTemp.ContainerID()
|
||||
ownerID = *objTemp.OwnerID()
|
||||
} else {
|
||||
readCID(cmd, &cnr)
|
||||
user.IDFromKey(&ownerID, pk.PublicKey)
|
||||
}
|
||||
|
||||
attrs, err := parseObjectAttrs(cmd)
|
||||
common.ExitOnErr(cmd, "can't parse object attributes: %w", err)
|
||||
|
||||
expiresOn, _ := cmd.Flags().GetUint64(commonflags.ExpireAt)
|
||||
if expiresOn > 0 {
|
||||
var expAttrFound bool
|
||||
expAttrValue := strconv.FormatUint(expiresOn, 10)
|
||||
|
||||
for i := range attrs {
|
||||
if attrs[i].Key() == objectV2.SysAttributeExpEpoch {
|
||||
attrs[i].SetValue(expAttrValue)
|
||||
expAttrFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !expAttrFound {
|
||||
index := len(attrs)
|
||||
attrs = append(attrs, object.Attribute{})
|
||||
attrs[index].SetKey(objectV2.SysAttributeExpEpoch)
|
||||
attrs[index].SetValue(expAttrValue)
|
||||
}
|
||||
}
|
||||
|
||||
obj.SetContainerID(cnr)
|
||||
obj.SetOwnerID(&ownerID)
|
||||
obj.SetAttributes(attrs...)
|
||||
|
||||
notificationInfo, err := parseObjectNotifications(cmd)
|
||||
common.ExitOnErr(cmd, "can't parse object notification information: %w", err)
|
||||
|
||||
if notificationInfo != nil {
|
||||
obj.SetNotification(*notificationInfo)
|
||||
}
|
||||
|
||||
var prm internalclient.PutObjectPrm
|
||||
ReadOrOpenSession(cmd, &prm, pk, cnr, nil)
|
||||
Prepare(cmd, &prm)
|
||||
prm.SetHeader(obj)
|
||||
|
||||
var p *pb.ProgressBar
|
||||
|
||||
noProgress, _ := cmd.Flags().GetBool(noProgressFlag)
|
||||
if noProgress {
|
||||
prm.SetPayloadReader(payloadReader)
|
||||
} else {
|
||||
if binary {
|
||||
p = pb.New(len(obj.Payload()))
|
||||
p.Output = cmd.OutOrStdout()
|
||||
prm.SetPayloadReader(p.NewProxyReader(payloadReader))
|
||||
prm.SetHeaderCallback(func(o *object.Object) { p.Start() })
|
||||
} else {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
cmd.PrintErrf("Failed to get file size, progress bar is disabled: %v\n", err)
|
||||
prm.SetPayloadReader(f)
|
||||
} else {
|
||||
p = pb.New64(fi.Size())
|
||||
p.Output = cmd.OutOrStdout()
|
||||
prm.SetPayloadReader(p.NewProxyReader(f))
|
||||
prm.SetHeaderCallback(func(o *object.Object) {
|
||||
p.Start()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res, err := internalclient.PutObject(prm)
|
||||
if p != nil {
|
||||
p.Finish()
|
||||
}
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
cmd.Printf("[%s] Object successfully stored\n", filename)
|
||||
cmd.Printf(" OID: %s\n CID: %s\n", res.ID(), cnr)
|
||||
}
|
||||
|
||||
func parseObjectAttrs(cmd *cobra.Command) ([]object.Attribute, error) {
|
||||
var rawAttrs []string
|
||||
|
||||
raw := cmd.Flag("attributes").Value.String()
|
||||
if len(raw) != 0 {
|
||||
rawAttrs = strings.Split(raw, ",")
|
||||
}
|
||||
|
||||
attrs := make([]object.Attribute, len(rawAttrs), len(rawAttrs)+2) // name + timestamp attributes
|
||||
for i := range rawAttrs {
|
||||
kv := strings.SplitN(rawAttrs[i], "=", 2)
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i])
|
||||
}
|
||||
attrs[i].SetKey(kv[0])
|
||||
attrs[i].SetValue(kv[1])
|
||||
}
|
||||
|
||||
disableFilename, _ := cmd.Flags().GetBool("disable-filename")
|
||||
if !disableFilename {
|
||||
filename := filepath.Base(cmd.Flag(fileFlag).Value.String())
|
||||
index := len(attrs)
|
||||
attrs = append(attrs, object.Attribute{})
|
||||
attrs[index].SetKey(object.AttributeFileName)
|
||||
attrs[index].SetValue(filename)
|
||||
}
|
||||
|
||||
disableTime, _ := cmd.Flags().GetBool("disable-timestamp")
|
||||
if !disableTime {
|
||||
index := len(attrs)
|
||||
attrs = append(attrs, object.Attribute{})
|
||||
attrs[index].SetKey(object.AttributeTimestamp)
|
||||
attrs[index].SetValue(strconv.FormatInt(time.Now().Unix(), 10))
|
||||
}
|
||||
|
||||
return attrs, nil
|
||||
}
|
||||
|
||||
func parseObjectNotifications(cmd *cobra.Command) (*object.NotificationInfo, error) {
|
||||
const (
|
||||
separator = ":"
|
||||
useDefaultTopic = "-"
|
||||
)
|
||||
|
||||
raw := cmd.Flag(notificationFlag).Value.String()
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rawSlice := strings.SplitN(raw, separator, 2)
|
||||
if len(rawSlice) != 2 {
|
||||
return nil, fmt.Errorf("notification must be in the form of: *epoch*%s*topic*, got %s", separator, raw)
|
||||
}
|
||||
|
||||
ni := new(object.NotificationInfo)
|
||||
|
||||
epoch, err := strconv.ParseUint(rawSlice[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse notification epoch %s: %w", rawSlice[0], err)
|
||||
}
|
||||
|
||||
ni.SetEpoch(epoch)
|
||||
|
||||
if rawSlice[1] == "" {
|
||||
return nil, fmt.Errorf("incorrect empty topic: use %s to force using default topic", useDefaultTopic)
|
||||
}
|
||||
|
||||
if rawSlice[1] != useDefaultTopic {
|
||||
ni.SetTopic(rawSlice[1])
|
||||
}
|
||||
|
||||
return ni, nil
|
||||
}
|
175
cmd/frostfs-cli/modules/object/range.go
Normal file
175
cmd/frostfs-cli/modules/object/range.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var objectRangeCmd = &cobra.Command{
|
||||
Use: "range",
|
||||
Short: "Get payload range data of an object",
|
||||
Long: "Get payload range data of an object",
|
||||
Run: getObjectRange,
|
||||
}
|
||||
|
||||
func initObjectRangeCmd() {
|
||||
commonflags.Init(objectRangeCmd)
|
||||
initFlagSession(objectRangeCmd, "RANGE")
|
||||
|
||||
flags := objectRangeCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = objectRangeCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
|
||||
_ = objectRangeCmd.MarkFlagRequired(commonflags.OIDFlag)
|
||||
|
||||
flags.String("range", "", "Range to take data from in the form offset:length")
|
||||
flags.String(fileFlag, "", "File to write object payload to. Default: stdout.")
|
||||
flags.Bool(rawFlag, false, rawFlagDesc)
|
||||
}
|
||||
|
||||
func getObjectRange(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
|
||||
objAddr := readObjectAddress(cmd, &cnr, &obj)
|
||||
|
||||
ranges, err := getRangeList(cmd)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
if len(ranges) != 1 {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("exactly one range must be specified, got: %d", len(ranges)))
|
||||
}
|
||||
|
||||
var out io.Writer
|
||||
|
||||
filename := cmd.Flag(fileFlag).Value.String()
|
||||
if filename == "" {
|
||||
out = os.Stdout
|
||||
} else {
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err))
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
out = f
|
||||
}
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.PayloadRangePrm
|
||||
prm.SetClient(cli)
|
||||
Prepare(cmd, &prm)
|
||||
readSession(cmd, &prm, pk, cnr, obj)
|
||||
|
||||
raw, _ := cmd.Flags().GetBool(rawFlag)
|
||||
prm.SetRawFlag(raw)
|
||||
prm.SetAddress(objAddr)
|
||||
prm.SetRange(ranges[0])
|
||||
prm.SetPayloadWriter(out)
|
||||
|
||||
_, err = internalclient.PayloadRange(prm)
|
||||
if err != nil {
|
||||
if ok := printSplitInfoErr(cmd, err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "can't get object payload range: %w", err)
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
cmd.Printf("[%s] Payload successfully saved\n", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func printSplitInfoErr(cmd *cobra.Command, err error) bool {
|
||||
var errSplitInfo *object.SplitInfoError
|
||||
|
||||
ok := errors.As(err, &errSplitInfo)
|
||||
|
||||
if ok {
|
||||
cmd.PrintErrln("Object is complex, split information received.")
|
||||
printSplitInfo(cmd, errSplitInfo.SplitInfo())
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func printSplitInfo(cmd *cobra.Command, info *object.SplitInfo) {
|
||||
bs, err := marshalSplitInfo(cmd, info)
|
||||
common.ExitOnErr(cmd, "can't marshal split info: %w", err)
|
||||
|
||||
cmd.Println(string(bs))
|
||||
}
|
||||
|
||||
func marshalSplitInfo(cmd *cobra.Command, info *object.SplitInfo) ([]byte, error) {
|
||||
toJSON, _ := cmd.Flags().GetBool(commonflags.JSON)
|
||||
toProto, _ := cmd.Flags().GetBool("proto")
|
||||
switch {
|
||||
case toJSON && toProto:
|
||||
return nil, errors.New("'--json' and '--proto' flags are mutually exclusive")
|
||||
case toJSON:
|
||||
return info.MarshalJSON()
|
||||
case toProto:
|
||||
return info.Marshal()
|
||||
default:
|
||||
b := bytes.NewBuffer(nil)
|
||||
if splitID := info.SplitID(); splitID != nil {
|
||||
b.WriteString("Split ID: " + splitID.String() + "\n")
|
||||
}
|
||||
if link, ok := info.Link(); ok {
|
||||
b.WriteString("Linking object: " + link.String() + "\n")
|
||||
}
|
||||
if last, ok := info.LastPart(); ok {
|
||||
b.WriteString("Last object: " + last.String() + "\n")
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func getRangeList(cmd *cobra.Command) ([]*object.Range, error) {
|
||||
v := cmd.Flag("range").Value.String()
|
||||
if len(v) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
vs := strings.Split(v, ",")
|
||||
rs := make([]*object.Range, len(vs))
|
||||
for i := range vs {
|
||||
r := strings.Split(vs[i], rangeSep)
|
||||
if len(r) != 2 {
|
||||
return nil, fmt.Errorf("invalid range specifier: %s", vs[i])
|
||||
}
|
||||
|
||||
offset, err := strconv.ParseUint(r[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid range specifier: %s", vs[i])
|
||||
}
|
||||
length, err := strconv.ParseUint(r[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid range specifier: %s", vs[i])
|
||||
}
|
||||
rs[i] = object.NewRange()
|
||||
rs[i].SetOffset(offset)
|
||||
rs[i].SetLength(length)
|
||||
}
|
||||
return rs, nil
|
||||
}
|
47
cmd/frostfs-cli/modules/object/root.go
Normal file
47
cmd/frostfs-cli/modules/object/root.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Cmd represents the object command.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "object",
|
||||
Short: "Operations with Objects",
|
||||
Long: `Operations with Objects`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// bind exactly that cmd's flags to
|
||||
// the viper before execution
|
||||
commonflags.Bind(cmd)
|
||||
commonflags.BindAPI(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
objectChildCommands := []*cobra.Command{
|
||||
objectPutCmd,
|
||||
objectDelCmd,
|
||||
objectGetCmd,
|
||||
objectSearchCmd,
|
||||
objectHeadCmd,
|
||||
objectHashCmd,
|
||||
objectRangeCmd,
|
||||
objectLockCmd}
|
||||
|
||||
Cmd.AddCommand(objectChildCommands...)
|
||||
|
||||
for _, objCommand := range objectChildCommands {
|
||||
InitBearer(objCommand)
|
||||
commonflags.InitAPI(objCommand)
|
||||
}
|
||||
|
||||
initObjectPutCmd()
|
||||
initObjectDeleteCmd()
|
||||
initObjectGetCmd()
|
||||
initObjectSearchCmd()
|
||||
initObjectHeadCmd()
|
||||
initObjectHashCmd()
|
||||
initObjectRangeCmd()
|
||||
initCommandObjectLock()
|
||||
}
|
145
cmd/frostfs-cli/modules/object/search.go
Normal file
145
cmd/frostfs-cli/modules/object/search.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oidSDK "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
searchFilters []string
|
||||
|
||||
objectSearchCmd = &cobra.Command{
|
||||
Use: "search",
|
||||
Short: "Search object",
|
||||
Long: "Search object",
|
||||
Run: searchObject,
|
||||
}
|
||||
)
|
||||
|
||||
func initObjectSearchCmd() {
|
||||
commonflags.Init(objectSearchCmd)
|
||||
initFlagSession(objectSearchCmd, "SEARCH")
|
||||
|
||||
flags := objectSearchCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = objectSearchCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.StringSliceVarP(&searchFilters, "filters", "f", nil,
|
||||
"Repeated filter expressions or files with protobuf JSON")
|
||||
|
||||
flags.Bool("root", false, "Search for user objects")
|
||||
flags.Bool("phy", false, "Search physically stored objects")
|
||||
flags.String(commonflags.OIDFlag, "", "Search object by identifier")
|
||||
}
|
||||
|
||||
func searchObject(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
readCID(cmd, &cnr)
|
||||
|
||||
sf, err := parseSearchFilters(cmd)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.SearchObjectsPrm
|
||||
prm.SetClient(cli)
|
||||
Prepare(cmd, &prm)
|
||||
readSessionGlobal(cmd, &prm, pk, cnr)
|
||||
prm.SetContainerID(cnr)
|
||||
prm.SetFilters(sf)
|
||||
|
||||
res, err := internalclient.SearchObjects(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
ids := res.IDList()
|
||||
|
||||
cmd.Printf("Found %d objects.\n", len(ids))
|
||||
for i := range ids {
|
||||
cmd.Println(ids[i].String())
|
||||
}
|
||||
}
|
||||
|
||||
var searchUnaryOpVocabulary = map[string]object.SearchMatchType{
|
||||
"NOPRESENT": object.MatchNotPresent,
|
||||
}
|
||||
|
||||
var searchBinaryOpVocabulary = map[string]object.SearchMatchType{
|
||||
"EQ": object.MatchStringEqual,
|
||||
"NE": object.MatchStringNotEqual,
|
||||
"COMMON_PREFIX": object.MatchCommonPrefix,
|
||||
}
|
||||
|
||||
func parseSearchFilters(cmd *cobra.Command) (object.SearchFilters, error) {
|
||||
var fs object.SearchFilters
|
||||
|
||||
for i := range searchFilters {
|
||||
words := strings.Fields(searchFilters[i])
|
||||
|
||||
switch len(words) {
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid field number: %d", len(words))
|
||||
case 1:
|
||||
data, err := os.ReadFile(words[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read attributes filter from file: %w", err)
|
||||
}
|
||||
|
||||
subFs := object.NewSearchFilters()
|
||||
|
||||
if err := subFs.UnmarshalJSON(data); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal attributes filter from file: %w", err)
|
||||
}
|
||||
|
||||
fs = append(fs, subFs...)
|
||||
case 2:
|
||||
m, ok := searchUnaryOpVocabulary[words[1]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported unary op: %s", words[1])
|
||||
}
|
||||
|
||||
fs.AddFilter(words[0], "", m)
|
||||
case 3:
|
||||
m, ok := searchBinaryOpVocabulary[words[1]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported binary op: %s", words[1])
|
||||
}
|
||||
|
||||
fs.AddFilter(words[0], words[2], m)
|
||||
}
|
||||
}
|
||||
|
||||
root, _ := cmd.Flags().GetBool("root")
|
||||
if root {
|
||||
fs.AddRootFilter()
|
||||
}
|
||||
|
||||
phy, _ := cmd.Flags().GetBool("phy")
|
||||
if phy {
|
||||
fs.AddPhyFilter()
|
||||
}
|
||||
|
||||
oid, _ := cmd.Flags().GetString(commonflags.OIDFlag)
|
||||
if oid != "" {
|
||||
var id oidSDK.ID
|
||||
if err := id.DecodeString(oid); err != nil {
|
||||
return nil, fmt.Errorf("could not parse object ID: %w", err)
|
||||
}
|
||||
|
||||
fs.AddObjectIDFilter(object.MatchStringEqual, id)
|
||||
}
|
||||
|
||||
return fs, nil
|
||||
}
|
474
cmd/frostfs-cli/modules/object/util.go
Normal file
474
cmd/frostfs-cli/modules/object/util.go
Normal file
|
@ -0,0 +1,474 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
internal "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
sessionCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/session"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
frostfsecdsa "github.com/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
bearerTokenFlag = "bearer"
|
||||
|
||||
rawFlag = "raw"
|
||||
rawFlagDesc = "Set raw request option"
|
||||
fileFlag = "file"
|
||||
binaryFlag = "binary"
|
||||
)
|
||||
|
||||
type RPCParameters interface {
|
||||
SetBearerToken(prm *bearer.Token)
|
||||
SetTTL(uint32)
|
||||
SetXHeaders([]string)
|
||||
}
|
||||
|
||||
// InitBearer adds bearer token flag to a command.
|
||||
func InitBearer(cmd *cobra.Command) {
|
||||
flags := cmd.Flags()
|
||||
flags.String(bearerTokenFlag, "", "File with signed JSON or binary encoded bearer token")
|
||||
}
|
||||
|
||||
// Prepare prepares object-related parameters for a command.
|
||||
func Prepare(cmd *cobra.Command, prms ...RPCParameters) {
|
||||
ttl := viper.GetUint32(commonflags.TTL)
|
||||
common.PrintVerbose("TTL: %d", ttl)
|
||||
|
||||
for i := range prms {
|
||||
btok := common.ReadBearerToken(cmd, bearerTokenFlag)
|
||||
|
||||
prms[i].SetBearerToken(btok)
|
||||
prms[i].SetTTL(ttl)
|
||||
prms[i].SetXHeaders(parseXHeaders(cmd))
|
||||
}
|
||||
}
|
||||
|
||||
func parseXHeaders(cmd *cobra.Command) []string {
|
||||
xHeaders, _ := cmd.Flags().GetStringSlice(commonflags.XHeadersKey)
|
||||
xs := make([]string, 0, 2*len(xHeaders))
|
||||
|
||||
for i := range xHeaders {
|
||||
kv := strings.SplitN(xHeaders[i], "=", 2)
|
||||
if len(kv) != 2 {
|
||||
panic(fmt.Errorf("invalid X-Header format: %s", xHeaders[i]))
|
||||
}
|
||||
|
||||
xs = append(xs, kv[0], kv[1])
|
||||
}
|
||||
|
||||
return xs
|
||||
}
|
||||
|
||||
func readObjectAddress(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID) oid.Address {
|
||||
readCID(cmd, cnr)
|
||||
readOID(cmd, obj)
|
||||
|
||||
var addr oid.Address
|
||||
addr.SetContainer(*cnr)
|
||||
addr.SetObject(*obj)
|
||||
return addr
|
||||
}
|
||||
|
||||
func readObjectAddressBin(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID, filename string) oid.Address {
|
||||
buf, err := os.ReadFile(filename)
|
||||
common.ExitOnErr(cmd, "unable to read given file: %w", err)
|
||||
objTemp := object.New()
|
||||
common.ExitOnErr(cmd, "can't unmarshal object from given file: %w", objTemp.Unmarshal(buf))
|
||||
|
||||
var addr oid.Address
|
||||
*cnr, _ = objTemp.ContainerID()
|
||||
*obj, _ = objTemp.ID()
|
||||
addr.SetContainer(*cnr)
|
||||
addr.SetObject(*obj)
|
||||
return addr
|
||||
}
|
||||
|
||||
func readCID(cmd *cobra.Command, id *cid.ID) {
|
||||
err := id.DecodeString(cmd.Flag(commonflags.CIDFlag).Value.String())
|
||||
common.ExitOnErr(cmd, "decode container ID string: %w", err)
|
||||
}
|
||||
|
||||
func readOID(cmd *cobra.Command, id *oid.ID) {
|
||||
err := id.DecodeString(cmd.Flag(commonflags.OIDFlag).Value.String())
|
||||
common.ExitOnErr(cmd, "decode object ID string: %w", err)
|
||||
}
|
||||
|
||||
// SessionPrm is a common interface of object operation's input which supports
|
||||
// sessions.
|
||||
type SessionPrm interface {
|
||||
SetSessionToken(*session.Object)
|
||||
SetClient(*client.Client)
|
||||
}
|
||||
|
||||
// forwards all parameters to _readVerifiedSession and object as nil.
|
||||
func readSessionGlobal(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID) {
|
||||
_readVerifiedSession(cmd, dst, key, cnr, nil)
|
||||
}
|
||||
|
||||
// forwards all parameters to _readVerifiedSession.
|
||||
func readSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj oid.ID) {
|
||||
_readVerifiedSession(cmd, dst, key, cnr, &obj)
|
||||
}
|
||||
|
||||
// decodes session.Object from the file by path specified in the
|
||||
// commonflags.SessionToken flag. Returns nil if flag is not set.
|
||||
func getSession(cmd *cobra.Command) *session.Object {
|
||||
common.PrintVerbose("Trying to read session from the file...")
|
||||
|
||||
path, _ := cmd.Flags().GetString(commonflags.SessionToken)
|
||||
if path == "" {
|
||||
common.PrintVerbose("File with session token is not provided.")
|
||||
return nil
|
||||
}
|
||||
|
||||
common.PrintVerbose("Reading session from the file [%s]...", path)
|
||||
|
||||
var tok session.Object
|
||||
|
||||
err := common.ReadBinaryOrJSON(&tok, path)
|
||||
common.ExitOnErr(cmd, "read session: %v", err)
|
||||
|
||||
return &tok
|
||||
}
|
||||
|
||||
// decodes object session from JSON file from commonflags.SessionToken command
|
||||
// flag if it is provided, and writes resulting session into the provided SessionPrm.
|
||||
// Returns flag presence. Checks:
|
||||
//
|
||||
// - if session verb corresponds to given SessionPrm according to its type
|
||||
// - relation to the given container
|
||||
// - relation to the given object if non-nil
|
||||
// - relation to the given private key used within the command
|
||||
// - session signature
|
||||
//
|
||||
// SessionPrm MUST be one of:
|
||||
//
|
||||
// *internal.GetObjectPrm
|
||||
// *internal.HeadObjectPrm
|
||||
// *internal.SearchObjectsPrm
|
||||
// *internal.PayloadRangePrm
|
||||
// *internal.HashPayloadRangesPrm
|
||||
func _readVerifiedSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) {
|
||||
var cmdVerb session.ObjectVerb
|
||||
|
||||
switch dst.(type) {
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported op parameters %T", dst))
|
||||
case *internal.GetObjectPrm:
|
||||
cmdVerb = session.VerbObjectGet
|
||||
case *internal.HeadObjectPrm:
|
||||
cmdVerb = session.VerbObjectHead
|
||||
case *internal.SearchObjectsPrm:
|
||||
cmdVerb = session.VerbObjectSearch
|
||||
case *internal.PayloadRangePrm:
|
||||
cmdVerb = session.VerbObjectRange
|
||||
case *internal.HashPayloadRangesPrm:
|
||||
cmdVerb = session.VerbObjectRangeHash
|
||||
}
|
||||
|
||||
tok := getSession(cmd)
|
||||
if tok == nil {
|
||||
return
|
||||
}
|
||||
|
||||
common.PrintVerbose("Checking session correctness...")
|
||||
|
||||
switch false {
|
||||
case tok.AssertContainer(cnr):
|
||||
common.ExitOnErr(cmd, "", errors.New("unrelated container in the session"))
|
||||
case obj == nil || tok.AssertObject(*obj):
|
||||
common.ExitOnErr(cmd, "", errors.New("unrelated object in the session"))
|
||||
case tok.AssertVerb(cmdVerb):
|
||||
common.ExitOnErr(cmd, "", errors.New("wrong verb of the session"))
|
||||
case tok.AssertAuthKey((*frostfsecdsa.PublicKey)(&key.PublicKey)):
|
||||
common.ExitOnErr(cmd, "", errors.New("unrelated key in the session"))
|
||||
case tok.VerifySignature():
|
||||
common.ExitOnErr(cmd, "", errors.New("invalid signature of the session data"))
|
||||
}
|
||||
|
||||
common.PrintVerbose("Session is correct.")
|
||||
|
||||
dst.SetSessionToken(tok)
|
||||
}
|
||||
|
||||
// ReadOrOpenSession opens client connection and calls ReadOrOpenSessionViaClient with it.
|
||||
func ReadOrOpenSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) {
|
||||
cli := internal.GetSDKClientByFlag(cmd, key, commonflags.RPC)
|
||||
ReadOrOpenSessionViaClient(cmd, dst, cli, key, cnr, obj)
|
||||
}
|
||||
|
||||
// ReadOrOpenSessionViaClient tries to read session from the file specified in
|
||||
// commonflags.SessionToken flag, finalizes structures of the decoded token
|
||||
// and write the result into provided SessionPrm. If file is missing,
|
||||
// ReadOrOpenSessionViaClient calls OpenSessionViaClient.
|
||||
func ReadOrOpenSessionViaClient(cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) {
|
||||
tok := getSession(cmd)
|
||||
if tok == nil {
|
||||
OpenSessionViaClient(cmd, dst, cli, key, cnr, obj)
|
||||
return
|
||||
}
|
||||
|
||||
var objs []oid.ID
|
||||
if obj != nil {
|
||||
objs = []oid.ID{*obj}
|
||||
|
||||
if _, ok := dst.(*internal.DeleteObjectPrm); ok {
|
||||
common.PrintVerbose("Collecting relatives of the removal object...")
|
||||
|
||||
objs = append(objs, collectObjectRelatives(cmd, cli, cnr, *obj)...)
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSession(cmd, dst, tok, key, cnr, objs...)
|
||||
dst.SetClient(cli)
|
||||
}
|
||||
|
||||
// OpenSession opens client connection and calls OpenSessionViaClient with it.
|
||||
func OpenSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) {
|
||||
cli := internal.GetSDKClientByFlag(cmd, key, commonflags.RPC)
|
||||
OpenSessionViaClient(cmd, dst, cli, key, cnr, obj)
|
||||
}
|
||||
|
||||
// OpenSessionViaClient opens object session with the remote node, finalizes
|
||||
// structure of the session token and writes the result into the provided
|
||||
// SessionPrm. Also writes provided client connection to the SessionPrm.
|
||||
//
|
||||
// SessionPrm MUST be one of:
|
||||
//
|
||||
// *internal.PutObjectPrm
|
||||
// *internal.DeleteObjectPrm
|
||||
//
|
||||
// If provided SessionPrm is of type internal.DeleteObjectPrm, OpenSessionViaClient
|
||||
// spreads the session to all object's relatives.
|
||||
func OpenSessionViaClient(cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) {
|
||||
var objs []oid.ID
|
||||
|
||||
if obj != nil {
|
||||
if _, ok := dst.(*internal.DeleteObjectPrm); ok {
|
||||
common.PrintVerbose("Collecting relatives of the removal object...")
|
||||
|
||||
rels := collectObjectRelatives(cmd, cli, cnr, *obj)
|
||||
|
||||
if len(rels) == 0 {
|
||||
objs = []oid.ID{*obj}
|
||||
} else {
|
||||
objs = append(rels, *obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tok session.Object
|
||||
|
||||
const sessionLifetime = 10 // in NeoFS epochs
|
||||
|
||||
common.PrintVerbose("Opening remote session with the node...")
|
||||
|
||||
err := sessionCli.CreateSession(&tok, cli, sessionLifetime)
|
||||
common.ExitOnErr(cmd, "open remote session: %w", err)
|
||||
|
||||
common.PrintVerbose("Session successfully opened.")
|
||||
|
||||
finalizeSession(cmd, dst, &tok, key, cnr, objs...)
|
||||
|
||||
dst.SetClient(cli)
|
||||
}
|
||||
|
||||
// specifies session verb, binds the session to the given container and limits
|
||||
// the session by the given objects (if specified). After all data is written,
|
||||
// signs session using provided private key and writes the session into the
|
||||
// given SessionPrm.
|
||||
//
|
||||
// SessionPrm MUST be one of:
|
||||
//
|
||||
// *internal.PutObjectPrm
|
||||
// *internal.DeleteObjectPrm
|
||||
func finalizeSession(cmd *cobra.Command, dst SessionPrm, tok *session.Object, key *ecdsa.PrivateKey, cnr cid.ID, objs ...oid.ID) {
|
||||
common.PrintVerbose("Finalizing session token...")
|
||||
|
||||
switch dst.(type) {
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported op parameters %T", dst))
|
||||
case *internal.PutObjectPrm:
|
||||
common.PrintVerbose("Binding session to object PUT...")
|
||||
tok.ForVerb(session.VerbObjectPut)
|
||||
case *internal.DeleteObjectPrm:
|
||||
common.PrintVerbose("Binding session to object DELETE...")
|
||||
tok.ForVerb(session.VerbObjectDelete)
|
||||
}
|
||||
|
||||
common.PrintVerbose("Binding session to container %s...", cnr)
|
||||
|
||||
tok.BindContainer(cnr)
|
||||
if len(objs) > 0 {
|
||||
common.PrintVerbose("Limiting session by the objects %v...", objs)
|
||||
tok.LimitByObjects(objs...)
|
||||
}
|
||||
|
||||
common.PrintVerbose("Signing session...")
|
||||
|
||||
err := tok.Sign(*key)
|
||||
common.ExitOnErr(cmd, "sign session: %w", err)
|
||||
|
||||
common.PrintVerbose("Session token successfully formed and attached to the request.")
|
||||
|
||||
dst.SetSessionToken(tok)
|
||||
}
|
||||
|
||||
// calls commonflags.InitSession with "object <verb>" name.
|
||||
func initFlagSession(cmd *cobra.Command, verb string) {
|
||||
commonflags.InitSession(cmd, "object "+verb)
|
||||
}
|
||||
|
||||
// collects and returns all relatives of the given object stored in the specified
|
||||
// container. Empty result without an error means lack of relationship in the
|
||||
// container.
|
||||
//
|
||||
// The object itself is not included in the result.
|
||||
func collectObjectRelatives(cmd *cobra.Command, cli *client.Client, cnr cid.ID, obj oid.ID) []oid.ID {
|
||||
common.PrintVerbose("Fetching raw object header...")
|
||||
|
||||
// request raw header first
|
||||
var addrObj oid.Address
|
||||
addrObj.SetContainer(cnr)
|
||||
addrObj.SetObject(obj)
|
||||
|
||||
var prmHead internal.HeadObjectPrm
|
||||
prmHead.SetClient(cli)
|
||||
prmHead.SetAddress(addrObj)
|
||||
prmHead.SetRawFlag(true)
|
||||
|
||||
Prepare(cmd, &prmHead)
|
||||
|
||||
_, err := internal.HeadObject(prmHead)
|
||||
|
||||
var errSplit *object.SplitInfoError
|
||||
|
||||
switch {
|
||||
default:
|
||||
common.ExitOnErr(cmd, "failed to get raw object header: %w", err)
|
||||
case err == nil:
|
||||
common.PrintVerbose("Raw header received - object is singular.")
|
||||
return nil
|
||||
case errors.As(err, &errSplit):
|
||||
common.PrintVerbose("Split information received - object is virtual.")
|
||||
}
|
||||
|
||||
splitInfo := errSplit.SplitInfo()
|
||||
|
||||
// collect split chain by the descending ease of operations (ease is evaluated heuristically).
|
||||
// If any approach fails, we don't try the next since we assume that it will fail too.
|
||||
|
||||
if idLinking, ok := splitInfo.Link(); ok {
|
||||
common.PrintVerbose("Collecting split members using linking object %s...", idLinking)
|
||||
|
||||
addrObj.SetObject(idLinking)
|
||||
prmHead.SetAddress(addrObj)
|
||||
prmHead.SetRawFlag(false)
|
||||
// client is already set
|
||||
|
||||
res, err := internal.HeadObject(prmHead)
|
||||
common.ExitOnErr(cmd, "failed to get linking object's header: %w", err)
|
||||
|
||||
children := res.Header().Children()
|
||||
|
||||
common.PrintVerbose("Received split members from the linking object: %v", children)
|
||||
|
||||
// include linking object
|
||||
return append(children, idLinking)
|
||||
}
|
||||
|
||||
if idSplit := splitInfo.SplitID(); idSplit != nil {
|
||||
common.PrintVerbose("Collecting split members by split ID...")
|
||||
|
||||
var query object.SearchFilters
|
||||
query.AddSplitIDFilter(object.MatchStringEqual, idSplit)
|
||||
|
||||
var prm internal.SearchObjectsPrm
|
||||
prm.SetContainerID(cnr)
|
||||
prm.SetClient(cli)
|
||||
prm.SetFilters(query)
|
||||
|
||||
res, err := internal.SearchObjects(prm)
|
||||
common.ExitOnErr(cmd, "failed to search objects by split ID: %w", err)
|
||||
|
||||
members := res.IDList()
|
||||
|
||||
common.PrintVerbose("Found objects by split ID: %v", res.IDList())
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
idMember, ok := splitInfo.LastPart()
|
||||
if !ok {
|
||||
common.ExitOnErr(cmd, "", errors.New("missing any data in received object split information"))
|
||||
}
|
||||
|
||||
common.PrintVerbose("Traverse the object split chain in reverse...", idMember)
|
||||
|
||||
var res *internal.HeadObjectRes
|
||||
chain := []oid.ID{idMember}
|
||||
chainSet := map[oid.ID]struct{}{idMember: {}}
|
||||
|
||||
prmHead.SetRawFlag(false)
|
||||
// split members are almost definitely singular, but don't get hung up on it
|
||||
|
||||
for {
|
||||
common.PrintVerbose("Reading previous element of the split chain member %s...", idMember)
|
||||
|
||||
addrObj.SetObject(idMember)
|
||||
|
||||
res, err = internal.HeadObject(prmHead)
|
||||
common.ExitOnErr(cmd, "failed to read split chain member's header: %w", err)
|
||||
|
||||
idMember, ok = res.Header().PreviousID()
|
||||
if !ok {
|
||||
common.PrintVerbose("Chain ended.")
|
||||
break
|
||||
}
|
||||
|
||||
if _, ok = chainSet[idMember]; ok {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("duplicated member in the split chain %s", idMember))
|
||||
}
|
||||
|
||||
chain = append(chain, idMember)
|
||||
chainSet[idMember] = struct{}{}
|
||||
}
|
||||
|
||||
common.PrintVerbose("Looking for a linking object...")
|
||||
|
||||
var query object.SearchFilters
|
||||
query.AddParentIDFilter(object.MatchStringEqual, obj)
|
||||
|
||||
var prmSearch internal.SearchObjectsPrm
|
||||
prmSearch.SetClient(cli)
|
||||
prmSearch.SetContainerID(cnr)
|
||||
prmSearch.SetFilters(query)
|
||||
|
||||
resSearch, err := internal.SearchObjects(prmSearch)
|
||||
common.ExitOnErr(cmd, "failed to find object children: %w", err)
|
||||
|
||||
list := resSearch.IDList()
|
||||
|
||||
for i := range list {
|
||||
if _, ok = chainSet[list[i]]; !ok {
|
||||
common.PrintVerbose("Found one more related object %s.", list[i])
|
||||
chain = append(chain, list[i])
|
||||
}
|
||||
}
|
||||
|
||||
return chain
|
||||
}
|
123
cmd/frostfs-cli/modules/root.go
Normal file
123
cmd/frostfs-cli/modules/root.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
accountingCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/accounting"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/acl"
|
||||
bearerCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/bearer"
|
||||
containerCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/container"
|
||||
controlCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/control"
|
||||
netmapCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/netmap"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
sessionCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/session"
|
||||
sgCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/storagegroup"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/tree"
|
||||
utilCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
"github.com/TrueCloudLab/frostfs-node/misc"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/util/gendoc"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
envPrefix = "NEOFS_CLI"
|
||||
)
|
||||
|
||||
// Global scope flags.
|
||||
var (
|
||||
cfgFile string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands.
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "frostfs-cli",
|
||||
Short: "Command Line Tool to work with NeoFS",
|
||||
Long: `NeoFS CLI provides all basic interactions with NeoFS and it's services.
|
||||
|
||||
It contains commands for interaction with NeoFS nodes using different versions
|
||||
of frostfs-api and some useful utilities for compiling ACL rules from JSON
|
||||
notation, managing container access through protocol gates, querying network map
|
||||
and much more!`,
|
||||
Run: entryPoint,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
common.ExitOnErr(rootCmd, "", err)
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// use stdout as default output for cmd.Print()
|
||||
rootCmd.SetOut(os.Stdout)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "Config file (default is $HOME/.config/frostfs-cli/config.yaml)")
|
||||
rootCmd.PersistentFlags().BoolP(commonflags.Verbose, commonflags.VerboseShorthand,
|
||||
false, commonflags.VerboseUsage)
|
||||
|
||||
_ = viper.BindPFlag(commonflags.Verbose, rootCmd.PersistentFlags().Lookup(commonflags.Verbose))
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
rootCmd.Flags().Bool("version", false, "Application version and NeoFS API compatibility")
|
||||
|
||||
rootCmd.AddCommand(acl.Cmd)
|
||||
rootCmd.AddCommand(bearerCli.Cmd)
|
||||
rootCmd.AddCommand(sessionCli.Cmd)
|
||||
rootCmd.AddCommand(accountingCli.Cmd)
|
||||
rootCmd.AddCommand(controlCli.Cmd)
|
||||
rootCmd.AddCommand(utilCli.Cmd)
|
||||
rootCmd.AddCommand(netmapCli.Cmd)
|
||||
rootCmd.AddCommand(objectCli.Cmd)
|
||||
rootCmd.AddCommand(sgCli.Cmd)
|
||||
rootCmd.AddCommand(containerCli.Cmd)
|
||||
rootCmd.AddCommand(tree.Cmd)
|
||||
rootCmd.AddCommand(gendoc.Command(rootCmd))
|
||||
}
|
||||
|
||||
func entryPoint(cmd *cobra.Command, _ []string) {
|
||||
printVersion, _ := cmd.Flags().GetBool("version")
|
||||
if printVersion {
|
||||
cmd.Print(misc.BuildInfo("NeoFS CLI"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = cmd.Usage()
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Find home directory.
|
||||
home, err := homedir.Dir()
|
||||
common.ExitOnErr(rootCmd, "", err)
|
||||
|
||||
// Search config in `$HOME/.config/frostfs-cli/` with name "config.yaml"
|
||||
viper.AddConfigPath(filepath.Join(home, ".config", "frostfs-cli"))
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix(envPrefix)
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
common.PrintVerbose("Using config file: %s", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
133
cmd/frostfs-cli/modules/session/create.go
Normal file
133
cmd/frostfs-cli/modules/session/create.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/network"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||
frostfsecdsa "github.com/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
outFlag = "out"
|
||||
jsonFlag = commonflags.JSON
|
||||
)
|
||||
|
||||
const defaultLifetime = 10
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create session token",
|
||||
Run: createSession,
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
_ = viper.BindPFlag(commonflags.WalletPath, cmd.Flags().Lookup(commonflags.WalletPath))
|
||||
_ = viper.BindPFlag(commonflags.Account, cmd.Flags().Lookup(commonflags.Account))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
createCmd.Flags().Uint64P(commonflags.Lifetime, "l", defaultLifetime, "Number of epochs for token to stay valid")
|
||||
createCmd.Flags().StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||
createCmd.Flags().StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
||||
createCmd.Flags().String(outFlag, "", "File to write session token to")
|
||||
createCmd.Flags().Bool(jsonFlag, false, "Output token in JSON")
|
||||
createCmd.Flags().StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
||||
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), commonflags.WalletPath)
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), outFlag)
|
||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), commonflags.RPC)
|
||||
}
|
||||
|
||||
func createSession(cmd *cobra.Command, _ []string) {
|
||||
privKey := key.Get(cmd)
|
||||
|
||||
var netAddr network.Address
|
||||
addrStr, _ := cmd.Flags().GetString(commonflags.RPC)
|
||||
common.ExitOnErr(cmd, "can't parse endpoint: %w", netAddr.FromString(addrStr))
|
||||
|
||||
c, err := internalclient.GetSDKClient(privKey, netAddr)
|
||||
common.ExitOnErr(cmd, "can't create client: %w", err)
|
||||
|
||||
lifetime := uint64(defaultLifetime)
|
||||
if lfArg, _ := cmd.Flags().GetUint64(commonflags.Lifetime); lfArg != 0 {
|
||||
lifetime = lfArg
|
||||
}
|
||||
|
||||
var tok session.Object
|
||||
|
||||
err = CreateSession(&tok, c, lifetime)
|
||||
common.ExitOnErr(cmd, "can't create session: %w", err)
|
||||
|
||||
var data []byte
|
||||
|
||||
if toJSON, _ := cmd.Flags().GetBool(jsonFlag); toJSON {
|
||||
data, err = tok.MarshalJSON()
|
||||
common.ExitOnErr(cmd, "can't decode session token JSON: %w", err)
|
||||
} else {
|
||||
data = tok.Marshal()
|
||||
}
|
||||
|
||||
filename, _ := cmd.Flags().GetString(outFlag)
|
||||
err = os.WriteFile(filename, data, 0644)
|
||||
common.ExitOnErr(cmd, "can't write token to file: %w", err)
|
||||
}
|
||||
|
||||
// CreateSession opens a new communication with NeoFS storage node using client connection.
|
||||
// The session is expected to be maintained by the storage node during the given
|
||||
// number of epochs.
|
||||
//
|
||||
// Fills ID, lifetime and session key.
|
||||
func CreateSession(dst *session.Object, c *client.Client, lifetime uint64) error {
|
||||
var netInfoPrm internalclient.NetworkInfoPrm
|
||||
netInfoPrm.SetClient(c)
|
||||
|
||||
ni, err := internalclient.NetworkInfo(netInfoPrm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't fetch network info: %w", err)
|
||||
}
|
||||
|
||||
cur := ni.NetworkInfo().CurrentEpoch()
|
||||
exp := cur + lifetime
|
||||
|
||||
var sessionPrm internalclient.CreateSessionPrm
|
||||
sessionPrm.SetClient(c)
|
||||
sessionPrm.SetExp(exp)
|
||||
|
||||
sessionRes, err := internalclient.CreateSession(sessionPrm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't open session: %w", err)
|
||||
}
|
||||
|
||||
binIDSession := sessionRes.ID()
|
||||
|
||||
var keySession frostfsecdsa.PublicKey
|
||||
|
||||
err = keySession.Decode(sessionRes.SessionKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode public session key: %w", err)
|
||||
}
|
||||
|
||||
var idSession uuid.UUID
|
||||
|
||||
err = idSession.UnmarshalBinary(binIDSession)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode session ID: %w", err)
|
||||
}
|
||||
|
||||
dst.SetID(idSession)
|
||||
dst.SetNbf(cur)
|
||||
dst.SetIat(cur)
|
||||
dst.SetExp(exp)
|
||||
dst.SetAuthKey(&keySession)
|
||||
|
||||
return nil
|
||||
}
|
14
cmd/frostfs-cli/modules/session/root.go
Normal file
14
cmd/frostfs-cli/modules/session/root.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "session",
|
||||
Short: "Operations with session token",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(createCmd)
|
||||
}
|
53
cmd/frostfs-cli/modules/storagegroup/delete.go
Normal file
53
cmd/frostfs-cli/modules/storagegroup/delete.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package storagegroup
|
||||
|
||||
import (
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var sgDelCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete storage group from NeoFS",
|
||||
Long: "Delete storage group from NeoFS",
|
||||
Run: delSG,
|
||||
}
|
||||
|
||||
func initSGDeleteCmd() {
|
||||
commonflags.Init(sgDelCmd)
|
||||
|
||||
flags := sgDelCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = sgDelCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.StringVarP(&sgID, sgIDFlag, "", "", "Storage group identifier")
|
||||
_ = sgDelCmd.MarkFlagRequired(sgIDFlag)
|
||||
}
|
||||
|
||||
func delSG(cmd *cobra.Command, _ []string) {
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
|
||||
addr := readObjectAddress(cmd, &cnr, &obj)
|
||||
|
||||
var prm internalclient.DeleteObjectPrm
|
||||
objectCli.OpenSession(cmd, &prm, pk, cnr, &obj)
|
||||
objectCli.Prepare(cmd, &prm)
|
||||
prm.SetAddress(addr)
|
||||
|
||||
res, err := internalclient.DeleteObject(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
tombstone := res.Tombstone()
|
||||
|
||||
cmd.Println("Storage group removed successfully.")
|
||||
cmd.Printf(" Tombstone: %s\n", tombstone)
|
||||
}
|
81
cmd/frostfs-cli/modules/storagegroup/get.go
Normal file
81
cmd/frostfs-cli/modules/storagegroup/get.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package storagegroup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
storagegroupSDK "github.com/TrueCloudLab/frostfs-sdk-go/storagegroup"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var sgID string
|
||||
|
||||
var sgGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get storage group from NeoFS",
|
||||
Long: "Get storage group from NeoFS",
|
||||
Run: getSG,
|
||||
}
|
||||
|
||||
func initSGGetCmd() {
|
||||
commonflags.Init(sgGetCmd)
|
||||
|
||||
flags := sgGetCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = sgGetCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.StringVarP(&sgID, sgIDFlag, "", "", "Storage group identifier")
|
||||
_ = sgGetCmd.MarkFlagRequired(sgIDFlag)
|
||||
|
||||
flags.Bool(sgRawFlag, false, "Set raw request option")
|
||||
}
|
||||
|
||||
func getSG(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
var obj oid.ID
|
||||
|
||||
addr := readObjectAddress(cmd, &cnr, &obj)
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.GetObjectPrm
|
||||
objectCli.Prepare(cmd, &prm)
|
||||
prm.SetClient(cli)
|
||||
|
||||
raw, _ := cmd.Flags().GetBool(sgRawFlag)
|
||||
prm.SetRawFlag(raw)
|
||||
prm.SetAddress(addr)
|
||||
prm.SetPayloadWriter(buf)
|
||||
|
||||
res, err := internalclient.GetObject(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
rawObj := res.Header()
|
||||
rawObj.SetPayload(buf.Bytes())
|
||||
|
||||
var sg storagegroupSDK.StorageGroup
|
||||
|
||||
err = storagegroupSDK.ReadFromObject(&sg, *rawObj)
|
||||
common.ExitOnErr(cmd, "could not read storage group from the obj: %w", err)
|
||||
|
||||
cmd.Printf("Expiration epoch: %d\n", sg.ExpirationEpoch())
|
||||
cmd.Printf("Group size: %d\n", sg.ValidationDataSize())
|
||||
common.PrintChecksum(cmd, "Group hash", sg.ValidationDataHash)
|
||||
|
||||
if members := sg.Members(); len(members) > 0 {
|
||||
cmd.Println("Members:")
|
||||
|
||||
for i := range members {
|
||||
cmd.Printf("\t%s\n", members[i].String())
|
||||
}
|
||||
}
|
||||
}
|
52
cmd/frostfs-cli/modules/storagegroup/list.go
Normal file
52
cmd/frostfs-cli/modules/storagegroup/list.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package storagegroup
|
||||
|
||||
import (
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/object_manager/storagegroup"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var sgListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List storage groups in NeoFS container",
|
||||
Long: "List storage groups in NeoFS container",
|
||||
Run: listSG,
|
||||
}
|
||||
|
||||
func initSGListCmd() {
|
||||
commonflags.Init(sgListCmd)
|
||||
|
||||
sgListCmd.Flags().String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = sgListCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
}
|
||||
|
||||
func listSG(cmd *cobra.Command, _ []string) {
|
||||
var cnr cid.ID
|
||||
readCID(cmd, &cnr)
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
|
||||
var prm internalclient.SearchObjectsPrm
|
||||
objectCli.Prepare(cmd, &prm)
|
||||
prm.SetClient(cli)
|
||||
prm.SetContainerID(cnr)
|
||||
prm.SetFilters(storagegroup.SearchQuery())
|
||||
|
||||
res, err := internalclient.SearchObjects(prm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
ids := res.IDList()
|
||||
|
||||
cmd.Printf("Found %d storage groups.\n", len(ids))
|
||||
|
||||
for i := range ids {
|
||||
cmd.Println(ids[i].String())
|
||||
}
|
||||
}
|
145
cmd/frostfs-cli/modules/storagegroup/put.go
Normal file
145
cmd/frostfs-cli/modules/storagegroup/put.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package storagegroup
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/object_manager/storagegroup"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
storagegroupSDK "github.com/TrueCloudLab/frostfs-sdk-go/storagegroup"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const sgMembersFlag = "members"
|
||||
|
||||
var sgMembers []string
|
||||
|
||||
var sgPutCmd = &cobra.Command{
|
||||
Use: "put",
|
||||
Short: "Put storage group to NeoFS",
|
||||
Long: "Put storage group to NeoFS",
|
||||
Run: putSG,
|
||||
}
|
||||
|
||||
func initSGPutCmd() {
|
||||
commonflags.Init(sgPutCmd)
|
||||
|
||||
flags := sgPutCmd.Flags()
|
||||
|
||||
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = sgPutCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
flags.StringSliceVarP(&sgMembers, sgMembersFlag, "m", nil, "ID list of storage group members")
|
||||
_ = sgPutCmd.MarkFlagRequired(sgMembersFlag)
|
||||
|
||||
flags.Uint64(commonflags.Lifetime, 0, "Storage group lifetime in epochs")
|
||||
_ = sgPutCmd.MarkFlagRequired(commonflags.Lifetime)
|
||||
}
|
||||
|
||||
func putSG(cmd *cobra.Command, _ []string) {
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
var ownerID user.ID
|
||||
user.IDFromKey(&ownerID, pk.PublicKey)
|
||||
|
||||
var cnr cid.ID
|
||||
readCID(cmd, &cnr)
|
||||
|
||||
members := make([]oid.ID, len(sgMembers))
|
||||
uniqueFilter := make(map[oid.ID]struct{}, len(sgMembers))
|
||||
|
||||
for i := range sgMembers {
|
||||
err := members[i].DecodeString(sgMembers[i])
|
||||
common.ExitOnErr(cmd, "could not parse object ID: %w", err)
|
||||
|
||||
if _, alreadyExists := uniqueFilter[members[i]]; alreadyExists {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("%s member in not unique", members[i]))
|
||||
}
|
||||
|
||||
uniqueFilter[members[i]] = struct{}{}
|
||||
}
|
||||
|
||||
var (
|
||||
headPrm internalclient.HeadObjectPrm
|
||||
putPrm internalclient.PutObjectPrm
|
||||
getCnrPrm internalclient.GetContainerPrm
|
||||
)
|
||||
|
||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||
getCnrPrm.SetClient(cli)
|
||||
getCnrPrm.SetContainer(cnr)
|
||||
|
||||
resGetCnr, err := internalclient.GetContainer(getCnrPrm)
|
||||
common.ExitOnErr(cmd, "get container RPC call: %w", err)
|
||||
|
||||
objectCli.OpenSessionViaClient(cmd, &putPrm, cli, pk, cnr, nil)
|
||||
objectCli.Prepare(cmd, &headPrm, &putPrm)
|
||||
|
||||
headPrm.SetRawFlag(true)
|
||||
headPrm.SetClient(cli)
|
||||
|
||||
sg, err := storagegroup.CollectMembers(sgHeadReceiver{
|
||||
cmd: cmd,
|
||||
key: pk,
|
||||
ownerID: &ownerID,
|
||||
prm: headPrm,
|
||||
}, cnr, members, !container.IsHomomorphicHashingDisabled(resGetCnr.Container()))
|
||||
common.ExitOnErr(cmd, "could not collect storage group members: %w", err)
|
||||
|
||||
var netInfoPrm internalclient.NetworkInfoPrm
|
||||
netInfoPrm.SetClient(cli)
|
||||
|
||||
ni, err := internalclient.NetworkInfo(netInfoPrm)
|
||||
common.ExitOnErr(cmd, "can't fetch network info: %w", err)
|
||||
|
||||
lifetime, _ := cmd.Flags().GetUint64(commonflags.Lifetime)
|
||||
sg.SetExpirationEpoch(ni.NetworkInfo().CurrentEpoch() + lifetime)
|
||||
|
||||
obj := object.New()
|
||||
obj.SetContainerID(cnr)
|
||||
obj.SetOwnerID(&ownerID)
|
||||
|
||||
storagegroupSDK.WriteToObject(*sg, obj)
|
||||
|
||||
putPrm.SetHeader(obj)
|
||||
|
||||
res, err := internalclient.PutObject(putPrm)
|
||||
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
cmd.Println("Storage group successfully stored")
|
||||
cmd.Printf(" ID: %s\n CID: %s\n", res.ID(), cnr)
|
||||
}
|
||||
|
||||
type sgHeadReceiver struct {
|
||||
cmd *cobra.Command
|
||||
key *ecdsa.PrivateKey
|
||||
ownerID *user.ID
|
||||
prm internalclient.HeadObjectPrm
|
||||
}
|
||||
|
||||
func (c sgHeadReceiver) Head(addr oid.Address) (interface{}, error) {
|
||||
c.prm.SetAddress(addr)
|
||||
|
||||
res, err := internalclient.HeadObject(c.prm)
|
||||
|
||||
var errSplitInfo *object.SplitInfoError
|
||||
|
||||
switch {
|
||||
default:
|
||||
return nil, err
|
||||
case err == nil:
|
||||
return res.Header(), nil
|
||||
case errors.As(err, &errSplitInfo):
|
||||
return errSplitInfo.SplitInfo(), nil
|
||||
}
|
||||
}
|
46
cmd/frostfs-cli/modules/storagegroup/root.go
Normal file
46
cmd/frostfs-cli/modules/storagegroup/root.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package storagegroup
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
objectCli "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Cmd represents the storagegroup command.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "storagegroup",
|
||||
Short: "Operations with Storage Groups",
|
||||
Long: `Operations with Storage Groups`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// bind exactly that cmd's flags to
|
||||
// the viper before execution
|
||||
commonflags.Bind(cmd)
|
||||
commonflags.BindAPI(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
sgIDFlag = "id"
|
||||
sgRawFlag = "raw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
storageGroupChildCommands := []*cobra.Command{
|
||||
sgPutCmd,
|
||||
sgGetCmd,
|
||||
sgListCmd,
|
||||
sgDelCmd,
|
||||
}
|
||||
|
||||
Cmd.AddCommand(storageGroupChildCommands...)
|
||||
|
||||
for _, sgCommand := range storageGroupChildCommands {
|
||||
objectCli.InitBearer(sgCommand)
|
||||
commonflags.InitAPI(sgCommand)
|
||||
}
|
||||
|
||||
initSGPutCmd()
|
||||
initSGGetCmd()
|
||||
initSGListCmd()
|
||||
initSGDeleteCmd()
|
||||
}
|
45
cmd/frostfs-cli/modules/storagegroup/util.go
Normal file
45
cmd/frostfs-cli/modules/storagegroup/util.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package storagegroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func readObjectAddress(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID) oid.Address {
|
||||
readCID(cmd, cnr)
|
||||
readSGID(cmd, obj)
|
||||
|
||||
var addr oid.Address
|
||||
addr.SetContainer(*cnr)
|
||||
addr.SetObject(*obj)
|
||||
return addr
|
||||
}
|
||||
|
||||
func readCID(cmd *cobra.Command, id *cid.ID) {
|
||||
f := cmd.Flag(commonflags.CIDFlag)
|
||||
if f == nil {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("missing container flag (%s)", commonflags.CIDFlag))
|
||||
return
|
||||
}
|
||||
|
||||
err := id.DecodeString(f.Value.String())
|
||||
common.ExitOnErr(cmd, "decode container ID string: %w", err)
|
||||
}
|
||||
|
||||
func readSGID(cmd *cobra.Command, id *oid.ID) {
|
||||
const flag = "id"
|
||||
|
||||
f := cmd.Flag(flag)
|
||||
if f == nil {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("missing storage group flag (%s)", flag))
|
||||
return
|
||||
}
|
||||
|
||||
err := id.DecodeString(f.Value.String())
|
||||
common.ExitOnErr(cmd, "decode storage group ID string: %w", err)
|
||||
}
|
95
cmd/frostfs-cli/modules/tree/add.go
Normal file
95
cmd/frostfs-cli/modules/tree/add.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/tree"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a node to the tree service",
|
||||
Run: add,
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
commonflags.Bind(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func initAddCmd() {
|
||||
commonflags.Init(addCmd)
|
||||
initCTID(addCmd)
|
||||
|
||||
ff := addCmd.Flags()
|
||||
ff.StringSlice(metaFlagKey, nil, "Meta pairs in the form of Key1=[0x]Value1,Key2=[0x]Value2")
|
||||
ff.Uint64(parentIDFlagKey, 0, "Parent node ID")
|
||||
|
||||
_ = cobra.MarkFlagRequired(ff, commonflags.RPC)
|
||||
}
|
||||
|
||||
func add(cmd *cobra.Command, _ []string) {
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
err := cnr.DecodeString(cmd.Flag(commonflags.CIDFlag).Value.String())
|
||||
common.ExitOnErr(cmd, "decode container ID string: %w", err)
|
||||
|
||||
tid, _ := cmd.Flags().GetString(treeIDFlagKey)
|
||||
pid, _ := cmd.Flags().GetUint64(parentIDFlagKey)
|
||||
|
||||
meta, err := parseMeta(cmd)
|
||||
common.ExitOnErr(cmd, "meta data parsing: %w", err)
|
||||
|
||||
ctx := cmd.Context()
|
||||
|
||||
cli, err := _client(ctx)
|
||||
common.ExitOnErr(cmd, "client: %w", err)
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
req := new(tree.AddRequest)
|
||||
req.Body = &tree.AddRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
TreeId: tid,
|
||||
ParentId: pid,
|
||||
Meta: meta,
|
||||
BearerToken: nil, // TODO: #1891 add token handling
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "message signing: %w", tree.SignMessage(req, pk))
|
||||
|
||||
resp, err := cli.Add(ctx, req)
|
||||
common.ExitOnErr(cmd, "rpc call: %w", err)
|
||||
|
||||
cmd.Println("Node ID: ", resp.Body.NodeId)
|
||||
}
|
||||
|
||||
func parseMeta(cmd *cobra.Command) ([]*tree.KeyValue, error) {
|
||||
raws, _ := cmd.Flags().GetStringSlice(metaFlagKey)
|
||||
if len(raws) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pairs := make([]*tree.KeyValue, 0, len(raws))
|
||||
for i := range raws {
|
||||
kv := strings.SplitN(raws[i], "=", 2)
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("invalid meta pair format: %s", raws[i])
|
||||
}
|
||||
|
||||
var pair tree.KeyValue
|
||||
pair.Key = kv[0]
|
||||
pair.Value = []byte(kv[1])
|
||||
|
||||
pairs = append(pairs, &pair)
|
||||
}
|
||||
|
||||
return pairs, nil
|
||||
}
|
94
cmd/frostfs-cli/modules/tree/add_by_path.go
Normal file
94
cmd/frostfs-cli/modules/tree/add_by_path.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/tree"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var addByPathCmd = &cobra.Command{
|
||||
Use: "add-by-path",
|
||||
Short: "Add a node by the path",
|
||||
Run: addByPath,
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
commonflags.Bind(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func initAddByPathCmd() {
|
||||
commonflags.Init(addByPathCmd)
|
||||
initCTID(addByPathCmd)
|
||||
|
||||
ff := addByPathCmd.Flags()
|
||||
|
||||
// tree service does not allow any attribute except
|
||||
// the 'FileName' but that's a limitation of the
|
||||
// current implementation, not the rule
|
||||
//ff.String(pathAttributeFlagKey, "", "Path attribute")
|
||||
ff.String(pathFlagKey, "", "Path to a node")
|
||||
ff.StringSlice(metaFlagKey, nil, "Meta pairs in the form of Key1=[0x]Value1,Key2=[0x]Value2")
|
||||
|
||||
_ = cobra.MarkFlagRequired(ff, commonflags.RPC)
|
||||
_ = cobra.MarkFlagRequired(ff, pathFlagKey)
|
||||
}
|
||||
|
||||
func addByPath(cmd *cobra.Command, _ []string) {
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cidRaw, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
|
||||
var cnr cid.ID
|
||||
err := cnr.DecodeString(cidRaw)
|
||||
common.ExitOnErr(cmd, "decode container ID string: %w", err)
|
||||
|
||||
tid, _ := cmd.Flags().GetString(treeIDFlagKey)
|
||||
ctx := cmd.Context()
|
||||
|
||||
cli, err := _client(ctx)
|
||||
common.ExitOnErr(cmd, "client: %w", err)
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
meta, err := parseMeta(cmd)
|
||||
common.ExitOnErr(cmd, "meta data parsing: %w", err)
|
||||
|
||||
path, _ := cmd.Flags().GetString(pathFlagKey)
|
||||
//pAttr, _ := cmd.Flags().GetString(pathAttributeFlagKey)
|
||||
|
||||
req := new(tree.AddByPathRequest)
|
||||
req.Body = &tree.AddByPathRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
TreeId: tid,
|
||||
PathAttribute: object.AttributeFileName,
|
||||
//PathAttribute: pAttr,
|
||||
Path: strings.Split(path, "/"),
|
||||
Meta: meta,
|
||||
BearerToken: nil, // TODO: #1891 add token handling
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "message signing: %w", tree.SignMessage(req, pk))
|
||||
|
||||
resp, err := cli.AddByPath(ctx, req)
|
||||
common.ExitOnErr(cmd, "rpc call: %w", err)
|
||||
|
||||
cmd.Printf("Parent ID: %d\n", resp.GetBody().GetParentId())
|
||||
|
||||
nn := resp.GetBody().GetNodes()
|
||||
if len(nn) == 0 {
|
||||
common.PrintVerbose("No new nodes were created")
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Println("Created nodes:")
|
||||
for _, node := range resp.GetBody().GetNodes() {
|
||||
cmd.Printf("\t%d\n", node)
|
||||
}
|
||||
}
|
40
cmd/frostfs-cli/modules/tree/client.go
Normal file
40
cmd/frostfs-cli/modules/tree/client.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/network"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/tree"
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// _client returns grpc Tree service client. Should be removed
|
||||
// after making Tree API public.
|
||||
func _client(ctx context.Context) (tree.TreeServiceClient, error) {
|
||||
var netAddr network.Address
|
||||
err := netAddr.FromString(viper.GetString(commonflags.RPC))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := make([]grpc.DialOption, 1, 2)
|
||||
opts[0] = grpc.WithBlock()
|
||||
|
||||
if !strings.HasPrefix(netAddr.URIAddr(), "grpcs:") {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
}
|
||||
|
||||
// a default connection establishing timeout
|
||||
const defaultClientConnectTimeout = time.Second * 2
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, defaultClientConnectTimeout)
|
||||
cc, err := grpc.DialContext(ctx, netAddr.URIAddr(), opts...)
|
||||
cancel()
|
||||
|
||||
return tree.NewTreeServiceClient(cc), err
|
||||
}
|
98
cmd/frostfs-cli/modules/tree/get_by_path.go
Normal file
98
cmd/frostfs-cli/modules/tree/get_by_path.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/tree"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getByPathCmd = &cobra.Command{
|
||||
Use: "get-by-path",
|
||||
Short: "Get a node by its path",
|
||||
Run: getByPath,
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
commonflags.Bind(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func initGetByPathCmd() {
|
||||
commonflags.Init(getByPathCmd)
|
||||
initCTID(getByPathCmd)
|
||||
|
||||
ff := getByPathCmd.Flags()
|
||||
|
||||
// tree service does not allow any attribute except
|
||||
// the 'FileName' but that's a limitation of the
|
||||
// current implementation, not the rule
|
||||
//ff.String(pathAttributeFlagKey, "", "Path attribute")
|
||||
ff.String(pathFlagKey, "", "Path to a node")
|
||||
|
||||
ff.Bool(latestOnlyFlagKey, false, "Look only for the latest version of a node")
|
||||
|
||||
_ = cobra.MarkFlagRequired(ff, commonflags.RPC)
|
||||
}
|
||||
|
||||
func getByPath(cmd *cobra.Command, _ []string) {
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
cidRaw, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
|
||||
var cnr cid.ID
|
||||
err := cnr.DecodeString(cidRaw)
|
||||
common.ExitOnErr(cmd, "decode container ID string: %w", err)
|
||||
|
||||
tid, _ := cmd.Flags().GetString(treeIDFlagKey)
|
||||
ctx := cmd.Context()
|
||||
|
||||
cli, err := _client(ctx)
|
||||
common.ExitOnErr(cmd, "client: %w", err)
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
latestOnly, _ := cmd.Flags().GetBool(latestOnlyFlagKey)
|
||||
path, _ := cmd.Flags().GetString(pathFlagKey)
|
||||
//pAttr, _ := cmd.Flags().GetString(pathAttributeFlagKey)
|
||||
|
||||
req := new(tree.GetNodeByPathRequest)
|
||||
req.Body = &tree.GetNodeByPathRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
TreeId: tid,
|
||||
PathAttribute: object.AttributeFileName,
|
||||
//PathAttribute: pAttr,
|
||||
Path: strings.Split(path, "/"),
|
||||
LatestOnly: latestOnly,
|
||||
AllAttributes: true,
|
||||
BearerToken: nil, // TODO: #1891 add token handling
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "message signing: %w", tree.SignMessage(req, pk))
|
||||
|
||||
resp, err := cli.GetNodeByPath(ctx, req)
|
||||
common.ExitOnErr(cmd, "rpc call: %w", err)
|
||||
|
||||
nn := resp.GetBody().GetNodes()
|
||||
if len(nn) == 0 {
|
||||
common.PrintVerbose("The node is not found")
|
||||
return
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
cmd.Printf("%d:\n", n.GetNodeId())
|
||||
|
||||
cmd.Println("\tParent ID: ", n.GetParentId())
|
||||
cmd.Println("\tTimestamp: ", n.GetTimestamp())
|
||||
|
||||
cmd.Println("\tMeta pairs: ")
|
||||
for _, kv := range n.GetMeta() {
|
||||
cmd.Printf("\t\t%s: %s\n", kv.GetKey(), string(kv.GetValue()))
|
||||
}
|
||||
}
|
||||
}
|
63
cmd/frostfs-cli/modules/tree/list.go
Normal file
63
cmd/frostfs-cli/modules/tree/list.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/services/tree"
|
||||
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Get tree IDs",
|
||||
Run: list,
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
commonflags.Bind(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func initListCmd() {
|
||||
commonflags.Init(listCmd)
|
||||
|
||||
ff := listCmd.Flags()
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = listCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
_ = cobra.MarkFlagRequired(ff, commonflags.RPC)
|
||||
}
|
||||
|
||||
func list(cmd *cobra.Command, _ []string) {
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
cidString, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
|
||||
var cnr cid.ID
|
||||
err := cnr.DecodeString(cidString)
|
||||
common.ExitOnErr(cmd, "decode container ID string: %w", err)
|
||||
|
||||
ctx := cmd.Context()
|
||||
|
||||
cli, err := _client(ctx)
|
||||
common.ExitOnErr(cmd, "client: %w", err)
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
req := &tree.TreeListRequest{
|
||||
Body: &tree.TreeListRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
},
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "message signing: %w", tree.SignMessage(req, pk))
|
||||
|
||||
resp, err := cli.TreeList(ctx, req)
|
||||
common.ExitOnErr(cmd, "rpc call: %w", err)
|
||||
|
||||
for _, treeID := range resp.GetBody().GetIds() {
|
||||
cmd.Println(treeID)
|
||||
}
|
||||
}
|
45
cmd/frostfs-cli/modules/tree/root.go
Normal file
45
cmd/frostfs-cli/modules/tree/root.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "tree",
|
||||
Short: "Operations with the Tree service",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(addCmd)
|
||||
Cmd.AddCommand(getByPathCmd)
|
||||
Cmd.AddCommand(addByPathCmd)
|
||||
Cmd.AddCommand(listCmd)
|
||||
|
||||
initAddCmd()
|
||||
initGetByPathCmd()
|
||||
initAddByPathCmd()
|
||||
initListCmd()
|
||||
}
|
||||
|
||||
const (
|
||||
treeIDFlagKey = "tid"
|
||||
parentIDFlagKey = "pid"
|
||||
|
||||
metaFlagKey = "meta"
|
||||
|
||||
pathFlagKey = "path"
|
||||
pathAttributeFlagKey = "pattr"
|
||||
|
||||
latestOnlyFlagKey = "latest"
|
||||
)
|
||||
|
||||
func initCTID(cmd *cobra.Command) {
|
||||
ff := cmd.Flags()
|
||||
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
_ = cmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||
|
||||
ff.String(treeIDFlagKey, "", "Tree ID")
|
||||
_ = cmd.MarkFlagRequired(treeIDFlagKey)
|
||||
}
|
330
cmd/frostfs-cli/modules/util/acl.go
Normal file
330
cmd/frostfs-cli/modules/util/acl.go
Normal file
|
@ -0,0 +1,330 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/flynn-archive/go-shlex"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PrettyPrintTableBACL print basic ACL in table format.
|
||||
func PrettyPrintTableBACL(cmd *cobra.Command, bacl *acl.Basic) {
|
||||
// Header
|
||||
w := tabwriter.NewWriter(cmd.OutOrStdout(), 1, 4, 4, ' ', 0)
|
||||
fmt.Fprintln(w, "\tRangeHASH\tRange\tSearch\tDelete\tPut\tHead\tGet")
|
||||
// Bits
|
||||
bits := []string{
|
||||
boolToString(bacl.Sticky()) + " " + boolToString(bacl.Extendable()),
|
||||
getRoleBitsForOperation(bacl, acl.OpObjectHash), getRoleBitsForOperation(bacl, acl.OpObjectRange),
|
||||
getRoleBitsForOperation(bacl, acl.OpObjectSearch), getRoleBitsForOperation(bacl, acl.OpObjectDelete),
|
||||
getRoleBitsForOperation(bacl, acl.OpObjectPut), getRoleBitsForOperation(bacl, acl.OpObjectHead),
|
||||
getRoleBitsForOperation(bacl, acl.OpObjectGet),
|
||||
}
|
||||
fmt.Fprintln(w, strings.Join(bits, "\t"))
|
||||
// Footer
|
||||
footer := []string{"X F"}
|
||||
for i := 0; i < 7; i++ {
|
||||
footer = append(footer, "U S O B")
|
||||
}
|
||||
fmt.Fprintln(w, strings.Join(footer, "\t"))
|
||||
|
||||
w.Flush()
|
||||
|
||||
cmd.Println(" X-Sticky F-Final U-User S-System O-Others B-Bearer")
|
||||
}
|
||||
|
||||
func getRoleBitsForOperation(bacl *acl.Basic, op acl.Op) string {
|
||||
return boolToString(bacl.IsOpAllowed(op, acl.RoleOwner)) + " " +
|
||||
boolToString(bacl.IsOpAllowed(op, acl.RoleContainer)) + " " +
|
||||
boolToString(bacl.IsOpAllowed(op, acl.RoleOthers)) + " " +
|
||||
boolToString(bacl.IsOpAllowed(op, acl.RoleInnerRing))
|
||||
}
|
||||
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
// PrettyPrintTableEACL print extended ACL in table format.
|
||||
func PrettyPrintTableEACL(cmd *cobra.Command, table *eacl.Table) {
|
||||
out := tablewriter.NewWriter(cmd.OutOrStdout())
|
||||
out.SetHeader([]string{"Operation", "Action", "Filters", "Targets"})
|
||||
out.SetAlignment(tablewriter.ALIGN_CENTER)
|
||||
out.SetRowLine(true)
|
||||
|
||||
out.SetAutoWrapText(false)
|
||||
|
||||
for _, r := range table.Records() {
|
||||
out.Append([]string{
|
||||
r.Operation().String(),
|
||||
r.Action().String(),
|
||||
eaclFiltersToString(r.Filters()),
|
||||
eaclTargetsToString(r.Targets()),
|
||||
})
|
||||
}
|
||||
|
||||
out.Render()
|
||||
}
|
||||
|
||||
func eaclTargetsToString(ts []eacl.Target) string {
|
||||
b := bytes.NewBuffer(nil)
|
||||
for _, t := range ts {
|
||||
keysExists := len(t.BinaryKeys()) > 0
|
||||
switch t.Role() {
|
||||
case eacl.RoleUser:
|
||||
b.WriteString("User")
|
||||
if keysExists {
|
||||
b.WriteString(": ")
|
||||
}
|
||||
case eacl.RoleSystem:
|
||||
b.WriteString("System")
|
||||
if keysExists {
|
||||
b.WriteString(": ")
|
||||
}
|
||||
case eacl.RoleOthers:
|
||||
b.WriteString("Others")
|
||||
if keysExists {
|
||||
b.WriteString(": ")
|
||||
}
|
||||
default:
|
||||
b.WriteString("Unknown")
|
||||
if keysExists {
|
||||
b.WriteString(": ")
|
||||
}
|
||||
}
|
||||
|
||||
for i, pub := range t.BinaryKeys() {
|
||||
if i != 0 {
|
||||
b.WriteString(" ")
|
||||
}
|
||||
b.WriteString(hex.EncodeToString(pub))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func eaclFiltersToString(fs []eacl.Filter) string {
|
||||
b := bytes.NewBuffer(nil)
|
||||
tw := tabwriter.NewWriter(b, 0, 0, 1, ' ', 0)
|
||||
|
||||
for _, f := range fs {
|
||||
switch f.From() {
|
||||
case eacl.HeaderFromObject:
|
||||
_, _ = tw.Write([]byte("O:\t"))
|
||||
case eacl.HeaderFromRequest:
|
||||
_, _ = tw.Write([]byte("R:\t"))
|
||||
case eacl.HeaderFromService:
|
||||
_, _ = tw.Write([]byte("S:\t"))
|
||||
default:
|
||||
_, _ = tw.Write([]byte(" \t"))
|
||||
}
|
||||
|
||||
_, _ = tw.Write([]byte(f.Key()))
|
||||
|
||||
switch f.Matcher() {
|
||||
case eacl.MatchStringEqual:
|
||||
_, _ = tw.Write([]byte("\t==\t"))
|
||||
case eacl.MatchStringNotEqual:
|
||||
_, _ = tw.Write([]byte("\t!=\t"))
|
||||
case eacl.MatchUnknown:
|
||||
}
|
||||
|
||||
_, _ = tw.Write([]byte(f.Value() + "\t"))
|
||||
_, _ = tw.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
_ = tw.Flush()
|
||||
|
||||
// To have nice output with tabwriter, we must append newline
|
||||
// after the last line. Here we strip it to delete empty line
|
||||
// in the final output.
|
||||
s := b.String()
|
||||
if len(s) > 0 {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseEACLRules parses eACL table.
|
||||
// Uses ParseEACLRule.
|
||||
//
|
||||
//nolint:godot
|
||||
func ParseEACLRules(table *eacl.Table, rules []string) error {
|
||||
if len(rules) == 0 {
|
||||
return errors.New("no extended ACL rules has been provided")
|
||||
}
|
||||
|
||||
for _, ruleStr := range rules {
|
||||
err := ParseEACLRule(table, ruleStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create extended acl record from rule '%s': %v", ruleStr, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseEACLRule parses eACL table from the following form:
|
||||
// <action> <operation> [<filter1> ...] [<target1> ...]
|
||||
//
|
||||
// Examples:
|
||||
// allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4
|
||||
//
|
||||
//nolint:godot
|
||||
func ParseEACLRule(table *eacl.Table, rule string) error {
|
||||
r, err := shlex.Split(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't parse rule '%s': %v", rule, err)
|
||||
}
|
||||
return parseEACLTable(table, r)
|
||||
}
|
||||
|
||||
func parseEACLTable(tb *eacl.Table, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return errors.New("at least 2 arguments must be provided")
|
||||
}
|
||||
|
||||
var action eacl.Action
|
||||
if !action.FromString(strings.ToUpper(args[0])) {
|
||||
return errors.New("invalid action (expected 'allow' or 'deny')")
|
||||
}
|
||||
|
||||
ops, err := eaclOperationsFromString(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := parseEACLRecord(args[2:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.SetAction(action)
|
||||
|
||||
for _, op := range ops {
|
||||
r := *r
|
||||
r.SetOperation(op)
|
||||
tb.AddRecord(&r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEACLRecord(args []string) (*eacl.Record, error) {
|
||||
r := new(eacl.Record)
|
||||
for i := range args {
|
||||
ss := strings.SplitN(args[i], ":", 2)
|
||||
|
||||
switch prefix := strings.ToLower(ss[0]); prefix {
|
||||
case "req", "obj": // filters
|
||||
if len(ss) != 2 {
|
||||
return nil, fmt.Errorf("invalid filter or target: %s", args[i])
|
||||
}
|
||||
|
||||
i := strings.Index(ss[1], "=")
|
||||
if i < 0 {
|
||||
return nil, fmt.Errorf("invalid filter key-value pair: %s", ss[1])
|
||||
}
|
||||
|
||||
var key, value string
|
||||
var op eacl.Match
|
||||
|
||||
if 0 < i && ss[1][i-1] == '!' {
|
||||
key = ss[1][:i-1]
|
||||
op = eacl.MatchStringNotEqual
|
||||
} else {
|
||||
key = ss[1][:i]
|
||||
op = eacl.MatchStringEqual
|
||||
}
|
||||
|
||||
value = ss[1][i+1:]
|
||||
|
||||
typ := eacl.HeaderFromRequest
|
||||
if ss[0] == "obj" {
|
||||
typ = eacl.HeaderFromObject
|
||||
}
|
||||
|
||||
r.AddFilter(typ, op, key, value)
|
||||
case "others", "system", "user", "pubkey": // targets
|
||||
var err error
|
||||
|
||||
var pubs []ecdsa.PublicKey
|
||||
if len(ss) == 2 {
|
||||
pubs, err = parseKeyList(ss[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var role eacl.Role
|
||||
if prefix != "pubkey" {
|
||||
role, err = eaclRoleFromString(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
eacl.AddFormedTarget(r, role, pubs...)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid prefix: %s", ss[0])
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// eaclRoleFromString parses eacl.Role from string.
|
||||
func eaclRoleFromString(s string) (eacl.Role, error) {
|
||||
var r eacl.Role
|
||||
if !r.FromString(strings.ToUpper(s)) {
|
||||
return r, fmt.Errorf("unexpected role %s", s)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// parseKeyList parses list of hex-encoded public keys separated by comma.
|
||||
func parseKeyList(s string) ([]ecdsa.PublicKey, error) {
|
||||
ss := strings.Split(s, ",")
|
||||
pubs := make([]ecdsa.PublicKey, len(ss))
|
||||
for i := range ss {
|
||||
st := strings.TrimPrefix(ss[i], "0x")
|
||||
pub, err := keys.NewPublicKeyFromString(st)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid public key '%s': %w", ss[i], err)
|
||||
}
|
||||
|
||||
pubs[i] = ecdsa.PublicKey(*pub)
|
||||
}
|
||||
|
||||
return pubs, nil
|
||||
}
|
||||
|
||||
// eaclOperationsFromString parses list of eacl.Operation separated by comma.
|
||||
func eaclOperationsFromString(s string) ([]eacl.Operation, error) {
|
||||
ss := strings.Split(s, ",")
|
||||
ops := make([]eacl.Operation, len(ss))
|
||||
|
||||
for i := range ss {
|
||||
if !ops[i].FromString(strings.ToUpper(ss[i])) {
|
||||
return nil, fmt.Errorf("invalid operation: %s", ss[i])
|
||||
}
|
||||
}
|
||||
|
||||
return ops, nil
|
||||
}
|
14
cmd/frostfs-cli/modules/util/convert.go
Normal file
14
cmd/frostfs-cli/modules/util/convert.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package util
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var convertCmd = &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Convert representation of NeoFS structures",
|
||||
}
|
||||
|
||||
func initConvertCmd() {
|
||||
convertCmd.AddCommand(convertEACLCmd)
|
||||
|
||||
initConvertEACLCmd()
|
||||
}
|
65
cmd/frostfs-cli/modules/util/convert_eacl.go
Normal file
65
cmd/frostfs-cli/modules/util/convert_eacl.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var convertEACLCmd = &cobra.Command{
|
||||
Use: "eacl",
|
||||
Short: "Convert representation of extended ACL table",
|
||||
Run: convertEACLTable,
|
||||
}
|
||||
|
||||
func initConvertEACLCmd() {
|
||||
flags := convertEACLCmd.Flags()
|
||||
|
||||
flags.String("from", "", "File with JSON or binary encoded extended ACL table")
|
||||
_ = convertEACLCmd.MarkFlagFilename("from")
|
||||
_ = convertEACLCmd.MarkFlagRequired("from")
|
||||
|
||||
flags.String("to", "", "File to dump extended ACL table (default: binary encoded)")
|
||||
flags.Bool(commonflags.JSON, false, "Dump extended ACL table in JSON encoding")
|
||||
}
|
||||
|
||||
func convertEACLTable(cmd *cobra.Command, _ []string) {
|
||||
pathFrom := cmd.Flag("from").Value.String()
|
||||
to := cmd.Flag("to").Value.String()
|
||||
jsonFlag, _ := cmd.Flags().GetBool(commonflags.JSON)
|
||||
|
||||
table := common.ReadEACL(cmd, pathFrom)
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if jsonFlag || len(to) == 0 {
|
||||
data, err = table.MarshalJSON()
|
||||
common.ExitOnErr(cmd, "can't JSON encode extended ACL table: %w", err)
|
||||
} else {
|
||||
data, err = table.Marshal()
|
||||
common.ExitOnErr(cmd, "can't binary encode extended ACL table: %w", err)
|
||||
}
|
||||
|
||||
if len(to) == 0 {
|
||||
prettyPrintJSON(cmd, data)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(to, data, 0644)
|
||||
common.ExitOnErr(cmd, "can't write exteded ACL table to file: %w", err)
|
||||
|
||||
cmd.Printf("extended ACL table was successfully dumped to %s\n", to)
|
||||
}
|
||||
|
||||
func prettyPrintJSON(cmd *cobra.Command, data []byte) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := json.Indent(buf, data, "", " "); err != nil {
|
||||
common.PrintVerbose("Can't pretty print json: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println(buf)
|
||||
}
|
99
cmd/frostfs-cli/modules/util/keyer.go
Normal file
99
cmd/frostfs-cli/modules/util/keyer.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/pkg/util/keyer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var keyerCmd = &cobra.Command{
|
||||
Use: "keyer",
|
||||
Short: "Generate or print information about keys",
|
||||
Run: processKeyer,
|
||||
}
|
||||
|
||||
var errKeyerSingleArgument = errors.New("pass only one argument at a time")
|
||||
|
||||
func initKeyerCmd() {
|
||||
keyerCmd.Flags().BoolP("generate", "g", false, "Generate new private key")
|
||||
keyerCmd.Flags().Bool("hex", false, "Print all values in hex encoding")
|
||||
keyerCmd.Flags().BoolP("uncompressed", "u", false, "Use uncompressed public key format")
|
||||
keyerCmd.Flags().BoolP("multisig", "m", false, "Calculate multisig address from public keys")
|
||||
}
|
||||
|
||||
func processKeyer(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
err error
|
||||
|
||||
result = new(keyer.Dashboard)
|
||||
generate, _ = cmd.Flags().GetBool("generate")
|
||||
useHex, _ = cmd.Flags().GetBool("hex")
|
||||
uncompressed, _ = cmd.Flags().GetBool("uncompressed")
|
||||
multisig, _ = cmd.Flags().GetBool("multisig")
|
||||
)
|
||||
|
||||
if multisig {
|
||||
err = result.ParseMultiSig(args)
|
||||
} else {
|
||||
if len(args) > 1 {
|
||||
common.ExitOnErr(cmd, "", errKeyerSingleArgument)
|
||||
}
|
||||
|
||||
var argument string
|
||||
if len(args) > 0 {
|
||||
argument = args[0]
|
||||
}
|
||||
|
||||
switch {
|
||||
case generate:
|
||||
err = keyerGenerate(argument, result)
|
||||
case fileExists(argument):
|
||||
err = keyerParseFile(argument, result)
|
||||
default:
|
||||
err = result.ParseString(argument)
|
||||
}
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
result.PrettyPrint(uncompressed, useHex)
|
||||
}
|
||||
|
||||
func keyerGenerate(filename string, d *keyer.Dashboard) error {
|
||||
key := make([]byte, keyer.NeoPrivateKeySize)
|
||||
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't get random source: %w", err)
|
||||
}
|
||||
|
||||
err = d.ParseBinary(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't parse key: %w", err)
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
return os.WriteFile(filename, key, 0600)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
return !os.IsNotExist(err) && !info.IsDir()
|
||||
}
|
||||
|
||||
func keyerParseFile(filename string, d *keyer.Dashboard) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't open %v file: %w", filename, err)
|
||||
}
|
||||
|
||||
return d.ParseBinary(data)
|
||||
}
|
18
cmd/frostfs-cli/modules/util/locode.go
Normal file
18
cmd/frostfs-cli/modules/util/locode.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// locode section.
|
||||
var locodeCmd = &cobra.Command{
|
||||
Use: "locode",
|
||||
Short: "Working with NeoFS UN/LOCODE database",
|
||||
}
|
||||
|
||||
func initLocodeCmd() {
|
||||
locodeCmd.AddCommand(locodeGenerateCmd, locodeInfoCmd)
|
||||
|
||||
initUtilLocodeInfoCmd()
|
||||
initUtilLocodeGenerateCmd()
|
||||
}
|
96
cmd/frostfs-cli/modules/util/locode_generate.go
Normal file
96
cmd/frostfs-cli/modules/util/locode_generate.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
locodedb "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/db"
|
||||
airportsdb "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/db/airports"
|
||||
locodebolt "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/db/boltdb"
|
||||
continentsdb "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/db/continents/geojson"
|
||||
csvlocode "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/table/csv"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type namesDB struct {
|
||||
*airportsdb.DB
|
||||
*csvlocode.Table
|
||||
}
|
||||
|
||||
const (
|
||||
locodeGenerateInputFlag = "in"
|
||||
locodeGenerateSubDivFlag = "subdiv"
|
||||
locodeGenerateAirportsFlag = "airports"
|
||||
locodeGenerateCountriesFlag = "countries"
|
||||
locodeGenerateContinentsFlag = "continents"
|
||||
locodeGenerateOutputFlag = "out"
|
||||
)
|
||||
|
||||
var (
|
||||
locodeGenerateInPaths []string
|
||||
locodeGenerateSubDivPath string
|
||||
locodeGenerateAirportsPath string
|
||||
locodeGenerateCountriesPath string
|
||||
locodeGenerateContinentsPath string
|
||||
locodeGenerateOutPath string
|
||||
|
||||
locodeGenerateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate UN/LOCODE database for NeoFS",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
locodeDB := csvlocode.New(
|
||||
csvlocode.Prm{
|
||||
Path: locodeGenerateInPaths[0],
|
||||
SubDivPath: locodeGenerateSubDivPath,
|
||||
},
|
||||
csvlocode.WithExtraPaths(locodeGenerateInPaths[1:]...),
|
||||
)
|
||||
|
||||
airportDB := airportsdb.New(airportsdb.Prm{
|
||||
AirportsPath: locodeGenerateAirportsPath,
|
||||
CountriesPath: locodeGenerateCountriesPath,
|
||||
})
|
||||
|
||||
continentsDB := continentsdb.New(continentsdb.Prm{
|
||||
Path: locodeGenerateContinentsPath,
|
||||
})
|
||||
|
||||
targetDB := locodebolt.New(locodebolt.Prm{
|
||||
Path: locodeGenerateOutPath,
|
||||
})
|
||||
|
||||
err := targetDB.Open()
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
defer targetDB.Close()
|
||||
|
||||
names := &namesDB{
|
||||
DB: airportDB,
|
||||
Table: locodeDB,
|
||||
}
|
||||
|
||||
err = locodedb.FillDatabase(locodeDB, airportDB, continentsDB, names, targetDB)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func initUtilLocodeGenerateCmd() {
|
||||
flags := locodeGenerateCmd.Flags()
|
||||
|
||||
flags.StringSliceVar(&locodeGenerateInPaths, locodeGenerateInputFlag, nil, "List of paths to UN/LOCODE tables (csv)")
|
||||
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateInputFlag)
|
||||
|
||||
flags.StringVar(&locodeGenerateSubDivPath, locodeGenerateSubDivFlag, "", "Path to UN/LOCODE subdivision database (csv)")
|
||||
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateSubDivFlag)
|
||||
|
||||
flags.StringVar(&locodeGenerateAirportsPath, locodeGenerateAirportsFlag, "", "Path to OpenFlights airport database (csv)")
|
||||
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateAirportsFlag)
|
||||
|
||||
flags.StringVar(&locodeGenerateCountriesPath, locodeGenerateCountriesFlag, "", "Path to OpenFlights country database (csv)")
|
||||
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateCountriesFlag)
|
||||
|
||||
flags.StringVar(&locodeGenerateContinentsPath, locodeGenerateContinentsFlag, "", "Path to continent polygons (GeoJSON)")
|
||||
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateContinentsFlag)
|
||||
|
||||
flags.StringVar(&locodeGenerateOutPath, locodeGenerateOutputFlag, "", "Target path for generated database")
|
||||
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateOutputFlag)
|
||||
}
|
56
cmd/frostfs-cli/modules/util/locode_info.go
Normal file
56
cmd/frostfs-cli/modules/util/locode_info.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
locodedb "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/db"
|
||||
locodebolt "github.com/TrueCloudLab/frostfs-node/pkg/util/locode/db/boltdb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
locodeInfoDBFlag = "db"
|
||||
locodeInfoCodeFlag = "locode"
|
||||
)
|
||||
|
||||
var (
|
||||
locodeInfoDBPath string
|
||||
locodeInfoCode string
|
||||
|
||||
locodeInfoCmd = &cobra.Command{
|
||||
Use: "info",
|
||||
Short: "Print information about UN/LOCODE from NeoFS database",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
targetDB := locodebolt.New(locodebolt.Prm{
|
||||
Path: locodeInfoDBPath,
|
||||
}, locodebolt.ReadOnly())
|
||||
|
||||
err := targetDB.Open()
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
defer targetDB.Close()
|
||||
|
||||
record, err := locodedb.LocodeRecord(targetDB, locodeInfoCode)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
cmd.Printf("Country: %s\n", record.CountryName())
|
||||
cmd.Printf("Location: %s\n", record.LocationName())
|
||||
cmd.Printf("Continent: %s\n", record.Continent())
|
||||
if subDivCode := record.SubDivCode(); subDivCode != "" {
|
||||
cmd.Printf("Subdivision: [%s] %s\n", subDivCode, record.SubDivName())
|
||||
}
|
||||
|
||||
geoPoint := record.GeoPoint()
|
||||
cmd.Printf("Coordinates: %0.2f, %0.2f\n", geoPoint.Latitude(), geoPoint.Longitude())
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func initUtilLocodeInfoCmd() {
|
||||
flags := locodeInfoCmd.Flags()
|
||||
|
||||
flags.StringVar(&locodeInfoDBPath, locodeInfoDBFlag, "", "Path to NeoFS UN/LOCODE database")
|
||||
_ = locodeInfoCmd.MarkFlagRequired(locodeInfoDBFlag)
|
||||
|
||||
flags.StringVar(&locodeInfoCode, locodeInfoCodeFlag, "", "UN/LOCODE")
|
||||
_ = locodeInfoCmd.MarkFlagRequired(locodeInfoCodeFlag)
|
||||
}
|
33
cmd/frostfs-cli/modules/util/root.go
Normal file
33
cmd/frostfs-cli/modules/util/root.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "util",
|
||||
Short: "Utility operations",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
flags := cmd.Flags()
|
||||
|
||||
_ = viper.BindPFlag(commonflags.GenerateKey, flags.Lookup(commonflags.GenerateKey))
|
||||
_ = viper.BindPFlag(commonflags.WalletPath, flags.Lookup(commonflags.WalletPath))
|
||||
_ = viper.BindPFlag(commonflags.Account, flags.Lookup(commonflags.Account))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(
|
||||
signCmd,
|
||||
convertCmd,
|
||||
keyerCmd,
|
||||
locodeCmd,
|
||||
)
|
||||
|
||||
initSignCmd()
|
||||
initConvertCmd()
|
||||
initKeyerCmd()
|
||||
initLocodeCmd()
|
||||
}
|
22
cmd/frostfs-cli/modules/util/sign.go
Normal file
22
cmd/frostfs-cli/modules/util/sign.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
signFromFlag = "from"
|
||||
signToFlag = "to"
|
||||
)
|
||||
|
||||
var signCmd = &cobra.Command{
|
||||
Use: "sign",
|
||||
Short: "Sign NeoFS structure",
|
||||
}
|
||||
|
||||
func initSignCmd() {
|
||||
signCmd.AddCommand(signBearerCmd, signSessionCmd)
|
||||
|
||||
initSignBearerCmd()
|
||||
initSignSessionCmd()
|
||||
}
|
63
cmd/frostfs-cli/modules/util/sign_bearer.go
Normal file
63
cmd/frostfs-cli/modules/util/sign_bearer.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
signBearerJSONFlag = commonflags.JSON
|
||||
)
|
||||
|
||||
var signBearerCmd = &cobra.Command{
|
||||
Use: "bearer-token",
|
||||
Short: "Sign bearer token to use it in requests",
|
||||
Run: signBearerToken,
|
||||
}
|
||||
|
||||
func initSignBearerCmd() {
|
||||
commonflags.InitWithoutRPC(signBearerCmd)
|
||||
|
||||
flags := signBearerCmd.Flags()
|
||||
|
||||
flags.String(signFromFlag, "", "File with JSON or binary encoded bearer token to sign")
|
||||
_ = signBearerCmd.MarkFlagFilename(signFromFlag)
|
||||
_ = signBearerCmd.MarkFlagRequired(signFromFlag)
|
||||
|
||||
flags.String(signToFlag, "", "File to dump signed bearer token (default: binary encoded)")
|
||||
flags.Bool(signBearerJSONFlag, false, "Dump bearer token in JSON encoding")
|
||||
}
|
||||
|
||||
func signBearerToken(cmd *cobra.Command, _ []string) {
|
||||
btok := common.ReadBearerToken(cmd, signFromFlag)
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
err := btok.Sign(*pk)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
to := cmd.Flag(signToFlag).Value.String()
|
||||
jsonFlag, _ := cmd.Flags().GetBool(signBearerJSONFlag)
|
||||
|
||||
var data []byte
|
||||
if jsonFlag || len(to) == 0 {
|
||||
data, err = btok.MarshalJSON()
|
||||
common.ExitOnErr(cmd, "can't JSON encode bearer token: %w", err)
|
||||
} else {
|
||||
data = btok.Marshal()
|
||||
}
|
||||
|
||||
if len(to) == 0 {
|
||||
prettyPrintJSON(cmd, data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(to, data, 0644)
|
||||
common.ExitOnErr(cmd, "can't write signed bearer token to file: %w", err)
|
||||
|
||||
cmd.Printf("signed bearer token was successfully dumped to %s\n", to)
|
||||
}
|
84
cmd/frostfs-cli/modules/util/sign_session.go
Normal file
84
cmd/frostfs-cli/modules/util/sign_session.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var signSessionCmd = &cobra.Command{
|
||||
Use: "session-token",
|
||||
Short: "Sign session token to use it in requests",
|
||||
Run: signSessionToken,
|
||||
}
|
||||
|
||||
func initSignSessionCmd() {
|
||||
commonflags.InitWithoutRPC(signSessionCmd)
|
||||
|
||||
flags := signSessionCmd.Flags()
|
||||
|
||||
flags.String(signFromFlag, "", "File with JSON encoded session token to sign")
|
||||
_ = signSessionCmd.MarkFlagFilename(signFromFlag)
|
||||
_ = signSessionCmd.MarkFlagRequired(signFromFlag)
|
||||
|
||||
flags.String(signToFlag, "", "File to save signed session token (optional)")
|
||||
}
|
||||
|
||||
func signSessionToken(cmd *cobra.Command, _ []string) {
|
||||
fPath, err := cmd.Flags().GetString(signFromFlag)
|
||||
common.ExitOnErr(cmd, "", err)
|
||||
|
||||
if fPath == "" {
|
||||
common.ExitOnErr(cmd, "", errors.New("missing session token flag"))
|
||||
}
|
||||
|
||||
type iTokenSession interface {
|
||||
json.Marshaler
|
||||
common.BinaryOrJSON
|
||||
Sign(ecdsa.PrivateKey) error
|
||||
}
|
||||
var errLast error
|
||||
var stok iTokenSession
|
||||
|
||||
for _, el := range [...]iTokenSession{
|
||||
new(session.Object),
|
||||
new(session.Container),
|
||||
} {
|
||||
errLast = common.ReadBinaryOrJSON(el, fPath)
|
||||
if errLast == nil {
|
||||
stok = el
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
common.ExitOnErr(cmd, "decode session: %v", errLast)
|
||||
|
||||
pk := key.GetOrGenerate(cmd)
|
||||
|
||||
err = stok.Sign(*pk)
|
||||
common.ExitOnErr(cmd, "can't sign token: %w", err)
|
||||
|
||||
data, err := stok.MarshalJSON()
|
||||
common.ExitOnErr(cmd, "can't encode session token: %w", err)
|
||||
|
||||
to := cmd.Flag(signToFlag).Value.String()
|
||||
if len(to) == 0 {
|
||||
prettyPrintJSON(cmd, data)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(to, data, 0644)
|
||||
if err != nil {
|
||||
common.ExitOnErr(cmd, "", fmt.Errorf("can't write signed session token to %s: %w", to, err))
|
||||
}
|
||||
|
||||
cmd.Printf("signed session token saved in %s\n", to)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue