diff --git a/client/netmap.go b/client/netmap.go index 5bee411c..98d01a04 100644 --- a/client/netmap.go +++ b/client/netmap.go @@ -196,3 +196,83 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetwo return &res, nil } + +// PrmNetMapSnapshot groups parameters of NetMapSnapshot operation. +type PrmNetMapSnapshot struct { + prmCommonMeta +} + +// ResNetMapSnapshot groups resulting values of NetMapSnapshot operation. +type ResNetMapSnapshot struct { + statusRes + + netMap netmap.NetMap +} + +// NetMap returns current server's local network map. +func (x ResNetMapSnapshot) NetMap() netmap.NetMap { + return x.netMap +} + +// NetMapSnapshot requests current network view of the remote server. +// +// Any client's internal or transport errors are returned as `error`. +// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful +// NeoFS status codes are returned as `error`, otherwise, are included +// in the returned result structure. +// +// Context is required and MUST NOT be nil. It is used for network communication. +// +// Exactly one return value is non-nil. Server status return is returned in ResNetMapSnapshot. +// Reflects all internal errors in second return value (transport problems, response processing, etc.). +// +// Return statuses: +// - global (see Client docs). +func (c *Client) NetMapSnapshot(ctx context.Context, prm PrmNetMapSnapshot) (*ResNetMapSnapshot, error) { + // check context + if ctx == nil { + panic(panicMsgMissingContext) + } + + // form request + var req v2netmap.SnapshotRequest + + // init call context + + var ( + cc contextCall + res ResNetMapSnapshot + ) + + c.initCallContext(&cc) + cc.meta = prm.prmCommonMeta + cc.req = &req + cc.statusRes = &res + cc.call = func() (responseV2, error) { + return rpcapi.NetMapSnapshot(&c.c, &req, client.WithContext(ctx)) + } + cc.result = func(r responseV2) { + resp := r.(*v2netmap.SnapshotResponse) + + const fieldNetMap = "network map" + + netMapV2 := resp.GetBody().NetMap() + if netMapV2 == nil { + cc.err = newErrMissingResponseField(fieldNetMap) + return + } + + cc.err = res.netMap.ReadFromV2(*netMapV2) + if cc.err != nil { + cc.err = newErrInvalidResponseField(fieldNetMap, cc.err) + return + } + } + + // process call + if !cc.processCall() { + return nil, cc.err + } + + return &res, nil +} diff --git a/go.mod b/go.mod index d4cf8b8d..00f2e23e 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mr-tron/base58 v1.2.0 github.com/nspcc-dev/hrw v1.0.9 github.com/nspcc-dev/neo-go v0.99.2 - github.com/nspcc-dev/neofs-api-go/v2 v2.13.2-0.20220916145053-f3e1f8ae7ae3 + github.com/nspcc-dev/neofs-api-go/v2 v2.13.2-0.20220919124434-cf868188ef9c github.com/nspcc-dev/neofs-contract v0.15.3 github.com/nspcc-dev/tzhash v1.6.1 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index e601ef11..8168942a 100644 --- a/go.sum +++ b/go.sum @@ -263,8 +263,8 @@ github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220809123759-3094d3e0c14b h1:J7 github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220809123759-3094d3e0c14b/go.mod h1:23bBw0v6pBYcrWs8CBEEDIEDJNbcFoIh8pGGcf2Vv8s= github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211201134523-3604d96f3fe1/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs= github.com/nspcc-dev/neofs-api-go/v2 v2.11.1/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs= -github.com/nspcc-dev/neofs-api-go/v2 v2.13.2-0.20220916145053-f3e1f8ae7ae3 h1:ZPulLHstChUJBKzRcfRSAwZWAZ+jMpPRtSE4Q4EERqM= -github.com/nspcc-dev/neofs-api-go/v2 v2.13.2-0.20220916145053-f3e1f8ae7ae3/go.mod h1:DRIr0Ic1s+6QgdqmNFNLIqMqd7lNMJfYwkczlm1hDtM= +github.com/nspcc-dev/neofs-api-go/v2 v2.13.2-0.20220919124434-cf868188ef9c h1:YZwtBY9uypaShbe/NLhosDanIfxt8VhQlSLYUeFIWv8= +github.com/nspcc-dev/neofs-api-go/v2 v2.13.2-0.20220919124434-cf868188ef9c/go.mod h1:DRIr0Ic1s+6QgdqmNFNLIqMqd7lNMJfYwkczlm1hDtM= github.com/nspcc-dev/neofs-contract v0.15.3 h1:7+NwyTtxFAnIevz0hR/XxQf6R2Ej2scjVR2bnnJnhBM= github.com/nspcc-dev/neofs-contract v0.15.3/go.mod h1:BXVZUZUJxrmmDETglXHI8+5DSgn84B9y5DoSWqEjYCs= github.com/nspcc-dev/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA= diff --git a/netmap/netmap.go b/netmap/netmap.go index 2f6b2b8a..3d312db5 100644 --- a/netmap/netmap.go +++ b/netmap/netmap.go @@ -9,10 +9,63 @@ import ( // NetMap represents NeoFS network map. It includes information about all // storage nodes registered in NeoFS the network. +// +// NetMap is mutually compatible with github.com/nspcc-dev/neofs-api-go/v2/netmap.NetMap +// message. See ReadFromV2 / WriteToV2 methods. +// +// Instances can be created using built-in var declaration. type NetMap struct { + epoch uint64 + nodes []NodeInfo } +// ReadFromV2 reads NetMap from the netmap.NetMap message. Checks if the +// message conforms to NeoFS API V2 protocol. +// +// See also WriteToV2. +func (m *NetMap) ReadFromV2(msg netmap.NetMap) error { + var err error + nodes := msg.Nodes() + + if nodes == nil { + m.nodes = nil + } else { + m.nodes = make([]NodeInfo, len(nodes)) + + for i := range nodes { + err = m.nodes[i].ReadFromV2(nodes[i]) + if err != nil { + return fmt.Errorf("invalid node info: %w", err) + } + } + } + + m.epoch = msg.Epoch() + + return nil +} + +// WriteToV2 writes NetMap to the netmap.NetMap message. The message +// MUST NOT be nil. +// +// See also ReadFromV2. +func (m NetMap) WriteToV2(msg *netmap.NetMap) { + var nodes []netmap.NodeInfo + + if m.nodes != nil { + nodes = make([]netmap.NodeInfo, len(m.nodes)) + + for i := range m.nodes { + m.nodes[i].WriteToV2(&nodes[i]) + } + + msg.SetNodes(nodes) + } + + msg.SetEpoch(m.epoch) +} + // SetNodes sets information list about all storage nodes from the NeoFS network. // // Argument MUST NOT be mutated, make a copy first. @@ -29,6 +82,20 @@ func (m NetMap) Nodes() []NodeInfo { return m.nodes } +// SetEpoch specifies revision number of the NetMap. +// +// See also Epoch. +func (m *NetMap) SetEpoch(epoch uint64) { + m.epoch = epoch +} + +// Epoch returns epoch set using SetEpoch. +// +// Zero NetMap has zero revision. +func (m NetMap) Epoch() uint64 { + return m.epoch +} + // nodes is a slice of NodeInfo instances needed for HRW sorting. type nodes []NodeInfo diff --git a/netmap/netmap_test.go b/netmap/netmap_test.go new file mode 100644 index 00000000..67af033e --- /dev/null +++ b/netmap/netmap_test.go @@ -0,0 +1,47 @@ +package netmap_test + +import ( + "testing" + + v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" + "github.com/stretchr/testify/require" +) + +func TestNetMapNodes(t *testing.T) { + var nm netmap.NetMap + + require.Empty(t, nm.Nodes()) + + nodes := []netmap.NodeInfo{netmaptest.NodeInfo(), netmaptest.NodeInfo()} + + nm.SetNodes(nodes) + require.ElementsMatch(t, nodes, nm.Nodes()) + + nodesV2 := make([]v2netmap.NodeInfo, len(nodes)) + for i := range nodes { + nodes[i].WriteToV2(&nodesV2[i]) + } + + var m v2netmap.NetMap + nm.WriteToV2(&m) + + require.ElementsMatch(t, nodesV2, m.Nodes()) +} + +func TestNetMap_SetEpoch(t *testing.T) { + var nm netmap.NetMap + + require.Zero(t, nm.Epoch()) + + const e = 158 + + nm.SetEpoch(e) + require.EqualValues(t, e, nm.Epoch()) + + var m v2netmap.NetMap + nm.WriteToV2(&m) + + require.EqualValues(t, e, m.Epoch()) +} diff --git a/netmap/test/generate.go b/netmap/test/generate.go index 17956d5d..45ab0e1b 100644 --- a/netmap/test/generate.go +++ b/netmap/test/generate.go @@ -1,6 +1,8 @@ package netmaptest import ( + "math/rand" + "github.com/nspcc-dev/neofs-sdk-go/netmap" subnetidtest "github.com/nspcc-dev/neofs-sdk-go/subnet/id/test" ) @@ -68,3 +70,14 @@ func NetworkInfo() (x netmap.NetworkInfo) { return } + +// NodeInfo returns random netmap.NodeInfo. +func NodeInfo() (x netmap.NodeInfo) { + key := make([]byte, 33) + rand.Read(key) + + x.SetPublicKey(key) + x.SetNetworkEndpoints("1", "2", "3") + + return +}