Move to frostfs-node

Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
This commit is contained in:
Pavel Karpy 2022-12-23 20:35:35 +03:00 committed by Stanislav Bogatyrev
parent 42554a9298
commit 923f84722a
934 changed files with 3470 additions and 3451 deletions

View 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`

View 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
}

View 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

View 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
}

View 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
}

View 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())
}
}

View 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
}

View 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)
}

View 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)
}

View 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)
}
}

View 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
}

View 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)
}

View 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))
}

View 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"
)

View 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))
}

View file

@ -0,0 +1,3 @@
package commonflags
const JSON = "json"

View 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),
)
}

View 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)
}

View 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()
}

View 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
View file

@ -0,0 +1,7 @@
package main
import cmd "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules"
func main() {
cmd.Execute()
}

View 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))
}
}

View 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()
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View file

@ -0,0 +1,9 @@
package cmd
import (
"github.com/TrueCloudLab/frostfs-node/pkg/util/autocomplete"
)
func init() {
rootCmd.AddCommand(autocomplete.Command("frostfs-cli"))
}

View 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
}

View 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")
}

View 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
}

View 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")
}

View 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",
)
}

View 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",
)
}

View 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")
}

View 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)
}
}

View 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")
}

View 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
}

View 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)
}

View 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)
}

View 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)
}

View 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())
}

View 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()
}

View 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.")
}

View 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()
}

View 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)
}

View 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"
}

View 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)
}

View 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
}

View 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.")
}

View 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)
}

View 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)
}

View 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)
}

View 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)
})
}

View 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()
}

View 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)
}

View 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)
}

View 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
}

View 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)
}
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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()
}

View 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
}

View 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
}

View 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())
}
}

View 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
}

View 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)
}

View 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)
}

View 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())
}
}
}

View 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())
}
}

View 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
}
}

View 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()
}

View 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)
}

View 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
}

View 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)
}
}

View 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
}

View 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()))
}
}
}

View 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)
}
}

View 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)
}

View 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
}

View 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()
}

View 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)
}

View 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)
}

View 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()
}

View 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)
}

View 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)
}

View 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()
}

View 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()
}

View 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)
}

View 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)
}