Testing and polishing connection graph

Added go tests
Refactored cluster connect graph to new file
Refactored dot file printing to new repo
Fixed code climate issues
Added sharness test

License: MIT
Signed-off-by: Wyatt Daviau <wdaviau@cs.stanford.edu>
This commit is contained in:
Wyatt Daviau 2018-01-17 21:49:35 -05:00
parent e712c87570
commit d2ef32f48f
21 changed files with 828 additions and 318 deletions

View File

@ -164,10 +164,10 @@ func (c *Client) Version() (api.Version, error) {
return ver, err return ver, err
} }
// GetConnectionGraph returns an ipfs-cluster connection graph. // GetConnectGraph returns an ipfs-cluster connection graph.
// The serialized version, strings instead of pids, is returned // The serialized version, strings instead of pids, is returned
func (c *Client) GetConnectGraph() (api.ConnectGraphSerial, error) { func (c *Client) GetConnectGraph() (api.ConnectGraphSerial, error) {
var graphS api.ConnectGraphSerial var graphS api.ConnectGraphSerial
err := c.do("GET", "/graph", nil, &graphS) err := c.do("GET", "/health/graph", nil, &graphS)
return graphS, err return graphS, err
} }

View File

@ -210,3 +210,17 @@ func TestRecoverAll(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestGetConnectGraph(t *testing.T) {
c, api := testClient(t)
defer api.Shutdown()
cg, err := c.GetConnectGraph()
if err != nil {
t.Fatal(err)
}
if len(cg.IPFSLinks) != 3 || len(cg.ClusterLinks) != 3 ||
len(cg.ClustertoIPFS) != 3 {
t.Fatal("Bad graph")
}
}

View File

@ -263,7 +263,7 @@ func (api *API) routes() []route {
{ {
"ConnectionGraph", "ConnectionGraph",
"GET", "GET",
"/graph", "/health/graph",
api.graphHandler, api.graphHandler,
}, },
} }
@ -335,9 +335,9 @@ func (api *API) versionHandler(w http.ResponseWriter, r *http.Request) {
sendResponse(w, err, v) sendResponse(w, err, v)
} }
func (rest *API) graphHandler(w http.ResponseWriter, r *http.Request) { func (api *API) graphHandler(w http.ResponseWriter, r *http.Request) {
var graph types.ConnectGraphSerial var graph types.ConnectGraphSerial
err := rest.rpcClient.Call("", err := api.rpcClient.Call("",
"Cluster", "Cluster",
"ConnectGraph", "ConnectGraph",
struct{}{}, struct{}{},

View File

@ -155,6 +155,34 @@ func TestAPIPeerRemoveEndpoint(t *testing.T) {
makeDelete(t, "/peers/"+test.TestPeerID1.Pretty(), &struct{}{}) makeDelete(t, "/peers/"+test.TestPeerID1.Pretty(), &struct{}{})
} }
func TestConnectGraphEndpoint(t *testing.T) {
rest := testAPI(t)
defer rest.Shutdown()
var cg api.ConnectGraphSerial
makeGet(t, "/health/graph", &cg)
if cg.ClusterID != test.TestPeerID1.Pretty() {
t.Error("unexpected cluster id")
}
if len(cg.IPFSLinks) != 3 {
t.Error("unexpected number of ipfs peers")
}
if len(cg.ClusterLinks) != 3 {
t.Error("unexpected number of cluster peers")
}
if len(cg.ClustertoIPFS) != 3 {
t.Error("unexpected number of cluster to ipfs links")
}
// test a few link values
pid1 := test.TestPeerID1.Pretty()
pid4 := test.TestPeerID4.Pretty()
if _, ok := cg.ClustertoIPFS[pid1]; !ok {
t.Fatal("missing cluster peer 1 from cluster to peer links map")
}
if cg.ClustertoIPFS[pid1] != pid4 {
t.Error("unexpected ipfs peer mapped to cluster peer 1 in graph")
}
}
func TestAPIPinEndpoint(t *testing.T) { func TestAPIPinEndpoint(t *testing.T) {
rest := testAPI(t) rest := testAPI(t)
defer rest.Shutdown() defer rest.Shutdown()

View File

@ -295,8 +295,8 @@ type ConnectGraphSerial struct {
// ToSerial converts a ConnectGraph to its Go-serializable version // ToSerial converts a ConnectGraph to its Go-serializable version
func (cg ConnectGraph) ToSerial() ConnectGraphSerial { func (cg ConnectGraph) ToSerial() ConnectGraphSerial {
IPFSLinksSerial := SerializeLinkMap(cg.IPFSLinks) IPFSLinksSerial := serializeLinkMap(cg.IPFSLinks)
ClusterLinksSerial := SerializeLinkMap(cg.ClusterLinks) ClusterLinksSerial := serializeLinkMap(cg.ClusterLinks)
ClustertoIPFSSerial := make(map[string]string) ClustertoIPFSSerial := make(map[string]string)
for k, v := range cg.ClustertoIPFS { for k, v := range cg.ClustertoIPFS {
ClustertoIPFSSerial[peer.IDB58Encode(k)] = peer.IDB58Encode(v) ClustertoIPFSSerial[peer.IDB58Encode(k)] = peer.IDB58Encode(v)
@ -309,7 +309,24 @@ func (cg ConnectGraph) ToSerial() ConnectGraphSerial {
} }
} }
func SerializeLinkMap(Links map[peer.ID][]peer.ID) map[string][]string { // ToConnectGraph converts a ConnectGraphSerial to a ConnectGraph
func (cgs ConnectGraphSerial) ToConnectGraph() ConnectGraph {
ClustertoIPFS := make(map[peer.ID]peer.ID)
for k, v := range cgs.ClustertoIPFS {
pid1, _ := peer.IDB58Decode(k)
pid2, _ := peer.IDB58Decode(v)
ClustertoIPFS[pid1] = pid2
}
pid, _ := peer.IDB58Decode(cgs.ClusterID)
return ConnectGraph{
ClusterID: pid,
IPFSLinks: deserializeLinkMap(cgs.IPFSLinks),
ClusterLinks: deserializeLinkMap(cgs.ClusterLinks),
ClustertoIPFS: ClustertoIPFS,
}
}
func serializeLinkMap(Links map[peer.ID][]peer.ID) map[string][]string {
LinksSerial := make(map[string][]string) LinksSerial := make(map[string][]string)
for k, v := range Links { for k, v := range Links {
kS := peer.IDB58Encode(k) kS := peer.IDB58Encode(k)
@ -318,28 +335,29 @@ func SerializeLinkMap(Links map[peer.ID][]peer.ID) map[string][]string {
return LinksSerial return LinksSerial
} }
// SwarmPeers lists an ipfs daemon's peers func deserializeLinkMap(LinksSerial map[string][]string) map[peer.ID][]peer.ID {
type SwarmPeers struct { Links := make(map[peer.ID][]peer.ID)
Peers []peer.ID for k, v := range LinksSerial {
pid, _ := peer.IDB58Decode(k)
Links[pid] = StringsToPeers(v)
}
return Links
} }
// SwarmPeers lists an ipfs daemon's peers
type SwarmPeers []peer.ID
// SwarmPeersSerial is the serialized form of SwarmPeers for RPC use // SwarmPeersSerial is the serialized form of SwarmPeers for RPC use
type SwarmPeersSerial struct { type SwarmPeersSerial []string
Peers []string `json:"peers"`
}
// ToSerial converts SwarmPeers to its Go-serializeable version // ToSerial converts SwarmPeers to its Go-serializeable version
func (swarm SwarmPeers) ToSerial() SwarmPeersSerial { func (swarm SwarmPeers) ToSerial() SwarmPeersSerial {
return SwarmPeersSerial{ return PeersToStrings(swarm)
Peers: PeersToStrings(swarm.Peers),
}
} }
// ToSwarmPeers converts a SwarmPeersSerial object to SwarmPeers. // ToSwarmPeers converts a SwarmPeersSerial object to SwarmPeers.
func (swarmS SwarmPeersSerial) ToSwarmPeers() SwarmPeers { func (swarmS SwarmPeersSerial) ToSwarmPeers() SwarmPeers {
return SwarmPeers{ return StringsToPeers(swarmS)
Peers: StringsToPeers(swarmS.Peers),
}
} }
// ID holds information about the Cluster peer // ID holds information about the Cluster peer

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"reflect"
"testing" "testing"
"time" "time"
@ -16,6 +17,10 @@ var testMAddr3, _ = ma.NewMultiaddr("/ip4/127.0.0.1/tcp/8081/ws/ipfs/QmSoLer265N
var testCid1, _ = cid.Decode("QmP63DkAFEnDYNjDYBpyNDfttu1fvUw99x1brscPzpqmmq") var testCid1, _ = cid.Decode("QmP63DkAFEnDYNjDYBpyNDfttu1fvUw99x1brscPzpqmmq")
var testPeerID1, _ = peer.IDB58Decode("QmXZrtE5jQwXNqCJMfHUTQkvhQ4ZAnqMnmzFMJfLewuabc") var testPeerID1, _ = peer.IDB58Decode("QmXZrtE5jQwXNqCJMfHUTQkvhQ4ZAnqMnmzFMJfLewuabc")
var testPeerID2, _ = peer.IDB58Decode("QmXZrtE5jQwXNqCJMfHUTQkvhQ4ZAnqMnmzFMJfLewuabd") var testPeerID2, _ = peer.IDB58Decode("QmXZrtE5jQwXNqCJMfHUTQkvhQ4ZAnqMnmzFMJfLewuabd")
var testPeerID3, _ = peer.IDB58Decode("QmPGDFvBkgWhvzEK9qaTWrWurSwqXNmhnK3hgELPdZZNPa")
var testPeerID4, _ = peer.IDB58Decode("QmZ8naDy5mEz4GLuQwjWt9MPYqHTBbsm8tQBrNSjiq6zBc")
var testPeerID5, _ = peer.IDB58Decode("QmZVAo3wd8s5eTTy2kPYs34J9PvfxpKPuYsePPYGjgRRjg")
var testPeerID6, _ = peer.IDB58Decode("QmR8Vu6kZk7JvAN2rWVWgiduHatgBq2bb15Yyq8RRhYSbx")
func TestTrackerFromString(t *testing.T) { func TestTrackerFromString(t *testing.T) {
testcases := []string{"bug", "cluster_error", "pin_error", "unpin_error", "pinned", "pinning", "unpinning", "unpinned", "remote"} testcases := []string{"bug", "cluster_error", "pin_error", "unpin_error", "pinned", "pinning", "unpinning", "unpinned", "remote"}
@ -127,6 +132,37 @@ func TestIDConv(t *testing.T) {
} }
} }
func TestConnectGraphConv(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatal("paniced")
}
}()
cg := ConnectGraph{
ClusterID: testPeerID1,
IPFSLinks: map[peer.ID][]peer.ID{
testPeerID4: []peer.ID{testPeerID5, testPeerID6},
testPeerID5: []peer.ID{testPeerID4, testPeerID6},
testPeerID6: []peer.ID{testPeerID4, testPeerID5},
},
ClusterLinks: map[peer.ID][]peer.ID{
testPeerID1: []peer.ID{testPeerID2, testPeerID3},
testPeerID2: []peer.ID{testPeerID1, testPeerID3},
testPeerID3: []peer.ID{testPeerID1, testPeerID2},
},
ClustertoIPFS: map[peer.ID]peer.ID{
testPeerID1: testPeerID4,
testPeerID2: testPeerID5,
testPeerID3: testPeerID6,
},
}
cgNew := cg.ToSerial().ToConnectGraph()
if !reflect.DeepEqual(cg, cgNew) {
t.Fatal("The new connect graph should be equivalent to the old")
}
}
func TestMultiaddrConv(t *testing.T) { func TestMultiaddrConv(t *testing.T) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {

View File

@ -1051,83 +1051,6 @@ func (c *Cluster) Peers() []api.ID {
return peers return peers
} }
// ConnectGraph returns a description of which cluster peers and ipfs
// daemons are connected to each other
func (c *Cluster) ConnectGraph() (api.ConnectGraph, error) {
ipfsLinks := make(map[peer.ID][]peer.ID)
clusterLinks := make(map[peer.ID][]peer.ID)
clusterToIpfs := make(map[peer.ID]peer.ID)
members, err := c.consensus.Peers()
if err != nil {
return api.ConnectGraph{}, err
}
for _, p := range members {
// Cluster connections
clusterLinks[p] = make([]peer.ID, 0)
var sPeers []api.IDSerial
var pId api.ID
err = c.rpcClient.Call(p,
"Cluster",
"Peers",
struct{}{},
&sPeers,
)
if err != nil { // Only setting cluster connections when no error occurs
logger.Debugf("RPC error reaching cluster peer %s: %s", p.Pretty(), err.Error())
continue
}
selfConnection := false
for _, sId := range sPeers {
id := sId.ToID()
if id.Error != "" {
logger.Debugf("Peer %s errored connecting to its peer %s", p.Pretty(), id.ID.Pretty())
continue
}
if id.ID == p {
selfConnection = true
pId = id
} else {
clusterLinks[p] = append(clusterLinks[p], id.ID)
}
}
// IPFS connections
if !selfConnection {
logger.Debugf("cluster peer %s not its own peer. No ipfs info ", p.Pretty())
continue
}
ipfsId := pId.IPFS.ID
if pId.IPFS.Error != "" { // Only setting ipfs connections when no error occurs
logger.Debugf("ipfs id: %s has error: %s", ipfsId.Pretty(), pId.IPFS.Error)
continue
}
clusterToIpfs[p] = ipfsId
ipfsLinks[ipfsId] = make([]peer.ID, 0)
var swarmPeersS api.SwarmPeersSerial
err = c.rpcClient.Call(p,
"Cluster",
"IPFSSwarmPeers",
struct{}{},
&swarmPeersS,
)
if err != nil {
continue
}
swarmPeers := swarmPeersS.ToSwarmPeers()
ipfsLinks[ipfsId] = swarmPeers.Peers
}
return api.ConnectGraph{
ClusterID: c.id,
IPFSLinks: ipfsLinks,
ClusterLinks: clusterLinks,
ClustertoIPFS: clusterToIpfs,
}, nil
}
// makeHost makes a libp2p-host. // makeHost makes a libp2p-host.
func makeHost(ctx context.Context, cfg *Config) (host.Host, error) { func makeHost(ctx context.Context, cfg *Config) (host.Host, error) {
ps := peerstore.NewPeerstore() ps := peerstore.NewPeerstore()

View File

@ -17,6 +17,7 @@ import (
rpc "github.com/hsanjuan/go-libp2p-gorpc" rpc "github.com/hsanjuan/go-libp2p-gorpc"
cid "github.com/ipfs/go-cid" cid "github.com/ipfs/go-cid"
peer "github.com/libp2p/go-libp2p-peer"
) )
type mockComponent struct { type mockComponent struct {
@ -79,6 +80,10 @@ func (ipfs *mockConnector) PinLs(filter string) (map[string]api.IPFSPinStatus, e
return m, nil return m, nil
} }
func (ipfs *mockConnector) SwarmPeers() (api.SwarmPeers, error) {
return []peer.ID{test.TestPeerID4, test.TestPeerID5}, nil
}
func (ipfs *mockConnector) ConnectSwarms() error { return nil } func (ipfs *mockConnector) ConnectSwarms() error { return nil }
func (ipfs *mockConnector) ConfigKey(keypath string) (interface{}, error) { return nil, nil } func (ipfs *mockConnector) ConfigKey(keypath string) (interface{}, error) { return nil, nil }
func (ipfs *mockConnector) FreeSpace() (uint64, error) { return 100, nil } func (ipfs *mockConnector) FreeSpace() (uint64, error) { return 100, nil }

89
connect_graph.go Normal file
View File

@ -0,0 +1,89 @@
package ipfscluster
import (
peer "github.com/libp2p/go-libp2p-peer"
"github.com/ipfs/ipfs-cluster/api"
)
// ConnectGraph returns a description of which cluster peers and ipfs
// daemons are connected to each other
func (c *Cluster) ConnectGraph() (api.ConnectGraph, error) {
cg := api.ConnectGraph{
IPFSLinks: make(map[peer.ID][]peer.ID),
ClusterLinks: make(map[peer.ID][]peer.ID),
ClustertoIPFS: make(map[peer.ID]peer.ID),
}
members, err := c.consensus.Peers()
if err != nil {
return cg, err
}
peersSerials := make([][]api.IDSerial, len(members), len(members))
errs := c.multiRPC(members, "Cluster", "Peers", struct{}{},
copyIDSerialSliceToIfaces(peersSerials))
for i, err := range errs {
p := members[i]
cg.ClusterLinks[p] = make([]peer.ID, 0)
if err != nil { // Only setting cluster connections when no error occurs
logger.Debugf("RPC error reaching cluster peer %s: %s", p.Pretty(), err.Error())
continue
}
selfConnection, pID := c.recordClusterLinks(&cg, p, peersSerials[i])
// IPFS connections
if !selfConnection {
logger.Warningf("cluster peer %s not its own peer. No ipfs info ", p.Pretty())
continue
}
c.recordIPFSLinks(&cg, pID)
}
return cg, nil
}
func (c *Cluster) recordClusterLinks(cg *api.ConnectGraph, p peer.ID, sPeers []api.IDSerial) (bool, api.ID) {
selfConnection := false
var pID api.ID
for _, sID := range sPeers {
id := sID.ToID()
if id.Error != "" {
logger.Debugf("Peer %s errored connecting to its peer %s", p.Pretty(), id.ID.Pretty())
continue
}
if id.ID == p {
selfConnection = true
pID = id
} else {
cg.ClusterLinks[p] = append(cg.ClusterLinks[p], id.ID)
}
}
return selfConnection, pID
}
func (c *Cluster) recordIPFSLinks(cg *api.ConnectGraph, pID api.ID) {
ipfsID := pID.IPFS.ID
if pID.IPFS.Error != "" { // Only setting ipfs connections when no error occurs
logger.Warningf("ipfs id: %s has error: %s. Skipping swarm connections", ipfsID.Pretty(), pID.IPFS.Error)
return
}
if _, ok := cg.IPFSLinks[pID.ID]; ok {
logger.Warningf("ipfs id: %s already recorded, one ipfs daemon in use by multiple cluster peers", ipfsID.Pretty())
}
cg.ClustertoIPFS[pID.ID] = ipfsID
cg.IPFSLinks[ipfsID] = make([]peer.ID, 0)
var swarmPeersS api.SwarmPeersSerial
err := c.rpcClient.Call(pID.ID,
"Cluster",
"IPFSSwarmPeers",
struct{}{},
&swarmPeersS,
)
if err != nil {
return
}
swarmPeers := swarmPeersS.ToSwarmPeers()
cg.IPFSLinks[ipfsID] = swarmPeers
}

View File

@ -2,17 +2,37 @@ package main
import ( import (
"errors" "errors"
"io"
"fmt" "fmt"
"io"
"sort"
peer "github.com/libp2p/go-libp2p-peer"
dot "github.com/zenground0/go-dot"
"github.com/ipfs/ipfs-cluster/api" "github.com/ipfs/ipfs-cluster/api"
) )
/*
These functions are used to write an IPFS Cluster connectivity graph to a
graphviz-style dot file. Input an api.ConnectGraphSerial object, makeDot
does some preprocessing and then passes all 3 link maps to a
cluster-dotWriter which handles iterating over the link maps and writing
dot file node and edge statements to make a dot-file graph. Nodes are
labeled with the go-libp2p-peer shortened peer id. IPFS nodes are rendered
with gold boundaries, Cluster nodes with blue. Currently preprocessing
consists of moving IPFS swarm peers not connected to any cluster peer to
the IPFSLinks map in the event that the function was invoked with the
allIpfs flag. This allows all IPFS peers connected to the cluster to be
rendered as nodes in the final graph.
*/
// nodeType specifies the type of node being represented in the dot file:
// either IPFS or Cluster
type nodeType int type nodeType int
const ( const (
tCluster nodeType = iota tCluster nodeType = iota // The cluster node type
tIpfs tIpfs // The IPFS node type
) )
var errUnfinishedWrite = errors.New("could not complete write of line to output") var errUnfinishedWrite = errors.New("could not complete write of line to output")
@ -39,188 +59,113 @@ func makeDot(cg api.ConnectGraphSerial, w io.Writer, allIpfs bool) error {
dW := dotWriter{ dW := dotWriter{
w: w, w: w,
dotGraph: dot.NewGraph("cluster"),
ipfsEdges: ipfsEdges, ipfsEdges: ipfsEdges,
clusterEdges: cg.ClusterLinks, clusterEdges: cg.ClusterLinks,
clusterIpfsEdges: cg.ClustertoIPFS, clusterIpfsEdges: cg.ClustertoIPFS,
clusterOrder: make(map[string]int, 0), clusterNodes: make(map[string]*dot.VertexDescription),
ipfsOrder: make(map[string]int, 0), ipfsNodes: make(map[string]*dot.VertexDescription),
} }
return dW.print() return dW.print()
} }
type dotWriter struct { type dotWriter struct {
clusterOrder map[string]int clusterNodes map[string]*dot.VertexDescription
ipfsOrder map[string]int ipfsNodes map[string]*dot.VertexDescription
w io.Writer w io.Writer
dotGraph dot.DotGraph
ipfsEdges map[string][]string ipfsEdges map[string][]string
clusterEdges map[string][]string clusterEdges map[string][]string
clusterIpfsEdges map[string]string clusterIpfsEdges map[string]string
}
func (dW dotWriter) writeComment(comment string) error {
final := fmt.Sprintf("/* %s */\n", comment)
n, err := io.WriteString(dW.w, final)
if err == nil && n != len([]byte(final)) {
err = errUnfinishedWrite
}
return err
}
// precondition: id has already been processed and id's ordering
// has been recorded by dW
func (dW dotWriter) getString(id string, idT nodeType) (string, error) {
switch idT {
case tCluster:
number, ok := dW.clusterOrder[id]
if !ok {
return "", errCorruptOrdering
}
return fmt.Sprintf("C%d", number), nil
case tIpfs:
number, ok := dW.ipfsOrder[id]
if !ok {
return "", errCorruptOrdering
}
return fmt.Sprintf("I%d", number), nil
default:
return "", errUnknownNodeType
}
return "", nil
}
func (dW dotWriter) writeEdge(from string, fT nodeType, to string, tT nodeType) error{
fromStr, err := dW.getString(from, fT)
if err != nil {
return err
}
toStr, err := dW.getString(to, tT)
if err != nil {
return err
}
edgeStr := fmt.Sprintf("%s -> %s\n", fromStr, toStr)
n, err := io.WriteString(dW.w, edgeStr)
if err == nil && n != len([]byte(edgeStr)) {
err = errUnfinishedWrite
}
return err
} }
// writes nodes to dot file output and creates and stores an ordering over nodes // writes nodes to dot file output and creates and stores an ordering over nodes
func (dW *dotWriter) writeNode(id string, nT nodeType) error { func (dW *dotWriter) addNode(id string, nT nodeType) error {
var nodeStr string var node dot.VertexDescription
node.Label = shortID(id)
switch nT { switch nT {
case tCluster: case tCluster:
nodeStr = fmt.Sprintf("C%d [label=\"%s\" color=\"blue2\"]\n", len(dW.clusterOrder), shortId(id)) node.ID = fmt.Sprintf("C%d", len(dW.clusterNodes))
dW.clusterOrder[id] = len(dW.clusterOrder) node.Color = "blue2"
dW.clusterNodes[id] = &node
case tIpfs: case tIpfs:
nodeStr = fmt.Sprintf("I%d [label=\"%s\" color=\"goldenrod\"]\n", len(dW.ipfsOrder), shortId(id)) node.ID = fmt.Sprintf("I%d", len(dW.ipfsNodes))
dW.ipfsOrder[id] = len(dW.ipfsOrder) node.Color = "goldenrod"
dW.ipfsNodes[id] = &node
default: default:
return errUnknownNodeType return errUnknownNodeType
} }
n, err := io.WriteString(dW.w, nodeStr) dW.dotGraph.AddVertex(&node)
if err == nil && n != len([]byte(nodeStr)) {
err = errUnfinishedWrite return nil
}
return err
} }
func (dW *dotWriter) print() error { func (dW *dotWriter) print() error {
_, err := io.WriteString(dW.w, "digraph cluster {\n\n") dW.dotGraph.AddComment("The nodes of the connectivity graph")
err = dW.writeComment("The nodes of the connectivity graph") dW.dotGraph.AddComment("The cluster-service peers")
if err != nil { // Write cluster nodes, use sorted order for consistent labels
return err for _, k := range sortedKeys(dW.clusterEdges) {
} err := dW.addNode(k, tCluster)
err = dW.writeComment("The cluster-service peers")
if err != nil {
return err
}
// Write cluster nodes
for k := range dW.clusterEdges {
err = dW.writeNode(k, tCluster)
if err != nil { if err != nil {
return err return err
} }
} }
_, err = io.WriteString(dW.w, "\n") dW.dotGraph.AddNewLine()
if err != nil {
return err
}
err = dW.writeComment("The ipfs peers") dW.dotGraph.AddComment("The ipfs peers")
if err != nil { // Write ipfs nodes, use sorted order for consistent labels
return err for _, k := range sortedKeys(dW.ipfsEdges) {
} err := dW.addNode(k, tIpfs)
// Write ipfs nodes
for k := range dW.ipfsEdges {
err = dW.writeNode(k, tIpfs)
if err != nil { if err != nil {
return err return err
} }
} }
_, err = io.WriteString(dW.w, "\n") dW.dotGraph.AddNewLine()
if err != nil {
return err
}
err = dW.writeComment("Edges representing active connections in the cluster") dW.dotGraph.AddComment("Edges representing active connections in the cluster")
if err != nil { dW.dotGraph.AddComment("The connections among cluster-service peers")
return err
}
err = dW.writeComment("The connections among cluster-service peers")
// Write cluster edges // Write cluster edges
for k, v := range dW.clusterEdges { for k, v := range dW.clusterEdges {
for _, id := range v { for _, id := range v {
err = dW.writeEdge(k,tCluster,id,tCluster) toNode := dW.clusterNodes[k]
if err != nil { fromNode := dW.clusterNodes[id]
return err dW.dotGraph.AddEdge(toNode, fromNode, true)
} }
} }
} dW.dotGraph.AddNewLine()
_, err = io.WriteString(dW.w, "\n")
if err != nil {
return err
}
err = dW.writeComment("The connections between cluster peers and their ipfs daemons") dW.dotGraph.AddComment("The connections between cluster peers and their ipfs daemons")
if err != nil {
return err
}
// Write cluster to ipfs edges // Write cluster to ipfs edges
for k, id := range dW.clusterIpfsEdges { for k, id := range dW.clusterIpfsEdges {
err = dW.writeEdge(k,tCluster,id,tIpfs) toNode := dW.clusterNodes[k]
if err != nil { fromNode := dW.ipfsNodes[id]
return err dW.dotGraph.AddEdge(toNode, fromNode, true)
}
}
_, err = io.WriteString(dW.w, "\n")
if err != nil {
return err
} }
dW.dotGraph.AddNewLine()
err = dW.writeComment("The swarm peer connections among ipfs daemons in the cluster") dW.dotGraph.AddComment("The swarm peer connections among ipfs daemons in the cluster")
if err != nil {
return err
}
// Write ipfs edges // Write ipfs edges
for k, v := range dW.ipfsEdges { for k, v := range dW.ipfsEdges {
for _, id := range v { for _, id := range v {
err = dW.writeEdge(k,tIpfs,id,tIpfs) toNode := dW.ipfsNodes[k]
if err != nil { fromNode := dW.ipfsNodes[id]
return err dW.dotGraph.AddEdge(toNode, fromNode, true)
} }
} }
} return dW.dotGraph.WriteDot(dW.w)
_, err = io.WriteString(dW.w, "\n }")
if err != nil {
return err
} }
return nil func sortedKeys(dict map[string][]string) []string {
keys := make([]string, len(dict), len(dict))
i := 0
for k := range dict {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
} }
// truncate the provided peer id string to the 3 last characters. Odds of // truncate the provided peer id string to the 3 last characters. Odds of
@ -228,11 +173,16 @@ func (dW *dotWriter) print() error {
// the chances of a collision are still less than 1 in 100 (birthday paradox). // the chances of a collision are still less than 1 in 100 (birthday paradox).
// As clusters grow bigger than this we can provide a flag for including // As clusters grow bigger than this we can provide a flag for including
// more characters. // more characters.
func shortId(peerString string) string { func shortID(peerString string) string {
if len(peerString) < 3 { pid, err := peer.IDB58Decode(peerString)
return peerString if err != nil {
// We'll truncate ourselves
// Should never get here but try to match with peer:String()
if len(peerString) < 6 {
return fmt.Sprintf("<peer.ID %s>", peerString)
} }
start := len(peerString) - 3 return fmt.Sprintf("<peer.ID %s>", peerString[:6])
end := len(peerString) }
return peerString[start:end] return pid.String()
} }

View File

@ -0,0 +1,217 @@
package main
import (
"bytes"
"fmt"
"sort"
"strings"
"testing"
"github.com/ipfs/ipfs-cluster/api"
)
func verifyOutput(t *testing.T, outStr string, trueStr string) {
// Because values are printed in the order of map keys we cannot enforce
// an exact ordering. Instead we split both strings and compare line by
// line.
outLines := strings.Split(outStr, "\n")
trueLines := strings.Split(trueStr, "\n")
sort.Strings(outLines)
sort.Strings(trueLines)
if len(outLines) != len(trueLines) {
fmt.Printf("expected: %s\n actual: %s", trueStr, outStr)
t.Fatal("Number of output lines does not match expectation")
}
for i := range outLines {
if outLines[i] != trueLines[i] {
t.Errorf("Difference in sorted outputs: %s vs %s", outLines[i], trueLines[i])
}
}
}
var simpleIpfs = `digraph cluster {
/* The nodes of the connectivity graph */
/* The cluster-service peers */
C0 [label="<peer.ID UBuxVH>" color="blue2"]
C1 [label="<peer.ID V35Ljb>" color="blue2"]
C2 [label="<peer.ID Z2ckU7>" color="blue2"]
/* The ipfs peers */
I0 [label="<peer.ID PFKAGZ>" color="goldenrod"]
I1 [label="<peer.ID XbiVZd>" color="goldenrod"]
I2 [label="<peer.ID bU7273>" color="goldenrod"]
/* Edges representing active connections in the cluster */
/* The connections among cluster-service peers */
C0 -> C1
C0 -> C2
C1 -> C0
C1 -> C2
C2 -> C0
C2 -> C1
/* The connections between cluster peers and their ipfs daemons */
C0 -> I1
C1 -> I0
C2 -> I2
/* The swarm peer connections among ipfs daemons in the cluster */
I0 -> I1
I0 -> I2
I1 -> I0
I1 -> I2
I2 -> I0
I2 -> I1
}`
func TestSimpleIpfsGraphs(t *testing.T) {
cg := api.ConnectGraphSerial{
ClusterID: "QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD",
ClusterLinks: map[string][]string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD": []string{
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ",
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu",
},
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ": []string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD",
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu",
},
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu": []string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD",
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ",
},
},
IPFSLinks: map[string][]string{
"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV": []string{
"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq",
"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL",
},
"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq": []string{
"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV",
"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL",
},
"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL": []string{
"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV",
"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq",
},
},
ClustertoIPFS: map[string]string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD":"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV",
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ":"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq",
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu":"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL",
},
}
buf := new(bytes.Buffer)
err := makeDot(cg, buf, false)
if err != nil {
t.Fatal(err)
}
verifyOutput(t, buf.String(), simpleIpfs)
}
var allIpfs = `digraph cluster {
/* The nodes of the connectivity graph */
/* The cluster-service peers */
C0 [label="<peer.ID UBuxVH>" color="blue2"]
C1 [label="<peer.ID V35Ljb>" color="blue2"]
C2 [label="<peer.ID Z2ckU7>" color="blue2"]
/* The ipfs peers */
I0 [label="<peer.ID PFKAGZ>" color="goldenrod"]
I1 [label="<peer.ID VV2enw>" color="goldenrod"]
I2 [label="<peer.ID XbiVZd>" color="goldenrod"]
I3 [label="<peer.ID bU7273>" color="goldenrod"]
I4 [label="<peer.ID Qmp43V>" color="goldenrod"]
I5 [label="<peer.ID QmqRmE>" color="goldenrod"]
/* Edges representing active connections in the cluster */
/* The connections among cluster-service peers */
C2 -> C0
C2 -> C1
C0 -> C1
C0 -> C2
C1 -> C0
C1 -> C2
/* The connections between cluster peers and their ipfs daemons */
C0 -> I2
C1 -> I0
C2 -> I3
/* The swarm peer connections among ipfs daemons in the cluster */
I0 -> I1
I0 -> I2
I0 -> I3
I0 -> I4
I0 -> I5
I2 -> I0
I2 -> I1
I2 -> I3
I2 -> I4
I2 -> I5
I3 -> I0
I3 -> I1
I3 -> I2
I3 -> I4
I3 -> I5
}`
func TestIpfsAllGraphs(t *testing.T) {
cg := api.ConnectGraphSerial{
ClusterID: "QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD",
ClusterLinks: map[string][]string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD": []string{
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ",
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu",
},
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ": []string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD",
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu",
},
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu": []string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD",
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ",
},
},
IPFSLinks: map[string][]string{
"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV": []string{
"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq",
"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL",
"QmqRmEm2rp5Ppqy9JwGiFLiu9TAm21F2y9fu8aaaaaaDBq",
"QmVV2enwXqqQf5esx4v36UeaFQvFehSPzNfi8aaaaaanM8",
"Qmp43VV2enwXp43VV2enwXNjfmNpTaff774yyQuu99akzp",
},
"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq": []string{
"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV",
"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL",
"QmqRmEm2rp5Ppqy9JwGiFLiu9TAm21F2y9fu8aaaaaaDBq",
"QmVV2enwXqqQf5esx4v36UeaFQvFehSPzNfi8aaaaaanM8",
"Qmp43VV2enwXp43VV2enwXNjfmNpTaff774yyQuu99akzp",
},
"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL": []string{
"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV",
"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq",
"QmqRmEm2rp5Ppqy9JwGiFLiu9TAm21F2y9fu8aaaaaaDBq",
"QmVV2enwXqqQf5esx4v36UeaFQvFehSPzNfi8aaaaaanM8",
"Qmp43VV2enwXp43VV2enwXNjfmNpTaff774yyQuu99akzp",
},
},
ClustertoIPFS: map[string]string{
"QmUBuxVHoNNjfmNpTad36UeaFQv3gXAtCv9r6KhmeqhEhD":"QmXbiVZd93SLiu9TAm21F2y9JwGiFLydbEVkPBaMR3DZDV",
"QmV35LjbEGPfN7KfMAJp43VV2enwXqqQf5esx4vUcgHDQJ":"QmPFKAGZbUjdzt8BBx8VTWCe9UeUQVcoqHFehSPzN5LSsq",
"QmZ2ckU7G35MYyJgMTwMUnicsGqSy3YUxGBX7qny6MQmJu":"QmbU7273zH6jxwDe2nqRmEm2rp5PpqP2xeQr2xCmwbBsuL",
},
}
buf := new(bytes.Buffer)
err := makeDot(cg, buf, true)
if err != nil {
t.Fatal(err)
}
verifyOutput(t, buf.String(), allIpfs)
}

View File

@ -225,46 +225,6 @@ cluster peers.
return nil return nil
}, },
}, },
{
Name: "graph",
Usage: "display connectivity of cluster nodes",
Description: `
This command queries all connected cluster peers and their ipfs nodes to generate a
graph of the connections. Output is a dot file encoding the cluster's connection state
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "file, f",
Value: "",
Usage: "sets an output dot-file for the connectivity graph",
},
cli.BoolFlag{
Name: "all-ipfs-peers",
Usage: "causes the graph to mark nodes for ipfs peers not directly in the cluster",
},
},
Action: func(c *cli.Context) error {
resp, cerr := globalClient.GetConnectGraph()
if cerr != nil {
formatResponse(c, resp, cerr)
return nil
}
var w io.WriteCloser
var err error
outputPath := c.String("file")
if outputPath == "" {
w = os.Stdout
} else {
w, err = os.Create(outputPath)
checkErr("creating output file", err)
}
defer w.Close()
err = makeDot(resp, w, c.Bool("all-ipfs-peers"))
checkErr("printing graph", err)
return nil
},
},
}, },
}, },
{ {
@ -471,6 +431,7 @@ operations on the contacted peer (as opposed to on every peer).
return nil return nil
}, },
}, },
{ {
Name: "version", Name: "version",
Usage: "Retrieve cluster version", Usage: "Retrieve cluster version",
@ -486,6 +447,52 @@ to check that it matches the CLI version (shown by -v).
return nil return nil
}, },
}, },
{
Name: "health",
Description: "Display information on clusterhealth",
Subcommands: []cli.Command{
{
Name: "graph",
Usage: "display connectivity of cluster nodes",
Description: `
This command queries all connected cluster peers and their ipfs nodes to generate a
graph of the connections. Output is a dot file encoding the cluster's connection state.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "file, f",
Value: "",
Usage: "sets an output dot-file for the connectivity graph",
},
cli.BoolFlag{
Name: "all-ipfs-peers",
Usage: "causes the graph to mark nodes for ipfs peers not directly in the cluster",
},
},
Action: func(c *cli.Context) error {
resp, cerr := globalClient.GetConnectGraph()
if cerr != nil {
formatResponse(c, resp, cerr)
return nil
}
var w io.WriteCloser
var err error
outputPath := c.String("file")
if outputPath == "" {
w = os.Stdout
} else {
w, err = os.Create(outputPath)
checkErr("creating output file", err)
}
defer w.Close()
err = makeDot(resp, w, c.Bool("all-ipfs-peers"))
checkErr("printing graph", err)
return nil
},
},
},
},
{ {
Name: "commands", Name: "commands",
Usage: "List all commands", Usage: "List all commands",

View File

@ -1005,3 +1005,140 @@ func TestClustersRebalanceOnPeerDown(t *testing.T) {
t.Errorf("it should be pinned and is %s", s) t.Errorf("it should be pinned and is %s", s)
} }
} }
// Helper function for verifying cluster graph. Will only pass if exactly the
// peers in clusterIDs are fully connected to each other and the expected ipfs
// mock connectivity exists. Cluster peers not in clusterIDs are assumed to
// be disconnected and the graph should reflect this
func validateClusterGraph(t *testing.T, graph api.ConnectGraph, clusterIDs map[peer.ID]struct{}) {
// Check that all cluster peers see each other as peers
for id1, peers := range graph.ClusterLinks {
if _, ok := clusterIDs[id1]; !ok {
if len(peers) != 0 {
t.Errorf("disconnected peer %s is still connected in graph", id1)
}
continue
}
fmt.Printf("id: %s, peers: %v\n", id1, peers)
if len(peers) > len(clusterIDs)-1 {
t.Errorf("More peers recorded in graph than expected")
}
// Make lookup index for peers connected to id1
peerIndex := make(map[peer.ID]struct{})
for _, peer := range peers {
peerIndex[peer] = struct{}{}
}
for id2 := range clusterIDs {
if _, ok := peerIndex[id2]; id1 != id2 && !ok {
t.Errorf("Expected graph to see peer %s connected to peer %s", id1, id2)
}
}
}
if len(graph.ClusterLinks) != nClusters {
t.Errorf("Unexpected number of cluster nodes in graph")
}
// Check that all cluster peers are recorded as nodes in the graph
for id := range clusterIDs {
if _, ok := graph.ClusterLinks[id]; !ok {
t.Errorf("Expected graph to record peer %s as a node", id)
}
}
// Check that the mocked ipfs swarm is recorded
if len(graph.IPFSLinks) != 1 {
t.Error("Expected exactly one ipfs peer for all cluster nodes, the mocked peer")
}
links, ok := graph.IPFSLinks[test.TestPeerID1]
if !ok {
t.Error("Expected the mocked ipfs peer to be a node in the graph")
} else {
if len(links) != 2 || links[0] != test.TestPeerID4 ||
links[1] != test.TestPeerID5 {
t.Error("Swarm peers of mocked ipfs are not those expected")
}
}
// Check that the cluster to ipfs connections are all recorded
for id := range clusterIDs {
if ipfsID, ok := graph.ClustertoIPFS[id]; !ok {
t.Errorf("Expected graph to record peer %s's ipfs connection", id)
} else {
if ipfsID != test.TestPeerID1 {
t.Errorf("Unexpected error %s", ipfsID)
}
}
}
if len(graph.ClustertoIPFS) > len(clusterIDs) {
t.Error("More cluster to ipfs links recorded in graph than expected")
}
}
// In this test we get a cluster graph report from a random peer in a healthy
// fully connected cluster and verify that it is formed as expected.
func TestClustersGraphConnected(t *testing.T) {
clusters, mock := createClusters(t)
defer shutdownClusters(t, clusters, mock)
delay()
delay()
j := rand.Intn(nClusters) // choose a random cluster peer to query
graph, err := clusters[j].ConnectGraph()
if err != nil {
t.Fatal(err)
}
clusterIDs := make(map[peer.ID]struct{})
for _, c := range clusters {
id := c.ID().ID
clusterIDs[id] = struct{}{}
}
validateClusterGraph(t, graph, clusterIDs)
}
// Similar to the previous test we get a cluster graph report from a peer.
// However now 2 peers have been shutdown and so we do not expect to see
// them in the graph
func TestClustersGraphUnhealthy(t *testing.T) {
clusters, mock := createClusters(t)
defer shutdownClusters(t, clusters, mock)
if nClusters < 5 {
t.Skip("Need at least 5 peers")
}
j := rand.Intn(nClusters) // choose a random cluster peer to query
// chose the clusters to shutdown
discon1 := -1
discon2 := -1
for i := range clusters {
if i != j {
if discon1 == -1 {
discon1 = i
} else {
discon2 = i
break
}
}
}
clusters[discon1].Shutdown()
clusters[discon2].Shutdown()
delay()
waitForLeader(t, clusters)
delay()
graph, err := clusters[j].ConnectGraph()
if err != nil {
t.Fatal(err)
}
clusterIDs := make(map[peer.ID]struct{})
for i, c := range clusters {
if i == discon1 || i == discon2 {
continue
}
id := c.ID().ID
clusterIDs[id] = struct{}{}
}
validateClusterGraph(t, graph, clusterIDs)
}

View File

@ -97,11 +97,7 @@ type ipfsSwarmPeersResp struct {
} }
type ipfsPeer struct { type ipfsPeer struct {
Addr string
Peer string Peer string
Latency string
Muxer string
Streams []ipfsStream
} }
type ipfsStream struct { type ipfsStream struct {
@ -867,14 +863,14 @@ func (ipfs *Connector) SwarmPeers() (api.SwarmPeers, error) {
return swarm, err return swarm, err
} }
swarm.Peers = make([]peer.ID, len(peersRaw.Peers)) swarm = make([]peer.ID, len(peersRaw.Peers))
for i, p := range peersRaw.Peers { for i, p := range peersRaw.Peers {
pID, err := peer.IDB58Decode(p.Peer) pID, err := peer.IDB58Decode(p.Peer)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
return swarm, err return swarm, err
} }
swarm.Peers[i] = pID swarm[i] = pID
} }
return swarm, nil return swarm, nil
} }

View File

@ -540,6 +540,26 @@ func TestConnectSwarms(t *testing.T) {
time.Sleep(time.Second) time.Sleep(time.Second)
} }
func TestSwarmPeers(t *testing.T) {
ipfs, mock := testIPFSConnector(t)
defer mock.Close()
defer ipfs.Shutdown()
swarmPeers, err := ipfs.SwarmPeers()
if err != nil {
t.Fatal(err)
}
if len(swarmPeers) != 2 {
t.Fatal("expected 2 swarm peers")
}
if swarmPeers[0] != test.TestPeerID4 {
t.Error("unexpected swarm peer")
}
if swarmPeers[1] != test.TestPeerID5 {
t.Error("unexpected swarm peer")
}
}
func TestRepoSize(t *testing.T) { func TestRepoSize(t *testing.T) {
ipfs, mock := testIPFSConnector(t) ipfs, mock := testIPFSConnector(t)
defer mock.Close() defer mock.Close()

View File

@ -66,6 +66,12 @@
"hash": "QmWi28zbQG6B1xfaaWx5cYoLn3kBFU6pQ6GWQNRV5P6dNe", "hash": "QmWi28zbQG6B1xfaaWx5cYoLn3kBFU6pQ6GWQNRV5P6dNe",
"name": "lock", "name": "lock",
"version": "0.0.0" "version": "0.0.0"
},
{
"author": "ZenGround0",
"hash": "QmXoatUMzVryJXW1iucE9H4BayxAKzHSyRVZPxhWwEuX8y",
"name": "go-dot",
"version": "0.0.0"
} }
], ],
"gxVersion": "0.11.0", "gxVersion": "0.11.0",

View File

@ -5,9 +5,7 @@ test_description="Test ctl's status reporting functionality. Test errors on inc
. lib/test-lib.sh . lib/test-lib.sh
test_ipfs_init test_ipfs_init
cleanup test_clean_ipfs
test_cluster_init test_cluster_init
cleanup test_clean_cluster
test_expect_success IPFS,CLUSTER,JQ "cluster-ctl can read id" ' test_expect_success IPFS,CLUSTER,JQ "cluster-ctl can read id" '
id=`cluster_id` id=`cluster_id`
@ -59,4 +57,12 @@ test_expect_success IPFS,CLUSTER "pin ls on invalid CID fails" '
test_must_fail ipfs-cluster-ctl pin ls XXXinvalid-CIDXXX test_must_fail ipfs-cluster-ctl pin ls XXXinvalid-CIDXXX
' '
test_expect_success IPFS,CLUSTER "health graph succeeds and prints as expected" '
ipfs-cluster-ctl health graph | grep -q "C0 -> I0"
'
test_clean_ipfs
test_clean_cluster
test_done test_done

View File

@ -13,4 +13,7 @@ var (
TestPeerID1, _ = peer.IDB58Decode("QmXZrtE5jQwXNqCJMfHUTQkvhQ4ZAnqMnmzFMJfLewuabc") TestPeerID1, _ = peer.IDB58Decode("QmXZrtE5jQwXNqCJMfHUTQkvhQ4ZAnqMnmzFMJfLewuabc")
TestPeerID2, _ = peer.IDB58Decode("QmUZ13osndQ5uL4tPWHXe3iBgBgq9gfewcBMSCAuMBsDJ6") TestPeerID2, _ = peer.IDB58Decode("QmUZ13osndQ5uL4tPWHXe3iBgBgq9gfewcBMSCAuMBsDJ6")
TestPeerID3, _ = peer.IDB58Decode("QmPGDFvBkgWhvzEK9qaTWrWurSwqXNmhnK3hgELPdZZNPa") TestPeerID3, _ = peer.IDB58Decode("QmPGDFvBkgWhvzEK9qaTWrWurSwqXNmhnK3hgELPdZZNPa")
TestPeerID4, _ = peer.IDB58Decode("QmZ8naDy5mEz4GLuQwjWt9MPYqHTBbsm8tQBrNSjiq6zBc")
TestPeerID5, _ = peer.IDB58Decode("QmZVAo3wd8s5eTTy2kPYs34J9PvfxpKPuYsePPYGjgRRjg")
TestPeerID6, _ = peer.IDB58Decode("QmR8Vu6kZk7JvAN2rWVWgiduHatgBq2bb15Yyq8RRhYSbx")
) )

View File

@ -63,6 +63,14 @@ type mockAddResp struct {
Bytes uint64 Bytes uint64
} }
type mockSwarmPeersResp struct {
Peers []mockIpfsPeer
}
type mockIpfsPeer struct {
Peer string
}
// NewIpfsMock returns a new mock. // NewIpfsMock returns a new mock.
func NewIpfsMock() *IpfsMock { func NewIpfsMock() *IpfsMock {
st := mapstate.NewMapState() st := mapstate.NewMapState()
@ -211,6 +219,18 @@ func (m *IpfsMock) handler(w http.ResponseWriter, r *http.Request) {
} }
j, _ := json.Marshal(resp) j, _ := json.Marshal(resp)
w.Write(j) w.Write(j)
case "swarm/peers":
peer1 := mockIpfsPeer{
Peer: TestPeerID4.Pretty(),
}
peer2 := mockIpfsPeer{
Peer: TestPeerID5.Pretty(),
}
resp := mockSwarmPeersResp{
Peers: []mockIpfsPeer{peer1, peer2},
}
j, _ := json.Marshal(resp)
w.Write(j)
case "repo/stat": case "repo/stat":
len := len(m.pinMap.List()) len := len(m.pinMap.List())
resp := mockRepoStatResp{ resp := mockRepoStatResp{

View File

@ -118,6 +118,28 @@ func (mock *mockService) PeerRemove(in peer.ID, out *struct{}) error {
return nil return nil
} }
func (mock *mockService) ConnectGraph(in struct{}, out *api.ConnectGraphSerial) error {
*out = api.ConnectGraphSerial{
ClusterID: TestPeerID1.Pretty(),
IPFSLinks: map[string][]string{
TestPeerID4.Pretty(): []string{TestPeerID5.Pretty(), TestPeerID6.Pretty()},
TestPeerID5.Pretty(): []string{TestPeerID4.Pretty(), TestPeerID6.Pretty()},
TestPeerID6.Pretty(): []string{TestPeerID4.Pretty(), TestPeerID5.Pretty()},
},
ClusterLinks: map[string][]string{
TestPeerID1.Pretty(): []string{TestPeerID2.Pretty(), TestPeerID3.Pretty()},
TestPeerID2.Pretty(): []string{TestPeerID1.Pretty(), TestPeerID3.Pretty()},
TestPeerID3.Pretty(): []string{TestPeerID1.Pretty(), TestPeerID2.Pretty()},
},
ClustertoIPFS: map[string]string{
TestPeerID1.Pretty(): TestPeerID4.Pretty(),
TestPeerID2.Pretty(): TestPeerID5.Pretty(),
TestPeerID3.Pretty(): TestPeerID6.Pretty(),
},
}
return nil
}
func (mock *mockService) StatusAll(in struct{}, out *[]api.GlobalPinInfoSerial) error { func (mock *mockService) StatusAll(in struct{}, out *[]api.GlobalPinInfoSerial) error {
c1, _ := cid.Decode(TestCid1) c1, _ := cid.Decode(TestCid1)
c2, _ := cid.Decode(TestCid2) c2, _ := cid.Decode(TestCid2)
@ -320,6 +342,11 @@ func (mock *mockService) IPFSConnectSwarms(in struct{}, out *struct{}) error {
return nil return nil
} }
func (mock *mockService) IPFSSwarmPeers(in struct{}, out *api.SwarmPeersSerial) error {
*out = []string{TestPeerID2.Pretty(), TestPeerID3.Pretty()}
return nil
}
func (mock *mockService) IPFSConfigKey(in string, out *interface{}) error { func (mock *mockService) IPFSConfigKey(in string, out *interface{}) error {
switch in { switch in {
case "Datastore/StorageMax": case "Datastore/StorageMax":

View File

@ -44,6 +44,14 @@ func copyPinInfoSerialSliceToIfaces(in [][]api.PinInfoSerial) []interface{} {
return ifaces return ifaces
} }
func copyIDSerialSliceToIfaces(in [][]api.IDSerial) []interface{} {
ifaces := make([]interface{}, len(in), len(in))
for i := range in {
ifaces[i] = &in[i]
}
return ifaces
}
func copyEmptyStructToIfaces(in []struct{}) []interface{} { func copyEmptyStructToIfaces(in []struct{}) []interface{} {
ifaces := make([]interface{}, len(in), len(in)) ifaces := make([]interface{}, len(in), len(in))
for i := range in { for i := range in {