Remove yet more eACL leftovers #1483
11 changed files with 1 additions and 191 deletions
|
@ -98,7 +98,7 @@ See `frostfs-contract`'s README.md for build instructions.
|
||||||
4. To create container and put object into it run (container and object IDs will be different):
|
4. To create container and put object into it run (container and object IDs will be different):
|
||||||
|
|
||||||
```
|
```
|
||||||
./bin/frostfs-cli container create -r 127.0.0.1:8080 --wallet ./dev/wallet.json --policy "REP 1 IN X CBF 1 SELECT 1 FROM * AS X" --basic-acl public-read-write --await
|
./bin/frostfs-cli container create -r 127.0.0.1:8080 --wallet ./dev/wallet.json --policy "REP 1 IN X CBF 1 SELECT 1 FROM * AS X" --await
|
||||||
Enter password > <- press ENTER, the is no password for wallet
|
Enter password > <- press ENTER, the is no password for wallet
|
||||||
CID: CfPhEuHQ2PRvM4gfBQDC4dWZY3NccovyfcnEdiq2ixju
|
CID: CfPhEuHQ2PRvM4gfBQDC4dWZY3NccovyfcnEdiq2ixju
|
||||||
|
|
||||||
|
|
|
@ -72,4 +72,3 @@ All other `object` sub-commands support only static sessions (2).
|
||||||
List of commands supporting sessions (static only):
|
List of commands supporting sessions (static only):
|
||||||
- `create`
|
- `create`
|
||||||
- `delete`
|
- `delete`
|
||||||
- `set-eacl`
|
|
||||||
|
|
|
@ -15,14 +15,12 @@ import (
|
||||||
containerApi "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
|
containerApi "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
containerACL string
|
|
||||||
containerPolicy string
|
containerPolicy string
|
||||||
containerAttributes []string
|
containerAttributes []string
|
||||||
containerAwait bool
|
containerAwait bool
|
||||||
|
@ -89,9 +87,6 @@ It will be stored in sidechain when inner ring will accepts it.`,
|
||||||
err = parseAttributes(&cnr, containerAttributes)
|
err = parseAttributes(&cnr, containerAttributes)
|
||||||
commonCmd.ExitOnErr(cmd, "", err)
|
commonCmd.ExitOnErr(cmd, "", err)
|
||||||
|
|
||||||
var basicACL acl.Basic
|
|
||||||
commonCmd.ExitOnErr(cmd, "decode basic ACL string: %w", basicACL.DecodeString(containerACL))
|
|
||||||
|
|
||||||
tok := getSession(cmd)
|
tok := getSession(cmd)
|
||||||
|
|
||||||
if tok != nil {
|
if tok != nil {
|
||||||
|
@ -105,7 +100,6 @@ It will be stored in sidechain when inner ring will accepts it.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
cnr.SetPlacementPolicy(*placementPolicy)
|
cnr.SetPlacementPolicy(*placementPolicy)
|
||||||
cnr.SetBasicACL(basicACL)
|
|
||||||
|
|
||||||
var syncContainerPrm internalclient.SyncContainerPrm
|
var syncContainerPrm internalclient.SyncContainerPrm
|
||||||
syncContainerPrm.SetClient(cli)
|
syncContainerPrm.SetClient(cli)
|
||||||
|
@ -163,10 +157,6 @@ func initContainerCreateCmd() {
|
||||||
flags.DurationP(commonflags.Timeout, commonflags.TimeoutShorthand, commonflags.TimeoutDefault, commonflags.TimeoutUsage)
|
flags.DurationP(commonflags.Timeout, commonflags.TimeoutShorthand, commonflags.TimeoutDefault, commonflags.TimeoutUsage)
|
||||||
flags.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
flags.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||||
flags.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
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.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.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.BoolVar(&containerAwait, "await", false, "Block execution until container is persisted")
|
||||||
|
|
|
@ -196,31 +196,6 @@ func (s ttlContainerStorage) DeletionInfo(cnr cid.ID) (*container.DelInfo, error
|
||||||
return s.delInfoCache.get(cnr)
|
return s.delInfoCache.get(cnr)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ttlEACLStorage struct {
|
|
||||||
*ttlNetCache[cid.ID, *container.EACL]
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCachedEACLStorage(v container.EACLSource, ttl time.Duration) ttlEACLStorage {
|
|
||||||
const eaclCacheSize = 100
|
|
||||||
|
|
||||||
lruCnrCache := newNetworkTTLCache(eaclCacheSize, ttl, func(id cid.ID) (*container.EACL, error) {
|
|
||||||
return v.GetEACL(id)
|
|
||||||
}, metrics.NewCacheMetrics("eacl"))
|
|
||||||
|
|
||||||
return ttlEACLStorage{lruCnrCache}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEACL returns eACL value from the cache. If value is missing in the cache
|
|
||||||
// or expired, then it returns value from side chain and updates cache.
|
|
||||||
func (s ttlEACLStorage) GetEACL(cnr cid.ID) (*container.EACL, error) {
|
|
||||||
return s.get(cnr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidateEACL removes cached eACL value.
|
|
||||||
func (s ttlEACLStorage) InvalidateEACL(cnr cid.ID) {
|
|
||||||
s.remove(cnr)
|
|
||||||
}
|
|
||||||
|
|
||||||
type lruNetmapSource struct {
|
type lruNetmapSource struct {
|
||||||
netState netmap.State
|
netState netmap.State
|
||||||
|
|
||||||
|
|
|
@ -642,8 +642,6 @@ type cfgObject struct {
|
||||||
|
|
||||||
cnrSource container.Source
|
cnrSource container.Source
|
||||||
|
|
||||||
eaclSource container.EACLSource
|
|
||||||
|
|
||||||
cfgAccessPolicyEngine cfgAccessPolicyEngine
|
cfgAccessPolicyEngine cfgAccessPolicyEngine
|
||||||
|
|
||||||
pool cfgObjectRoutines
|
pool cfgObjectRoutines
|
||||||
|
|
|
@ -70,10 +70,6 @@ func initContainerService(_ context.Context, c *cfg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureEACLAndContainerSources(c *cfg, client *cntClient.Client, cnrSrc containerCore.Source) (*morphContainerReader, *morphContainerWriter) {
|
func configureEACLAndContainerSources(c *cfg, client *cntClient.Client, cnrSrc containerCore.Source) (*morphContainerReader, *morphContainerWriter) {
|
||||||
eACLFetcher := &morphEACLFetcher{
|
|
||||||
w: client,
|
|
||||||
}
|
|
||||||
|
|
||||||
cnrRdr := new(morphContainerReader)
|
cnrRdr := new(morphContainerReader)
|
||||||
|
|
||||||
cnrWrt := &morphContainerWriter{
|
cnrWrt := &morphContainerWriter{
|
||||||
|
@ -81,8 +77,6 @@ func configureEACLAndContainerSources(c *cfg, client *cntClient.Client, cnrSrc c
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.cfgMorph.cacheTTL <= 0 {
|
if c.cfgMorph.cacheTTL <= 0 {
|
||||||
c.cfgObject.eaclSource = eACLFetcher
|
|
||||||
cnrRdr.eacl = eACLFetcher
|
|
||||||
c.cfgObject.cnrSource = cnrSrc
|
c.cfgObject.cnrSource = cnrSrc
|
||||||
cnrRdr.src = cnrSrc
|
cnrRdr.src = cnrSrc
|
||||||
cnrRdr.lister = client
|
cnrRdr.lister = client
|
||||||
|
@ -126,11 +120,7 @@ func configureEACLAndContainerSources(c *cfg, client *cntClient.Client, cnrSrc c
|
||||||
c.cfgObject.cnrSource = containerCache
|
c.cfgObject.cnrSource = containerCache
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedEACLStorage := newCachedEACLStorage(eACLFetcher, c.cfgMorph.cacheTTL)
|
|
||||||
c.cfgObject.eaclSource = cachedEACLStorage
|
|
||||||
|
|
||||||
cnrRdr.lister = client
|
cnrRdr.lister = client
|
||||||
cnrRdr.eacl = c.cfgObject.eaclSource
|
|
||||||
cnrRdr.src = c.cfgObject.cnrSource
|
cnrRdr.src = c.cfgObject.cnrSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,8 +211,6 @@ func (c *cfg) ExternalAddresses() []string {
|
||||||
|
|
||||||
// implements interface required by container service provided by morph executor.
|
// implements interface required by container service provided by morph executor.
|
||||||
type morphContainerReader struct {
|
type morphContainerReader struct {
|
||||||
eacl containerCore.EACLSource
|
|
||||||
|
|
||||||
src containerCore.Source
|
src containerCore.Source
|
||||||
|
|
||||||
lister interface {
|
lister interface {
|
||||||
|
@ -238,10 +226,6 @@ func (x *morphContainerReader) DeletionInfo(id cid.ID) (*containerCore.DelInfo,
|
||||||
return x.src.DeletionInfo(id)
|
return x.src.DeletionInfo(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *morphContainerReader) GetEACL(id cid.ID) (*containerCore.EACL, error) {
|
|
||||||
return x.eacl.GetEACL(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *morphContainerReader) ContainersOf(id *user.ID) ([]cid.ID, error) {
|
func (x *morphContainerReader) ContainersOf(id *user.ID) ([]cid.ID, error) {
|
||||||
return x.lister.ContainersOf(id)
|
return x.lister.ContainersOf(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
@ -14,7 +13,6 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/engine"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/engine"
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
||||||
cntClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
|
|
||||||
nmClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
nmClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/cache"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/cache"
|
||||||
objectTransportGRPC "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/transport/object/grpc"
|
objectTransportGRPC "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/transport/object/grpc"
|
||||||
|
@ -37,7 +35,6 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/replicator"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/replicator"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
|
||||||
objectGRPC "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object/grpc"
|
objectGRPC "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object/grpc"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
@ -481,29 +478,6 @@ func createAPEService(c *cfg, splitSvc *objectService.TransportSplitter) *object
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type morphEACLFetcher struct {
|
|
||||||
w *cntClient.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *morphEACLFetcher) GetEACL(cnr cid.ID) (*containercore.EACL, error) {
|
|
||||||
eaclInfo, err := s.w.GetEACL(cnr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
binTable, err := eaclInfo.Value.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("marshal eACL table: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !eaclInfo.Signature.Verify(binTable) {
|
|
||||||
// TODO(@cthulhu-rider): #468 use "const" error
|
|
||||||
return nil, errors.New("invalid signature of the eACL table")
|
|
||||||
}
|
|
||||||
|
|
||||||
return eaclInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type engineWithoutNotifications struct {
|
type engineWithoutNotifications struct {
|
||||||
engine *engine.StorageEngine
|
engine *engine.StorageEngine
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,16 +58,3 @@ type EACL struct {
|
||||||
// Session within which Value was set. Nil means session absence.
|
// Session within which Value was set. Nil means session absence.
|
||||||
Session *session.Container
|
Session *session.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// EACLSource is the interface that wraps
|
|
||||||
// basic methods of extended ACL table source.
|
|
||||||
type EACLSource interface {
|
|
||||||
// GetEACL reads the table from the source by identifier.
|
|
||||||
// It returns any error encountered.
|
|
||||||
//
|
|
||||||
// GetEACL must return exactly one non-nil value.
|
|
||||||
//
|
|
||||||
// Must return apistatus.ErrEACLNotFound if requested
|
|
||||||
// eACL table is not in source.
|
|
||||||
GetEACL(cid.ID) (*EACL, error)
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ const (
|
||||||
getMethod = "get"
|
getMethod = "get"
|
||||||
listMethod = "list"
|
listMethod = "list"
|
||||||
containersOfMethod = "containersOf"
|
containersOfMethod = "containersOf"
|
||||||
eaclMethod = "eACL"
|
|
||||||
deletionInfoMethod = "deletionInfo"
|
deletionInfoMethod = "deletionInfo"
|
||||||
|
|
||||||
// putNamedMethod is method name for container put with an alias. It is exported to provide custom fee.
|
// putNamedMethod is method name for container put with an alias. It is exported to provide custom fee.
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetEACL reads the extended ACL table from FrostFS system
|
|
||||||
// through Container contract call.
|
|
||||||
//
|
|
||||||
// Returns apistatus.EACLNotFound if eACL table is missing in the contract.
|
|
||||||
func (c *Client) GetEACL(cnr cid.ID) (*container.EACL, error) {
|
|
||||||
binCnr := make([]byte, sha256.Size)
|
|
||||||
cnr.Encode(binCnr)
|
|
||||||
|
|
||||||
prm := client.TestInvokePrm{}
|
|
||||||
prm.SetMethod(eaclMethod)
|
|
||||||
prm.SetArgs(binCnr)
|
|
||||||
|
|
||||||
prms, err := c.client.TestInvoke(prm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not perform test invocation (%s): %w", eaclMethod, err)
|
|
||||||
} else if ln := len(prms); ln != 1 {
|
|
||||||
return nil, fmt.Errorf("unexpected stack item count (%s): %d", eaclMethod, ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr, err := client.ArrayFromStackItem(prms[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get item array of eACL (%s): %w", eaclMethod, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(arr) != 4 {
|
|
||||||
return nil, fmt.Errorf("unexpected eacl stack item count (%s): %d", eaclMethod, len(arr))
|
|
||||||
}
|
|
||||||
|
|
||||||
rawEACL, err := client.BytesFromStackItem(arr[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get byte array of eACL (%s): %w", eaclMethod, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sig, err := client.BytesFromStackItem(arr[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get byte array of eACL signature (%s): %w", eaclMethod, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client may not return errors if the table is missing, so check this case additionally.
|
|
||||||
// The absence of a signature in the response can be taken as an eACL absence criterion,
|
|
||||||
// since unsigned table cannot be approved in the storage by design.
|
|
||||||
if len(sig) == 0 {
|
|
||||||
return nil, new(apistatus.EACLNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := client.BytesFromStackItem(arr[2])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get byte array of eACL public key (%s): %w", eaclMethod, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
binToken, err := client.BytesFromStackItem(arr[3])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get byte array of eACL session token (%s): %w", eaclMethod, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res container.EACL
|
|
||||||
|
|
||||||
res.Value = eacl.NewTable()
|
|
||||||
if err = res.Value.Unmarshal(rawEACL); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(binToken) > 0 {
|
|
||||||
res.Session = new(session.Container)
|
|
||||||
|
|
||||||
err = res.Session.Unmarshal(binToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not unmarshal session token: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(@cthulhu-rider): #468 implement and use another approach to avoid conversion
|
|
||||||
var sigV2 refs.Signature
|
|
||||||
sigV2.SetKey(pub)
|
|
||||||
sigV2.SetSign(sig)
|
|
||||||
sigV2.SetScheme(refs.ECDSA_RFC6979_SHA256)
|
|
||||||
|
|
||||||
err = res.Signature.ReadFromV2(sigV2)
|
|
||||||
return &res, err
|
|
||||||
}
|
|
|
@ -25,7 +25,6 @@ type morphExecutor struct {
|
||||||
// Reader is an interface of read-only container storage.
|
// Reader is an interface of read-only container storage.
|
||||||
type Reader interface {
|
type Reader interface {
|
||||||
containercore.Source
|
containercore.Source
|
||||||
containercore.EACLSource
|
|
||||||
|
|
||||||
// ContainersOf returns a list of container identifiers belonging
|
// ContainersOf returns a list of container identifiers belonging
|
||||||
// to the specified user of FrostFS system. Returns the identifiers
|
// to the specified user of FrostFS system. Returns the identifiers
|
||||||
|
|
Loading…
Reference in a new issue