Merge pull request #103 from ethersphere/network-testing-framework-p2psim

Simulation framework improvements
This commit is contained in:
Lewis Marshall 2017-06-21 09:34:45 +02:00 committed by GitHub
commit 3899c3e086
6 changed files with 156 additions and 98 deletions

View file

@ -13,6 +13,7 @@ import (
"github.com/docker/docker/pkg/reexec" "github.com/docker/docker/pkg/reexec"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/discover"
) )
// DockerAdapter is a NodeAdapter which runs nodes inside Docker containers. // DockerAdapter is a NodeAdapter which runs nodes inside Docker containers.
@ -20,7 +21,9 @@ import (
// A Docker image is built which contains the current binary at /bin/p2p-node // A Docker image is built which contains the current binary at /bin/p2p-node
// which when executed runs the underlying service (see the description // which when executed runs the underlying service (see the description
// of the execP2PNode function for more details) // of the execP2PNode function for more details)
type DockerAdapter struct{} type DockerAdapter struct {
ExecAdapter
}
// NewDockerAdapter builds the p2p-node Docker image containing the current // NewDockerAdapter builds the p2p-node Docker image containing the current
// binary and returns a DockerAdapter // binary and returns a DockerAdapter
@ -33,7 +36,11 @@ func NewDockerAdapter() (*DockerAdapter, error) {
return nil, err return nil, err
} }
return &DockerAdapter{}, nil return &DockerAdapter{
ExecAdapter{
nodes: make(map[discover.NodeID]*ExecNode),
},
}, nil
} }
// Name returns the name of the adapter for logging purpoeses // Name returns the name of the adapter for logging purpoeses
@ -58,17 +65,22 @@ func (d *DockerAdapter) NewNode(config *NodeConfig) (Node, error) {
Node: config, Node: config,
} }
conf.Stack.DataDir = "/data" conf.Stack.DataDir = "/data"
conf.Stack.WSHost = "0.0.0.0"
conf.Stack.WSOrigins = []string{"*"}
conf.Stack.WSExposeAll = true
conf.Stack.P2P.EnableMsgEvents = true conf.Stack.P2P.EnableMsgEvents = true
conf.Stack.P2P.NoDiscovery = true conf.Stack.P2P.NoDiscovery = true
conf.Stack.P2P.NAT = nil conf.Stack.P2P.NAT = nil
node := &DockerNode{ node := &DockerNode{
ExecNode: ExecNode{ ExecNode: ExecNode{
ID: config.ID, ID: config.ID,
Config: conf, Config: conf,
adapter: &d.ExecAdapter,
}, },
} }
node.newCmd = node.dockerCommand node.newCmd = node.dockerCommand
d.ExecAdapter.nodes[node.ID] = &node.ExecNode
return node, nil return node, nil
} }

View file

@ -183,25 +183,18 @@ func (n *ExecNode) Start(snapshots map[string][]byte) (err error) {
// read the WebSocket address from the stderr logs // read the WebSocket address from the stderr logs
var wsAddr string var wsAddr string
errC := make(chan error) wsAddrC := make(chan string)
go func() { go func() {
s := bufio.NewScanner(stderrR) s := bufio.NewScanner(stderrR)
for s.Scan() { for s.Scan() {
if strings.Contains(s.Text(), "WebSocket endpoint opened:") { if strings.Contains(s.Text(), "WebSocket endpoint opened:") {
wsAddr = wsAddrPattern.FindString(s.Text()) wsAddrC <- wsAddrPattern.FindString(s.Text())
break
} }
} }
select {
case errC <- s.Err():
default:
}
}() }()
select { select {
case err := <-errC: case wsAddr = <-wsAddrC:
if err != nil { if wsAddr == "" {
return fmt.Errorf("error reading WebSocket address from stderr: %s", err)
} else if wsAddr == "" {
return errors.New("failed to read WebSocket address from stderr") return errors.New("failed to read WebSocket address from stderr")
} }
case <-time.After(10 * time.Second): case <-time.After(10 * time.Second):
@ -354,17 +347,24 @@ func execP2PNode() {
conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey
// use explicit IP address in ListenAddr so that Enode URL is usable // use explicit IP address in ListenAddr so that Enode URL is usable
if strings.HasPrefix(conf.Stack.P2P.ListenAddr, ":") { externalIP := func() string {
addrs, err := net.InterfaceAddrs() addrs, err := net.InterfaceAddrs()
if err != nil { if err != nil {
log.Crit("error getting IP address", "err", err) log.Crit("error getting IP address", "err", err)
} }
for _, addr := range addrs { for _, addr := range addrs {
if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() { if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() {
conf.Stack.P2P.ListenAddr = ip.IP.String() + conf.Stack.P2P.ListenAddr return ip.IP.String()
break
} }
} }
log.Crit("unable to determine explicit IP address")
return ""
}
if strings.HasPrefix(conf.Stack.P2P.ListenAddr, ":") {
conf.Stack.P2P.ListenAddr = externalIP() + conf.Stack.P2P.ListenAddr
}
if conf.Stack.WSHost == "0.0.0.0" {
conf.Stack.WSHost = externalIP()
} }
// initialize the devp2p stack // initialize the devp2p stack

View file

@ -26,20 +26,26 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/simulations" "github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" "github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
var client *simulations.Client var (
client *simulations.Client
networkID string
)
func main() { func main() {
app := cli.NewApp() app := cli.NewApp()
@ -73,9 +79,14 @@ func main() {
Action: createNetwork, Action: createNetwork,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "config", Name: "id",
Value: "{}", Value: "",
Usage: "JSON encoded network config", Usage: "network ID",
},
cli.StringFlag{
Name: "default-service",
Value: "",
Usage: "default service",
}, },
}, },
}, },
@ -109,59 +120,83 @@ func main() {
Name: "node", Name: "node",
Usage: "manage simulation nodes", Usage: "manage simulation nodes",
Action: listNodes, Action: listNodes,
Flags: []cli.Flag{
cli.StringFlag{
Name: "network",
Usage: "simulation network",
},
},
Before: func(ctx *cli.Context) error {
networkID = ctx.GlobalString("network")
if networkID == "" {
networkID = os.Getenv("P2PSIM_NETWORK")
}
if networkID == "" {
return errors.New("missing network, set with --network or P2PSIM_NETWORK")
}
return nil
},
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{ {
Name: "list", Name: "list",
ArgsUsage: "<network>", Usage: "list nodes",
Usage: "list nodes", Action: listNodes,
Action: listNodes,
}, },
{ {
Name: "create", Name: "create",
ArgsUsage: "<network>", Usage: "create a node",
Usage: "create a node", Action: createNode,
Action: createNode,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "config", Name: "name",
Value: "{}", Value: "",
Usage: "JSON encoded node config", Usage: "node name",
},
cli.StringFlag{
Name: "services",
Value: "",
Usage: "node services (comma separated)",
},
cli.StringFlag{
Name: "key",
Value: "",
Usage: "node private key (hex encoded)",
}, },
}, },
}, },
{ {
Name: "show", Name: "show",
ArgsUsage: "<network> <node>", ArgsUsage: "<node>",
Usage: "show node information", Usage: "show node information",
Action: showNode, Action: showNode,
}, },
{ {
Name: "start", Name: "start",
ArgsUsage: "<network> <node>", ArgsUsage: "<node>",
Usage: "start a node", Usage: "start a node",
Action: startNode, Action: startNode,
}, },
{ {
Name: "stop", Name: "stop",
ArgsUsage: "<network> <node>", ArgsUsage: "<node>",
Usage: "stop a node", Usage: "stop a node",
Action: stopNode, Action: stopNode,
}, },
{ {
Name: "connect", Name: "connect",
ArgsUsage: "<network> <node> <peer>", ArgsUsage: "<node> <peer>",
Usage: "connect a node to a peer node", Usage: "connect a node to a peer node",
Action: connectNode, Action: connectNode,
}, },
{ {
Name: "disconnect", Name: "disconnect",
ArgsUsage: "<network> <node> <peer>", ArgsUsage: "<node> <peer>",
Usage: "disconnect a node from a peer node", Usage: "disconnect a node from a peer node",
Action: disconnectNode, Action: disconnectNode,
}, },
{ {
Name: "rpc", Name: "rpc",
ArgsUsage: "<network> <node> <method> [<args>]", ArgsUsage: "<node> <method> [<args>]",
Usage: "call a node RPC method", Usage: "call a node RPC method",
Action: rpcNode, Action: rpcNode,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -198,9 +233,9 @@ func createNetwork(ctx *cli.Context) error {
if len(ctx.Args()) != 0 { if len(ctx.Args()) != 0 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
config := &simulations.NetworkConfig{} config := &simulations.NetworkConfig{
if err := json.Unmarshal([]byte(ctx.String("config")), config); err != nil { ID: ctx.String("id"),
return err DefaultService: ctx.String("default-service"),
} }
network, err := client.CreateNetwork(config) network, err := client.CreateNetwork(config)
if err != nil { if err != nil {
@ -280,11 +315,9 @@ func loadSnapshot(ctx *cli.Context) error {
} }
func listNodes(ctx *cli.Context) error { func listNodes(ctx *cli.Context) error {
args := ctx.Args() if len(ctx.Args()) != 0 {
if len(args) != 1 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0]
nodes, err := client.GetNodes(networkID) nodes, err := client.GetNodes(networkID)
if err != nil { if err != nil {
return err return err
@ -307,14 +340,22 @@ func protocolList(node *p2p.NodeInfo) []string {
} }
func createNode(ctx *cli.Context) error { func createNode(ctx *cli.Context) error {
args := ctx.Args() if len(ctx.Args()) != 0 {
if len(args) != 1 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] config := &adapters.NodeConfig{
config := &adapters.NodeConfig{} Name: ctx.String("name"),
if err := json.Unmarshal([]byte(ctx.String("config")), config); err != nil { }
return err if key := ctx.String("key"); key != "" {
privKey, err := crypto.HexToECDSA(key)
if err != nil {
return err
}
config.ID = discover.PubkeyID(&privKey.PublicKey)
config.PrivateKey = privKey
}
if services := ctx.String("services"); services != "" {
config.Services = strings.Split(services, ",")
} }
node, err := client.CreateNode(networkID, config) node, err := client.CreateNode(networkID, config)
if err != nil { if err != nil {
@ -326,11 +367,10 @@ func createNode(ctx *cli.Context) error {
func showNode(ctx *cli.Context) error { func showNode(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
if len(args) != 2 { if len(args) != 1 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] nodeName := args[0]
nodeName := args[1]
node, err := client.GetNode(networkID, nodeName) node, err := client.GetNode(networkID, nodeName)
if err != nil { if err != nil {
return err return err
@ -352,11 +392,10 @@ func showNode(ctx *cli.Context) error {
func startNode(ctx *cli.Context) error { func startNode(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
if len(args) != 2 { if len(args) != 1 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] nodeName := args[0]
nodeName := args[1]
if err := client.StartNode(networkID, nodeName); err != nil { if err := client.StartNode(networkID, nodeName); err != nil {
return err return err
} }
@ -366,11 +405,10 @@ func startNode(ctx *cli.Context) error {
func stopNode(ctx *cli.Context) error { func stopNode(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
if len(args) != 2 { if len(args) != 1 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] nodeName := args[0]
nodeName := args[1]
if err := client.StopNode(networkID, nodeName); err != nil { if err := client.StopNode(networkID, nodeName); err != nil {
return err return err
} }
@ -380,12 +418,11 @@ func stopNode(ctx *cli.Context) error {
func connectNode(ctx *cli.Context) error { func connectNode(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
if len(args) != 3 { if len(args) != 2 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] nodeName := args[0]
nodeName := args[1] peerName := args[1]
peerName := args[2]
if err := client.ConnectNode(networkID, nodeName, peerName); err != nil { if err := client.ConnectNode(networkID, nodeName, peerName); err != nil {
return err return err
} }
@ -395,12 +432,11 @@ func connectNode(ctx *cli.Context) error {
func disconnectNode(ctx *cli.Context) error { func disconnectNode(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
if len(args) != 3 { if len(args) != 2 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] nodeName := args[0]
nodeName := args[1] peerName := args[1]
peerName := args[2]
if err := client.DisconnectNode(networkID, nodeName, peerName); err != nil { if err := client.DisconnectNode(networkID, nodeName, peerName); err != nil {
return err return err
} }
@ -410,12 +446,11 @@ func disconnectNode(ctx *cli.Context) error {
func rpcNode(ctx *cli.Context) error { func rpcNode(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
if len(args) < 3 { if len(args) < 2 {
return cli.ShowCommandHelp(ctx, ctx.Command.Name) return cli.ShowCommandHelp(ctx, ctx.Command.Name)
} }
networkID := args[0] nodeName := args[0]
nodeName := args[1] method := args[1]
method := args[2]
rpcClient, err := client.RPCClient(context.Background(), networkID, nodeName) rpcClient, err := client.RPCClient(context.Background(), networkID, nodeName)
if err != nil { if err != nil {
return err return err

View file

@ -67,7 +67,9 @@ func main() {
} }
log.Info("starting simulation server on 0.0.0.0:8888...") log.Info("starting simulation server on 0.0.0.0:8888...")
http.ListenAndServe(":8888", simulations.NewServer(config)) if err := http.ListenAndServe(":8888", simulations.NewServer(config)); err != nil {
log.Crit("error starting simulation server", "err", err)
}
} }
// pingPongService runs a ping-pong protocol between nodes where each node // pingPongService runs a ping-pong protocol between nodes where each node

View file

@ -10,17 +10,18 @@ main() {
fi fi
info "creating the example network" info "creating the example network"
p2psim network create --config '{"id": "example", "default_service": "ping-pong"}' export P2PSIM_NETWORK="example"
p2psim network create --id "${P2PSIM_NETWORK}"
info "creating 10 nodes" info "creating 10 nodes"
for i in $(seq 1 10); do for i in $(seq 1 10); do
p2psim node create "example" p2psim node create --name "$(node_name $i)" --services "ping-pong"
p2psim node start "example" "$(node_name $i)" p2psim node start "$(node_name $i)"
done done
info "connecting node01 to all other nodes" info "connecting node01 to all other nodes"
for i in $(seq 2 10); do for i in $(seq 2 10); do
p2psim node connect "example" "node01" "$(node_name $i)" p2psim node connect "node01" "$(node_name $i)"
done done
info "done" info "done"

View file

@ -201,10 +201,21 @@ func (self *Network) NewNode() (*Node, error) {
func (self *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) { func (self *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) {
self.lock.Lock() self.lock.Lock()
defer self.lock.Unlock() defer self.lock.Unlock()
if conf.ID == (discover.NodeID{}) {
c := adapters.RandomNodeConfig()
conf.ID = c.ID
conf.PrivateKey = c.PrivateKey
}
id := conf.ID id := conf.ID
if node := self.getNode(id); node != nil {
return nil, fmt.Errorf("node already exists: %q", id)
}
if conf.Name == "" { if conf.Name == "" {
conf.Name = fmt.Sprintf("node%02d", len(self.Nodes)+1) conf.Name = fmt.Sprintf("node%02d", len(self.Nodes)+1)
} }
if node := self.getNodeByName(conf.Name); node != nil {
return nil, fmt.Errorf("node already exists: %q", conf.Name)
}
if len(conf.Services) == 0 { if len(conf.Services) == 0 {
conf.Services = []string{self.DefaultService} conf.Services = []string{self.DefaultService}
} }
@ -326,7 +337,16 @@ func (self *Network) startWithSnapshots(id discover.NodeID, snapshots map[string
} }
func (self *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEvent, sub event.Subscription) { func (self *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEvent, sub event.Subscription) {
defer sub.Unsubscribe() defer func() {
sub.Unsubscribe()
// assume the node is now down
self.lock.Lock()
node := self.getNode(id)
node.Up = false
self.lock.Unlock()
self.events.Send(NewEvent(node))
}()
for { for {
select { select {
case event, ok := <-events: case event, ok := <-events:
@ -336,21 +356,13 @@ func (self *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEv
peer := event.Peer peer := event.Peer
switch event.Type { switch event.Type {
case p2p.PeerEventTypeAdd: case p2p.PeerEventTypeAdd:
if err := self.DidConnect(id, peer); err != nil { self.DidConnect(id, peer)
log.Error(fmt.Sprintf("error generating connection up event %s => %s", id.TerminalString(), peer.TerminalString()), "err", err)
}
case p2p.PeerEventTypeDrop: case p2p.PeerEventTypeDrop:
if err := self.DidDisconnect(id, peer); err != nil { self.DidDisconnect(id, peer)
log.Error(fmt.Sprintf("error generating connection down event %s => %s", id.TerminalString(), peer.TerminalString()), "err", err)
}
case p2p.PeerEventTypeMsgSend: case p2p.PeerEventTypeMsgSend:
if err := self.DidSend(id, peer, *event.MsgCode); err != nil { self.DidSend(id, peer, *event.MsgCode)
log.Error(fmt.Sprintf("error generating msg send event %s => %s", id.TerminalString(), peer.TerminalString()), "err", err)
}
case p2p.PeerEventTypeMsgRecv: case p2p.PeerEventTypeMsgRecv:
if err := self.DidReceive(peer, id, *event.MsgCode); err != nil { self.DidReceive(peer, id, *event.MsgCode)
log.Error(fmt.Sprintf("error generating msg receive event %s => %s", peer.TerminalString(), id.TerminalString()), "err", err)
}
} }
case err := <-sub.Err(): case err := <-sub.Err():
if err != nil { if err != nil {
@ -528,6 +540,10 @@ func (self *Network) GetNode(id discover.NodeID) *Node {
func (self *Network) GetNodeByName(name string) *Node { func (self *Network) GetNodeByName(name string) *Node {
self.lock.Lock() self.lock.Lock()
defer self.lock.Unlock() defer self.lock.Unlock()
return self.getNodeByName(name)
}
func (self *Network) getNodeByName(name string) *Node {
for _, node := range self.Nodes { for _, node := range self.Nodes {
if node.Config.Name == name { if node.Config.Name == name {
return node return node
@ -589,14 +605,6 @@ func (self *Network) getConn(oneID, otherID discover.NodeID) *Conn {
} }
func (self *Network) Shutdown() { func (self *Network) Shutdown() {
// disconnect all nodes
for _, conn := range self.Conns {
log.Debug(fmt.Sprintf("disconnecting %s from %s", conn.One.TerminalString(), conn.Other.TerminalString()))
if err := self.Disconnect(conn.One, conn.Other); err != nil {
log.Warn(fmt.Sprintf("error disconnecting %s from %s", conn.One.TerminalString(), conn.Other.TerminalString()), "err", err)
}
}
// stop all nodes // stop all nodes
for _, node := range self.Nodes { for _, node := range self.Nodes {
log.Debug(fmt.Sprintf("stopping node %s", node.ID().TerminalString())) log.Debug(fmt.Sprintf("stopping node %s", node.ID().TerminalString()))