PoC: Network simulation framework (#1555)

* simv2: wip

* simulation: exec adapter start/stop

* simulation: add node status to exec adapter

* simulation: initial simulation code

* simulation: exec adapter, configure path to executable

* simulation: initial docker adapter

* simulation: wip kubernetes adapter

* simulation: kubernetes adapter proxy

* simulation: implement GetAll/StartAll/StopAll

* simulation: kuberentes adapter - set env vars and resource limits

* simulation: discovery test

* simulation: remove port definitions within docker adapter

* simulation: simplify wait for healthy loop

* simulation: get nat ip addr from interface

* simulation: pull docker images automatically

* simulation: NodeStatus -> NodeInfo

* simulation: move discovery test to example dir

* simulation: example snapshot usage

* simulation: add goclient specific simulation

* simulation: add peer connections to snapshot

* simulation: close rpc client

* simulation: don't export kubernetes proxy server

* simulation: merge simulation code

* simulation: don't export nodemap

* simulation: rename SimulationSnapshot -> Snapshot

* simulation: linting fixes

* simulation: add k8s available helper func

* simulation: vendor

* simulation: fix 'no non-test Go files' when building

* simulation: remove errors from interface methods where non were returned

* simulation: run getHealthInfo check in parallel
This commit is contained in:
Rafael Matias
2019-07-24 17:00:13 +02:00
committed by GitHub
parent 9720da34db
commit 388d8ccd9f
1570 changed files with 460774 additions and 3203 deletions

416
simulation/docker.go Normal file
View File

@@ -0,0 +1,416 @@
package simulation
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/swarm"
"github.com/ethersphere/swarm/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/jsonmessage"
)
const (
dockerP2PPort = 30399
dockerWebsocketPort = 8546
dockerHTTPPort = 8500
dockerPProfPort = 6060
)
// DockerAdapter is an adapter that can manage DockerNodes
type DockerAdapter struct {
client *client.Client
image string
config DockerAdapterConfig
}
// DockerAdapterConfig is the configuration that can be provided when
// initializing a DockerAdapter
type DockerAdapterConfig struct {
// BuildContext can be used to build a docker image
// from a Dockerfile and a context directory
BuildContext *DockerBuildContext `json:"build,omitempty"`
// DockerImage points to an existing docker image
// e.g. ethersphere/swarm:latest
DockerImage string `json:"image,omitempty"`
// DaemonAddr is the docker daemon address
DaemonAddr string `json:"daemonAddr,omitempty"`
}
// DockerBuildContext defines the build context to build
// local docker images
type DockerBuildContext struct {
// Dockefile is the path to the dockerfile
Dockerfile string `json:"dockerfile"`
// Directory is the directory that will be used
// in the context of a docker build
Directory string `json:"directory"`
// Tag is used to tag the image
Tag string `json:"tag"`
}
// DockerNode is a node that was started via the DockerAdapter
type DockerNode struct {
config NodeConfig
adapter *DockerAdapter
info NodeInfo
ipAddr string
}
// DefaultDockerAdapterConfig returns the default configuration
// that uses the local docker daemon
func DefaultDockerAdapterConfig() DockerAdapterConfig {
return DockerAdapterConfig{
DaemonAddr: client.DefaultDockerHost,
}
}
// DefaultDockerBuildContext returns the default build context that uses a Dockerfile
func DefaultDockerBuildContext() DockerBuildContext {
return DockerBuildContext{
Dockerfile: "Dockerfile",
Directory: ".",
}
}
// IsDockerAvailable can be used to check the connectivity to the docker daemon
func IsDockerAvailable(daemonAddr string) bool {
cli, err := client.NewClientWithOpts(
client.WithHost(daemonAddr),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return false
}
_, err = cli.ServerVersion(context.Background())
if err != nil {
return false
}
cli.Close()
return true
}
// NewDockerAdapter creates a DockerAdapter by receiving a DockerAdapterConfig
func NewDockerAdapter(config DockerAdapterConfig) (*DockerAdapter, error) {
if config.DockerImage != "" && config.BuildContext != nil {
return nil, fmt.Errorf("only one can be defined: BuildContext (%v) or DockerImage(%s)",
config.BuildContext, config.DockerImage)
}
if config.DockerImage == "" && config.BuildContext == nil {
return nil, errors.New("required: BuildContext or ExecutablePath")
}
// Create docker client
cli, err := client.NewClientWithOpts(
client.WithHost(config.DaemonAddr),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return nil, fmt.Errorf("could not create the docker client: %v", err)
}
// Figure out which docker image should be used
image := config.DockerImage
// Build docker image
if config.BuildContext != nil {
var err error
image, err = buildImage(*config.BuildContext, config.DaemonAddr)
if err != nil {
return nil, fmt.Errorf("could not build the docker image: %v", err)
}
}
// Pull docker image
if config.DockerImage != "" {
reader, err := cli.ImagePull(context.Background(), config.DockerImage, types.ImagePullOptions{})
if err != nil {
return nil, fmt.Errorf("pull image error: %v", err)
}
if _, err := io.Copy(os.Stdout, reader); err != nil && err != io.EOF {
log.Error("Error pulling docker image", "err", err)
}
}
return &DockerAdapter{
image: image,
client: cli,
config: config,
}, nil
}
// NewNode creates a new node
func (a DockerAdapter) NewNode(config NodeConfig) Node {
info := NodeInfo{
ID: config.ID,
}
node := &DockerNode{
config: config,
adapter: &a,
info: info,
}
return node
}
// Snapshot returns a snapshot of the adapter
func (a DockerAdapter) Snapshot() AdapterSnapshot {
return AdapterSnapshot{
Type: "docker",
Config: a.config,
}
}
// Info returns the node status
func (n *DockerNode) Info() NodeInfo {
return n.info
}
// Start starts the node
func (n *DockerNode) Start() error {
var err error
defer func() {
if err != nil {
log.Error("Stopping node due to errors", "err", err)
if err := n.Stop(); err != nil {
log.Error("Failed stopping node", "err", err)
}
}
}()
// Define arguments
args := []string{}
// Append user defined arguments
args = append(args, n.config.Args...)
// Append network ports arguments
args = append(args, "--pprofport", strconv.Itoa(dockerPProfPort))
args = append(args, "--bzzport", strconv.Itoa(dockerHTTPPort))
args = append(args, "--ws")
// TODO: Can we get the APIs from somewhere instead of hardcoding them here?
args = append(args, "--wsapi", "admin,net,debug,bzz,accounting,hive")
args = append(args, "--wsport", strconv.Itoa(dockerWebsocketPort))
args = append(args, "--wsaddr", "0.0.0.0")
args = append(args, "--wsorigins", "*")
args = append(args, "--port", strconv.Itoa(dockerP2PPort))
args = append(args, "--natif", "eth0")
// Start the node via a container
ctx := context.Background()
dockercli := n.adapter.client
resp, err := dockercli.ContainerCreate(ctx, &container.Config{
Image: n.adapter.image,
Cmd: args,
Env: n.config.Env,
}, &container.HostConfig{}, nil, n.containerName())
if err != nil {
return fmt.Errorf("failed to create container %s: %v", n.containerName(), err)
}
if err := dockercli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
return fmt.Errorf("failed to start container %s: %v", n.containerName(), err)
}
// Get container logs
if n.config.Stderr != nil {
go func() {
// Stderr
stderr, err := dockercli.ContainerLogs(context.Background(), n.containerName(), types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: false,
Follow: true,
})
if err != nil && err != io.EOF {
log.Error("Error getting stderr container logs", "err", err)
}
defer stderr.Close()
if _, err := io.Copy(n.config.Stderr, stderr); err != nil && err != io.EOF {
log.Error("Error writing stderr container logs", "err", err)
}
}()
}
if n.config.Stdout != nil {
go func() {
// Stdout
stdout, err := dockercli.ContainerLogs(context.Background(), n.containerName(), types.ContainerLogsOptions{
ShowStderr: false,
ShowStdout: true,
Follow: true,
})
if err != nil && err != io.EOF {
log.Error("Error getting stdout container logs", "err", err)
}
defer stdout.Close()
if _, err := io.Copy(n.config.Stdout, stdout); err != nil && err != io.EOF {
log.Error("Error writing stdout container logs", "err", err)
}
}()
}
// Get container info
cinfo := types.ContainerJSON{}
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
cinfo, err = dockercli.ContainerInspect(ctx, n.containerName())
if err == nil {
break
}
}
if err != nil {
return fmt.Errorf("could not get container info: %v", err)
}
// Get the container IP addr
n.ipAddr = cinfo.NetworkSettings.IPAddress
// Wait for the node to start
client, err := n.rpcClient()
if err != nil {
return err
}
defer client.Close()
var swarminfo swarm.Info
err = client.Call(&swarminfo, "bzz_info")
if err != nil {
return fmt.Errorf("could not get info via rpc call. node %s: %v", n.config.ID, err)
}
var p2pinfo p2p.NodeInfo
err = client.Call(&p2pinfo, "admin_nodeInfo")
if err != nil {
return fmt.Errorf("could not get info via rpc call. node %s: %v", n.config.ID, err)
}
n.info = NodeInfo{
ID: n.config.ID,
Enode: strings.Replace(p2pinfo.Enode, "127.0.0.1", n.ipAddr, 1),
BzzAddr: swarminfo.BzzKey,
RPCListen: fmt.Sprintf("ws://%s:%d", n.ipAddr, dockerWebsocketPort),
HTTPListen: fmt.Sprintf("http://%s:%d", n.ipAddr, dockerHTTPPort),
PprofListen: fmt.Sprintf("http://%s:%d", n.ipAddr, dockerPProfPort),
}
return nil
}
// Stop stops the node
func (n *DockerNode) Stop() error {
cli := n.adapter.client
var stopTimeout = 30 * time.Second
err := cli.ContainerStop(context.Background(), n.containerName(), &stopTimeout)
if err != nil {
return fmt.Errorf("failed to stop container %s : %v", n.containerName(), err)
}
err = cli.ContainerRemove(context.Background(), n.containerName(), types.ContainerRemoveOptions{})
if err != nil {
return fmt.Errorf("failed to remove container %s : %v", n.containerName(), err)
}
return nil
}
// Snapshot returns a snapshot of the node
func (n *DockerNode) Snapshot() (NodeSnapshot, error) {
snap := NodeSnapshot{
Config: n.config,
}
adapterSnap := n.adapter.Snapshot()
snap.Adapter = &adapterSnap
return snap, nil
}
func (n *DockerNode) containerName() string {
return fmt.Sprintf("sim-docker-%s", n.config.ID)
}
func (n *DockerNode) rpcClient() (*rpc.Client, error) {
var client *rpc.Client
var err error
wsAddr := fmt.Sprintf("ws://%s:%d", n.ipAddr, dockerWebsocketPort)
for start := time.Now(); time.Since(start) < 30*time.Second; time.Sleep(50 * time.Millisecond) {
client, err = rpc.Dial(wsAddr)
if err == nil {
break
}
}
if client == nil {
return nil, fmt.Errorf("could not establish rpc connection. node %s: %v", n.config.ID, err)
}
return client, nil
}
// buildImage builds a docker image and returns the image identifier (tag).
func buildImage(buildContext DockerBuildContext, deamonAddr string) (string, error) {
// Connect to docker daemon
c, err := client.NewClientWithOpts(
client.WithHost(deamonAddr),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return "", fmt.Errorf("could not create docker client: %v", err)
}
defer c.Close()
// Use directory for build context
ctx, err := archive.TarWithOptions(buildContext.Directory, &archive.TarOptions{})
if err != nil {
return "", err
}
// Default image tag
imageTag := "sim-docker:latest"
// Use a tag if one is defined
if buildContext.Tag != "" {
imageTag = buildContext.Tag
}
// Build image
opts := types.ImageBuildOptions{
SuppressOutput: false,
PullParent: true,
Tags: []string{imageTag},
Dockerfile: buildContext.Dockerfile,
}
buildResp, err := c.ImageBuild(context.Background(), ctx, opts)
if err != nil {
return "", fmt.Errorf("build error: %v", err)
}
// Parse build output
d := json.NewDecoder(buildResp.Body)
var event *jsonmessage.JSONMessage
for {
if err := d.Decode(&event); err != nil {
if err == io.EOF {
break
}
return "", err
}
log.Info("Docker build", "msg", event.Stream)
if event.Error != nil {
log.Error("Docker build error", "err", event.Error.Message)
return "", fmt.Errorf("failed to build docker image: %v", event.Error)
}
}
return imageTag, nil
}

53
simulation/docker_test.go Normal file
View File

@@ -0,0 +1,53 @@
package simulation
import (
"context"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func TestDockerAdapterBuild(t *testing.T) {
if !IsDockerAvailable(client.DefaultDockerHost) {
t.Skip("could not connect to the docker daemon")
}
// Create a docker client
c, err := dockerClient()
if err != nil {
t.Fatalf("could not create docker client: %v", err)
}
defer c.Close()
imageTag := "test-docker-adapter-build:latest"
config := DefaultDockerAdapterConfig()
// Build based on a Dockerfile
config.BuildContext = &DockerBuildContext{
Directory: "../",
Dockerfile: "Dockerfile",
Tag: imageTag,
}
// Create docker adapter: This will build the image
_, err = NewDockerAdapter(config)
if err != nil {
t.Fatalf("could not create docker adapter: %v", err)
}
// Cleanup image
_, err = c.ImageRemove(context.Background(), imageTag, types.ImageRemoveOptions{})
if err != nil {
t.Fatalf("could not delete docker image: %v", err)
}
}
// Create docker client
func dockerClient() (*client.Client, error) {
return client.NewClientWithOpts(
client.WithHost(client.DefaultDockerHost),
client.WithAPIVersionNegotiation(),
)
}

View File

@@ -0,0 +1 @@
package cluster

View File

@@ -0,0 +1,132 @@
package cluster
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/log"
"github.com/ethersphere/swarm/simulation"
colorable "github.com/mattn/go-colorable"
)
var (
nodes = flag.Int("nodes", 20, "number of nodes to create")
loglevel = flag.Int("loglevel", 3, "verbosity of logs")
rawlog = flag.Bool("rawlog", false, "remove terminal formatting from logs")
)
func init() {
flag.Parse()
log.PrintOrigins(true)
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(!*rawlog))))
}
func TestCluster(t *testing.T) {
nodeCount := *nodes
// Test exec adapter
t.Run("exec", func(t *testing.T) {
execPath := "../../../build/bin/swarm"
if _, err := os.Stat(execPath); err != nil {
if os.IsNotExist(err) {
t.Skip("swarm binary not found. build it before running the test")
}
}
tmpdir, err := ioutil.TempDir("", "test-sim-exec")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
adapter, err := simulation.NewExecAdapter(simulation.ExecAdapterConfig{
ExecutablePath: execPath,
BaseDataDirectory: tmpdir,
})
if err != nil {
t.Fatalf("could not create exec adapter: %v", err)
}
startSimulation(t, adapter, nodeCount)
})
// Test docker adapter
t.Run("docker", func(t *testing.T) {
config := simulation.DefaultDockerAdapterConfig()
if !simulation.IsDockerAvailable(config.DaemonAddr) {
t.Skip("docker is not available, skipping test")
}
config.DockerImage = "ethersphere/swarm:edge"
adapter, err := simulation.NewDockerAdapter(config)
if err != nil {
t.Fatalf("could not create docker adapter: %v", err)
}
startSimulation(t, adapter, nodeCount)
})
// Test kubernetes adapter
t.Run("kubernetes", func(t *testing.T) {
config := simulation.DefaultKubernetesAdapterConfig()
if !simulation.IsKubernetesAvailable(config.KubeConfigPath) {
t.Skip("kubernetes is not available, skipping test")
}
config.Namespace = "simulation-test"
config.DockerImage = "ethersphere/swarm:edge"
adapter, err := simulation.NewKubernetesAdapter(config)
if err != nil {
t.Fatalf("could not create kubernetes adapter: %v", err)
}
startSimulation(t, adapter, nodeCount)
})
}
func startSimulation(t *testing.T, adapter simulation.Adapter, count int) {
sim := simulation.NewSimulation(adapter)
defer sim.StopAll()
// Common args used by all nodes
commonArgs := []string{
"--bzznetworkid", "599",
}
// Start a cluster with 'count' nodes and a bootnode
nodes, err := sim.CreateClusterWithBootnode("test", count, commonArgs)
if err != nil {
t.Fatal(err)
}
// Wait for all nodes to be considered healthy
err = sim.WaitForHealthyNetwork()
if err != nil {
t.Errorf("Failed to get healthy network: %v", err)
}
// Check hive output on the first node
client, err := sim.RPCClient(nodes[0].Info().ID)
if err != nil {
t.Errorf("Failed to get rpc client: %v", err)
}
var hive string
err = client.Call(&hive, "bzz_hive")
if err != nil {
t.Errorf("could not get hive info: %v", err)
}
snap, err := sim.Snapshot()
if err != nil {
t.Error(err)
}
b, err := json.Marshal(snap)
if err != nil {
t.Error(err)
}
fmt.Println(string(b))
fmt.Println(hive)
}

View File

@@ -0,0 +1,641 @@
{
"defaultAdapter": {
"type": "docker",
"config": {
"image": "ethersphere/swarm:edge",
"daemonAddr": "unix:///var/run/docker.sock"
}
},
"nodes": [
{
"config": {
"id": "test-1",
"args": [
"--bzzkeyhex",
"675a1c7dbaa5ea1bbddd46ee1a06e55929175760c5824bb5a6992d0b96f84e9d",
"--nodekeyhex",
"1de361ad2978679213635f7f11e35dbd55a34979c931734ff9c3760cb5a3e6ad",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-11",
"args": [
"--bzzkeyhex",
"9c7f8bfdcfcc129baf677585703e6061189f9076e354d27f4b7f663e37fdc92b",
"--nodekeyhex",
"a8f2e98633c85a0b3a89facafbd35cb5d10c556bdf142daab63fb554a53715ec",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-2",
"args": [
"--bzzkeyhex",
"f60e0733d0cf9ae1e77bccd3256e709d9e552848d5ae19b1536beeac7157c514",
"--nodekeyhex",
"51135dc28378604cb7d7113f22114ac68f0dc235332649ac688fb1d13b622e3f",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-15",
"args": [
"--bzzkeyhex",
"5dfd72d5b92dabde0e58a4e6289be7c1e738c40dac8fafd151e56974f739a8f9",
"--nodekeyhex",
"70893f8a2c3fe1a24dcfb202a81e33dce03c3ded913d8fda92e6eb51c737e8de",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-10",
"args": [
"--bzzkeyhex",
"bbd17f109585e6df0a92953985059b791a7be2d01a973743a5e0100b7ab06715",
"--nodekeyhex",
"8592bf30d3a88dff49bdf57d583e0b235475151a958e6bf285520767ab7e7104",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-18",
"args": [
"--bzzkeyhex",
"2315b8bc42c2ba56ed4f88ec2a60a4b02bf3f8e94f2faac8dd4d642502c2056a",
"--nodekeyhex",
"975466b3530c2bf5940219d43525eadf5385df9f2defc1fa16b10d141f272b49",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-7",
"args": [
"--bzzkeyhex",
"494bceb4a9e564b1da2f00f5b6ff872dbfcfcbee4655e704e85ebfdd86bf29e5",
"--nodekeyhex",
"c677fdca73661ef5dc3b7773bbc52f7444b5ac9913230b7084a84f1a5f10b2cc",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-12",
"args": [
"--bzzkeyhex",
"dde10d9d208b0f70951e96941db0d9b245175f43ab5485cf0c8811d12f9b9cdb",
"--nodekeyhex",
"747ed88c92cdd8bf77aa0b09528038f457657ffb97e460895a7284b796da9dd4",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-9",
"args": [
"--bzzkeyhex",
"10b0fff107a590a424e5e74abdd119406ed096b7e600cbe04ff67584f9d55c51",
"--nodekeyhex",
"7ce5b908dfe70f5103948c413debc0fa03f50a6426b51da158438922abf3ea10",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-4",
"args": [
"--bzzkeyhex",
"3b0f49376815b1952f3c73fe6fd01a93e48d4bfac5f871c4712e092181549d91",
"--nodekeyhex",
"7bfe1dd5c1bbe83d9e29828e58e8cf9a431584a7d34c7bfb795a027a4adced8a",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-16",
"args": [
"--bzzkeyhex",
"46ed3a44c6fa46d4a6368b3d915134b185986ac7627a70b970f787c8896d6c6a",
"--nodekeyhex",
"e48066d163683429380f8b48fea282e25a184c9a50a5dbf24d85186469ca795c",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-19",
"args": [
"--bzzkeyhex",
"9314b3cc4d2cfa01fd044df334ec840f999f0ebb11f7a8a184786cef17703fc2",
"--nodekeyhex",
"4775f2c8a3cec55208c14d0a9b7fcdaf2447513142e69e03a3e8e2a759de0b55",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-14",
"args": [
"--bzzkeyhex",
"c54019ce7e3692db5f7d3531c6c00c96b9b744acf520ebd124194a5789c732d9",
"--nodekeyhex",
"94b5c4e9fd4496bdc3f1bd673cca9cbb2f4afe354a8d6ebb50d198798f0f4a45",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-13",
"args": [
"--bzzkeyhex",
"c7f8d404fee2b04242534a3486b1cf031ce6af150a075ba943d50bf1968dd85a",
"--nodekeyhex",
"24ae9cdc2ceea666fc588586b0e062721744562495e064fccb0ada19481f133d",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-17",
"args": [
"--bzzkeyhex",
"d48f7cb8ac5be805e700f1575cb644ea5cf05413dbdd9f969c43d1d54a9df29e",
"--nodekeyhex",
"8f85b38e6c6034d7c3c23d1391aaeae3eb3d680803b199cb716388de5fde3cf5",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-8",
"args": [
"--bzzkeyhex",
"ac7c28909be33eab4fccf5f39e808dd0a96ae412a3e7fe52f040ebb4d4d4d6f0",
"--nodekeyhex",
"bbef6fca5bc85f91d6315626fab3fdecdad8abfa3f38b216f98ee92489625a7c",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-5",
"args": [
"--bzzkeyhex",
"1a16fab4e3cbbc217db7dcb1bddb0ef12dd28c48c8a50bd4bb5550beaa80c7a0",
"--nodekeyhex",
"19234026cedfdfb10cf682f6491ff4c876411804949ac1b0dd5f1faee66f0bf3",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-3",
"args": [
"--bzzkeyhex",
"6410c2eadadac52be6a261a0e72060e5488f9165b24cc4c1764ad7a5ef6c0532",
"--nodekeyhex",
"6a3997bc8a3884797f18c8e4e345470db695d827a923975e5ce4dfb3c5d09d1e",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-0",
"args": [
"--bzzkeyhex",
"118ecf1dd2ebd700975af6de7dff806a3fcd41bc82987db96c45264694ec4276",
"--nodekeyhex",
"3422448fa8b3e6e41ea51a84262917d38d54e2c4345ec95d0586d741d31f91b5",
"--bzznetworkid",
"599"
]
}
},
{
"config": {
"id": "test-6",
"args": [
"--bzzkeyhex",
"66fa75cc080f0b5115c165004310378e98bb90cd652d4f9d92c71c489995942f",
"--nodekeyhex",
"e1a6f1bb9ed90cfa534dc8abb5088afa010acd7585837b3a8ea19c31c38da068",
"--bzznetworkid",
"599"
]
}
}
],
"connections": [
{
"from": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3",
"to": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4"
},
{
"from": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3",
"to": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa"
},
{
"from": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3",
"to": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d"
},
{
"from": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715",
"to": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af"
},
{
"from": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715",
"to": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d"
},
{
"from": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2"
},
{
"from": "enode://7f3385e72e489ff1b8f17f65af808348708d80df56d8a797fe9c8369128ecac6c8b2a58537f5d8987ea57a2779b3038b2a2fa3428eda55d5e0d6f4fc2f5c717f",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54",
"to": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4"
},
{
"from": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54",
"to": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2"
},
{
"from": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa",
"to": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4"
},
{
"from": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa",
"to": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54"
},
{
"from": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8",
"to": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715"
},
{
"from": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8",
"to": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d"
},
{
"from": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2",
"to": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa"
},
{
"from": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2",
"to": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2"
},
{
"from": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa",
"to": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3"
},
{
"from": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa",
"to": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4"
},
{
"from": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa",
"to": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa"
},
{
"from": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa",
"to": "enode://6419bc38b529be4c7ce7065783eeac741f20fbe8138f6cb0a12b2928e64cefda8c7c1ab6657e59a2c7d68ab9170e25e7e83a34d4fbd5231727c7f5bde800fc54"
},
{
"from": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://13b19a04feca4ebb50813019ef145d4622c650ae2cdc2cba0b00758b2f4bc2d6629a646d3a49eddae17b8e818fe41c558f1746bb1b713c3acad880c537c87ca2",
"to": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://d8cc415f199cba179af255ac6effc72afec5aee0c7754ae0a00ee8f91a6be4affc3aa75a75bba4ab75fb07aa7e2b50eeb0ca785c7066d1d23fd772deeefbd3aa"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://41a6567457b90fb8e9856575deaaaa90c344dda1897275bff680d56018d4dcdc8330a85aecc9a5d4f69f9b145f047e0d1fcda4d78647534b32167e983129e091"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d"
},
{
"from": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://f5c8f1ab0be7d77364cf7da43297abcc19cb8fbf66458ab78236235b362093c34d68bd0cf22c9a900a1b15398515edbb42663dafdaee8d69d91d40e8e58f69c8"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d"
},
{
"from": "enode://cfa1547aeb9d5ab143f138d945b3f72878ec204b7cc90f163fb3dce4201ad837478770c615785e597390eb8dbd26536f5b42b2e7620f1a6a9974bc894fb09d6d",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2",
"to": "enode://e76225bd1dc9ee34b2e551f82646f7a6f2f74bbd8a5f5bb048b9057e34696813b2ceddd3c75069b50c506c15bbc9b7153410e73e0637390a6550c29dea3995aa"
},
{
"from": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d",
"to": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a"
},
{
"from": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d",
"to": "enode://9947aaf751f83384eada5a358c679a322e26c79051097eea44164c9d5d6da2c9984b61d865a777b250fabcb1cf31a835045cf132fadb43e067641914c2991265"
},
{
"from": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d",
"to": "enode://f17f5ce80833211c6a93762f2ee55bf97115dd3f7b743b2ab7e9c3d7962defca70f3b5e4a8b96fa46d5a33f4319140cf2ff0ab85939a62d4cbf1b69c65d53946"
},
{
"from": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a",
"to": "enode://2db289c17757f863d2d833a4d6641cd1a096dd0d392e356e3c4b6fed73382f9451b1fb23ff25f8c99e76bdb587428358817ab4969d358b24a126fa59c8bd98af"
},
{
"from": "enode://c9902d1a77c0d51adf2b94c6d14f7dd10ad732b18986a221464563758b9b9a04fe159b75cdbf777196b5f90e75f0f04513e018a2f7622e4fcadd45a6b9e2cc9a",
"to": "enode://6833ef592ba6ac03d9797fd757467909db84ce832f749c060b29a4d4e0b82cd6865e4bc340cd73462a39677142656e7999eaff7aecc4746703d3350b50c15ebd"
},
{
"from": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd",
"to": "enode://7eb662ba026270024a625d9df71b5a466f61a303dff47ae61cccc3d18550ecc2731f2ea2d768f121da4e7cd7fc4f9ad30ab278e981a2e9c2a0d7d6ee0430cfe2"
},
{
"from": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd",
"to": "enode://b270650132ec89eda0e0addac435d1074007818291cc25510d332752f81901d161777e80e138f5b25028660e9af05983f77bc240aa7628a4cf487131910c64b3"
},
{
"from": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd",
"to": "enode://5b94644c474fef835f68bca9460b79daa0d44101b581f159ca4de594f4e4d4b7f701f97fb5713267bd89b22df351ead20a1587dd604d5c6f1c3ebddf3f7f48c4"
},
{
"from": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd",
"to": "enode://520b57f167660f2b4004866b848fe1015d8b5d7d98dff0207a7e4925478c937eaa1d0e07994be75d5023d18f6fc7633562d0f69b320835726d3c24645140b715"
},
{
"from": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd",
"to": "enode://cd29456310d3526660cf3895bceeac648dc02ed5496f4c0e2f7332c4faf1a006d1b89fcf1d3761b3d343dc8cde605b598ddab8c1b7f003ad44d05b2f90c680e2"
},
{
"from": "enode://9b246088fd8224ce44a106068a6aa4ca1e17433443babe41905e3d07d89df4685359de20c6115ff253e3a38d0ed0954c50afe60cdf970930d05037313db17afd",
"to": "enode://7c9adbfec6f76c7efb599db2300aded9a35efc616f3f4aeb1a20dc54eb4d238ed48c3b79273eff264578bd2a1fc583afd8254841c246c55dfe7fe851986fa77d"
}
]
}

View File

@@ -0,0 +1,55 @@
package snapshot
import (
"fmt"
"testing"
"github.com/ethersphere/swarm/simulation"
)
func TestDockerSnapshotFromFile(t *testing.T) {
snap, err := simulation.LoadSnapshotFromFile("docker.json")
if err != nil {
t.Fatal(err)
}
if !simulation.IsDockerAvailable(snap.DefaultAdapter.Config.(simulation.DockerAdapterConfig).DaemonAddr) {
t.Skip("docker is not available, skipping test")
}
sim, err := simulation.NewSimulationFromSnapshot(snap)
if err != nil {
t.Fatal(err)
}
defer func() {
err = sim.StopAll()
if err != nil {
t.Error(err)
}
}()
nodes := sim.GetAll()
if len(nodes) != len(snap.Nodes) {
t.Fatalf("Got %d . Expected %d nodes", len(nodes), len(snap.Nodes))
}
// Check hive output on the first node
node, err := sim.Get(simulation.NodeID("test-0"))
if err != nil {
t.Error(err)
}
client, err := sim.RPCClient(node.Info().ID)
if err != nil {
t.Errorf("Failed to get rpc client: %v", err)
}
var hive string
err = client.Call(&hive, "bzz_hive")
if err != nil {
t.Errorf("could not get hive info: %v", err)
}
fmt.Println(hive)
}

View File

@@ -0,0 +1 @@
package snapshot

228
simulation/exec.go Normal file
View File

@@ -0,0 +1,228 @@
package simulation
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
"time"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/swarm"
)
// ExecAdapter can manage local exec nodes
type ExecAdapter struct {
config ExecAdapterConfig
}
// ExecAdapterConfig is used to configure an ExecAdapter
type ExecAdapterConfig struct {
// Path to the executable
ExecutablePath string `json:"executable"`
// BaseDataDirectory stores all the nodes' data directories
BaseDataDirectory string `json:"basedir"`
}
// ExecNode is a node that is executed locally
type ExecNode struct {
adapter *ExecAdapter
config NodeConfig
cmd *exec.Cmd
info NodeInfo
}
// NewExecAdapter creates an ExecAdapter by receiving a ExecAdapterConfig
func NewExecAdapter(config ExecAdapterConfig) (*ExecAdapter, error) {
if _, err := os.Stat(config.BaseDataDirectory); os.IsNotExist(err) {
return nil, fmt.Errorf("'%s' directory does not exist", config.BaseDataDirectory)
}
if _, err := os.Stat(config.ExecutablePath); os.IsNotExist(err) {
return nil, fmt.Errorf("'%s' executable does not exist", config.ExecutablePath)
}
absExec, err := filepath.Abs(config.ExecutablePath)
if err != nil {
return nil, fmt.Errorf("could not get absolute path for %s: %v", config.ExecutablePath, err)
}
config.ExecutablePath = absExec
absDir, err := filepath.Abs(config.BaseDataDirectory)
if err != nil {
return nil, fmt.Errorf("could not get absolute path for %s: %v", config.BaseDataDirectory, err)
}
config.BaseDataDirectory = absDir
a := &ExecAdapter{
config: config,
}
return a, nil
}
// NewNode creates a new node
func (a ExecAdapter) NewNode(config NodeConfig) Node {
info := NodeInfo{
ID: config.ID,
}
node := &ExecNode{
config: config,
adapter: &a,
info: info,
}
return node
}
// Snapshot returns a snapshot of the adapter
func (a ExecAdapter) Snapshot() AdapterSnapshot {
return AdapterSnapshot{
Type: "exec",
Config: a.config,
}
}
// Info returns the node info
func (n *ExecNode) Info() NodeInfo {
return n.info
}
// Start starts the node
func (n *ExecNode) Start() error {
// Check if command already exists
if n.cmd != nil {
return fmt.Errorf("node %s is already running", n.config.ID)
}
// Create command line arguments
args := []string{filepath.Base(n.adapter.config.ExecutablePath)}
// Create data directory for this node
dir := n.dataDir()
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create node directory: %s", err)
}
// Configure data directory
args = append(args, "--datadir", dir)
// Configure IPC path
args = append(args, "--ipcpath", n.ipcPath())
// Automatically allocate ports
args = append(args, "--pprofport", "0")
args = append(args, "--bzzport", "0")
args = append(args, "--wsport", "0")
args = append(args, "--port", "0")
// Append user defined arguments
args = append(args, n.config.Args...)
// Start command
n.cmd = &exec.Cmd{
Path: n.adapter.config.ExecutablePath,
Args: args,
Dir: dir,
Env: n.config.Env,
Stdout: n.config.Stdout,
Stderr: n.config.Stderr,
}
if err := n.cmd.Start(); err != nil {
n.cmd = nil
return fmt.Errorf("error starting node %s: %s", n.config.ID, err)
}
// Wait for the node to start
var client *rpc.Client
var err error
defer func() {
if err != nil {
n.Stop()
}
}()
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
client, err = rpc.Dial(n.ipcPath())
if err == nil {
break
}
}
if client == nil {
return fmt.Errorf("could not establish rpc connection. node %s: %v", n.config.ID, err)
}
defer client.Close()
var swarminfo swarm.Info
err = client.Call(&swarminfo, "bzz_info")
if err != nil {
return fmt.Errorf("could not get info via rpc call. node %s: %v", n.config.ID, err)
}
var p2pinfo p2p.NodeInfo
err = client.Call(&p2pinfo, "admin_nodeInfo")
if err != nil {
return fmt.Errorf("could not get info via rpc call. node %s: %v", n.config.ID, err)
}
n.info = NodeInfo{
ID: n.config.ID,
Enode: p2pinfo.Enode,
BzzAddr: swarminfo.BzzKey,
RPCListen: n.ipcPath(),
HTTPListen: fmt.Sprintf("http://localhost:%s", swarminfo.Port),
}
return nil
}
// Stop stops the node
func (n *ExecNode) Stop() error {
if n.cmd == nil {
return nil
}
defer func() {
n.cmd = nil
}()
// Try to gracefully terminate the process
if err := n.cmd.Process.Signal(syscall.SIGTERM); err != nil {
return n.cmd.Process.Kill()
}
// Wait for the process to terminate or timeout
waitErr := make(chan error)
go func() {
waitErr <- n.cmd.Wait()
}()
select {
case err := <-waitErr:
return err
case <-time.After(20 * time.Second):
return n.cmd.Process.Kill()
}
}
// Snapshot returns a snapshot of the node
func (n *ExecNode) Snapshot() (NodeSnapshot, error) {
snap := NodeSnapshot{
Config: n.config,
}
adapterSnap := n.adapter.Snapshot()
snap.Adapter = &adapterSnap
return snap, nil
}
// ipcPath returns the path to the ipc socket
func (n *ExecNode) ipcPath() string {
ipcfile := "bzzd.ipc"
// On windows we can have to use pipes
if runtime.GOOS == "windows" {
return `\\.\pipe\` + ipcfile
}
return fmt.Sprintf("%s/%s", n.dataDir(), ipcfile)
}
// dataDir returns the path to the data directory that the node should use
func (n *ExecNode) dataDir() string {
return filepath.Join(n.adapter.config.BaseDataDirectory, string(n.config.ID))
}

94
simulation/exec_test.go Normal file
View File

@@ -0,0 +1,94 @@
package simulation
import (
"encoding/hex"
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/crypto"
)
func TestExecAdapter(t *testing.T) {
execPath := "../build/bin/swarm"
// Skip test if binary doesn't exist
if _, err := os.Stat(execPath); err != nil {
if os.IsNotExist(err) {
t.Skip("swarm binary not found. build it before running the test")
}
}
tmpdir, err := ioutil.TempDir("", "test-adapter-exec")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
adapter, err := NewExecAdapter(ExecAdapterConfig{
ExecutablePath: execPath,
BaseDataDirectory: tmpdir,
})
if err != nil {
t.Fatalf("could not create exec adapter: %v", err)
}
bzzkey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("could not generate key: %v", err)
}
bzzkeyhex := hex.EncodeToString(crypto.FromECDSA(bzzkey))
nodekey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("could not generate key: %v", err)
}
nodekeyhex := hex.EncodeToString(crypto.FromECDSA(nodekey))
args := []string{
"--bootnodes", "",
"--bzzkeyhex", bzzkeyhex,
"--nodekeyhex", nodekeyhex,
"--bzznetworkid", "499",
}
nodeconfig := NodeConfig{
ID: "node1",
Args: args,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
node := adapter.NewNode(nodeconfig)
info := node.Info()
if info.ID != "node1" {
t.Fatal("node id is different")
}
err = node.Start()
if err != nil {
t.Fatalf("node did not start: %v", err)
}
infoA := node.Info()
err = node.Stop()
if err != nil {
t.Fatalf("node didn't stop: %v", err)
}
err = node.Start()
if err != nil {
t.Fatalf("node didn't start again: %v", err)
}
infoB := node.Info()
if infoA.BzzAddr != infoB.BzzAddr {
t.Errorf("bzzaddr should be the same: %s - %s", infoA.Enode, infoB.Enode)
}
err = node.Stop()
if err != nil {
t.Fatalf("node didn't stop: %v", err)
}
}

467
simulation/kubernetes.go Normal file
View File

@@ -0,0 +1,467 @@
package simulation
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/swarm"
"github.com/ethersphere/swarm/log"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// KubernetesAdapter can manage nodes on a kubernetes cluster
type KubernetesAdapter struct {
client *kubernetes.Clientset
config KubernetesAdapterConfig
image string
proxy string
}
// KubernetesAdapterConfig is the configuration provided to a KubernetesAdapter
type KubernetesAdapterConfig struct {
// KubeConfigPath is the path to your kubernetes configuration path
KubeConfigPath string `json:"kubeConfigPath"`
// Namespace is the kubernetes namespaces where the pods should be running
Namespace string `json:"namespace"`
// BuildContext can be used to build a docker image
// from a Dockerfile and a context directory
BuildContext *KubernetesBuildContext `json:"build,omitempty"`
// DockerImage points to an existing docker image
// e.g. ethersphere/swarm:latest
DockerImage string `json:"image,omitempty"`
}
// KubernetesBuildContext defines the build context to build
// local docker images
type KubernetesBuildContext struct {
// Dockefile is the path to the dockerfile
Dockerfile string `json:"dockerfile"`
// Directory is the directory that will be used
// in the context of a docker build
Directory string `json:"dir"`
// Tag is used to tag the image
Tag string `json:"tag"`
// Registry is the image registry where the image will be pushed to
Registry string `json:"registry"`
// Username is the user used to push the image to the registry
Username string `json:"username"`
// Password is the password of the user that is used to push the image
// to the registry
Password string `json:"-"`
}
// ImageTag is the full image tag, including the registry
func (bc *KubernetesBuildContext) ImageTag() string {
return fmt.Sprintf("%s/%s", bc.Registry, bc.Tag)
}
// DefaultKubernetesAdapterConfig uses the default ~/.kube/config
// to discover the kubernetes clusters. It also uses the "default" namespace.
func DefaultKubernetesAdapterConfig() KubernetesAdapterConfig {
kubeconfig := filepath.Join(homeDir(), ".kube", "config")
return KubernetesAdapterConfig{
KubeConfigPath: kubeconfig,
Namespace: "default",
}
}
// IsKubernetesAvailable checks if a kubernetes configuration file exists
func IsKubernetesAvailable(kubeConfigPath string) bool {
k8scfg, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
return false
}
_, err = kubernetes.NewForConfig(k8scfg)
return err == nil
}
// NewKubernetesAdapter creates a KubernetesAdpater by receiving a KubernetesAdapterConfig
func NewKubernetesAdapter(config KubernetesAdapterConfig) (*KubernetesAdapter, error) {
if config.DockerImage != "" && config.BuildContext != nil {
return nil, fmt.Errorf("only one can be defined: BuildContext (%v) or DockerImage(%s)",
config.BuildContext, config.DockerImage)
}
if config.DockerImage == "" && config.BuildContext == nil {
return nil, errors.New("required: Dockerfile or DockerImage")
}
// Define k8s client configuration
k8scfg, err := clientcmd.BuildConfigFromFlags("", config.KubeConfigPath)
if err != nil {
return nil, fmt.Errorf("could not start k8s client from config: %v", err)
}
// Create the clientset
clientset, err := kubernetes.NewForConfig(k8scfg)
if err != nil {
return nil, fmt.Errorf("could not create clientset: %v", err)
}
// Figure out which docker image should be used
image := config.DockerImage
// Build and push container image
if config.BuildContext != nil {
var err error
// Build image
image, err = buildImage(DockerBuildContext{
Dockerfile: config.BuildContext.Dockerfile,
Directory: config.BuildContext.Directory,
Tag: config.BuildContext.ImageTag(),
}, DefaultDockerAdapterConfig().DaemonAddr)
if err != nil {
return nil, fmt.Errorf("could not build the docker image: %v", err)
}
// Push image
dockerClient, err := client.NewClientWithOpts(
client.WithHost(client.DefaultDockerHost),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return nil, fmt.Errorf("could not create the docker client: %v", err)
}
authConfig := types.AuthConfig{
Username: config.BuildContext.Username,
Password: config.BuildContext.Password,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return nil, errors.New("failed marshaling the authentication parameters")
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
out, err := dockerClient.ImagePush(
context.Background(),
config.BuildContext.ImageTag(),
types.ImagePushOptions{
RegistryAuth: authStr,
})
if err != nil {
return nil, fmt.Errorf("failed to push image: %v", err)
}
defer out.Close()
if _, err := io.Copy(os.Stdout, out); err != nil && err != io.EOF {
log.Error("Error pushing docker image", "err", err)
}
}
// Setup proxy to access pods
server, err := newProxyServer(k8scfg)
if err != nil {
return nil, fmt.Errorf("failed to create proxy: %v", err)
}
l, err := server.Listen("127.0.0.1", 0)
if err != nil {
return nil, fmt.Errorf("failed to start proxy: %v", err)
}
go func() {
if err := server.ServeOnListener(l); err != nil {
log.Error("Kubernetes dapater proxy failed:", "err", err.Error())
}
}()
// Return adapter
return &KubernetesAdapter{
client: clientset,
image: image,
config: config,
proxy: l.Addr().String(),
}, nil
}
// NewNode creates a new node
func (a KubernetesAdapter) NewNode(config NodeConfig) Node {
info := NodeInfo{
ID: config.ID,
}
node := &KubernetesNode{
config: config,
adapter: &a,
info: info,
}
return node
}
// Snapshot returns a snapshot of the Adapter
func (a KubernetesAdapter) Snapshot() AdapterSnapshot {
return AdapterSnapshot{
Type: "kubernetes",
Config: a.config,
}
}
// KubernetesNode is a node that was started via the KubernetesAdapter
type KubernetesNode struct {
config NodeConfig
adapter *KubernetesAdapter
info NodeInfo
}
// Info returns the node info
func (n *KubernetesNode) Info() NodeInfo {
return n.info
}
// Start starts the node
func (n *KubernetesNode) Start() error {
// Define arguments
args := []string{}
// Append user defined arguments
args = append(args, n.config.Args...)
// Append network ports arguments
args = append(args, "--pprofport", strconv.Itoa(dockerPProfPort))
args = append(args, "--bzzport", strconv.Itoa(dockerHTTPPort))
args = append(args, "--ws")
// TODO: Can we get the APIs from somewhere instead of hardcoding them here?
args = append(args, "--wsapi", "admin,net,debug,bzz,accounting,hive")
args = append(args, "--wsport", strconv.Itoa(dockerWebsocketPort))
args = append(args, "--wsaddr", "0.0.0.0")
args = append(args, "--wsorigins", "*")
args = append(args, "--port", strconv.Itoa(dockerP2PPort))
args = append(args, "--nat", "ip:$(POD_IP)")
// Build environment variables
env := []v1.EnvVar{
{
// POD_IP is useful for setting the NAT config: e.g. `--nat ip:$POD_IP`
Name: "POD_IP",
ValueFrom: &v1.EnvVarSource{
FieldRef: &v1.ObjectFieldSelector{
FieldPath: "status.podIP",
},
},
},
}
for _, e := range n.config.Env {
var name, value string
s := strings.SplitN(e, "=", 1)
name = s[0]
if len(s) > 1 {
value = s[1]
}
env = append(env, v1.EnvVar{
Name: name,
Value: value,
})
}
adapter := n.adapter
// Create Kubernetes Pod
podRequest := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: n.podName(),
Labels: map[string]string{
"app": "simulation",
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: n.podName(),
Image: adapter.image,
Args: args,
Env: env,
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("400Mi"),
},
},
},
},
},
}
pod, err := adapter.client.CoreV1().Pods(adapter.config.Namespace).Create(podRequest)
if err != nil {
return fmt.Errorf("failed to create pod: %v", err)
}
// Wait for pod
start := time.Now()
for {
log.Debug("Waiting for pod", "pod", n.podName())
pod, err := adapter.client.CoreV1().Pods(adapter.config.Namespace).Get(n.podName(), metav1.GetOptions{})
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
if pod.Status.Phase == v1.PodRunning {
break
}
if time.Since(start) > 5*time.Minute {
return errors.New("timeout waiting for pod")
}
time.Sleep(500 * time.Millisecond)
}
// Get logs
logOpts := &v1.PodLogOptions{
Container: n.podName(),
Follow: true,
Previous: false,
}
req := adapter.client.CoreV1().Pods(adapter.config.Namespace).GetLogs(n.podName(), logOpts)
readCloser, err := req.Stream()
if err != nil {
return fmt.Errorf("could not get logs: %v", err)
}
go func() {
defer readCloser.Close()
if _, err := io.Copy(n.config.Stderr, readCloser); err != nil && err != io.EOF {
log.Error("Error writing pod logs", "pod", pod.Name, "err", err)
}
}()
// Wait for the node to start
var client *rpc.Client
wsAddr := fmt.Sprintf("ws://%s/api/v1/namespaces/%s/pods/%s:%d/proxy",
adapter.proxy, adapter.config.Namespace, n.podName(), dockerWebsocketPort)
for start := time.Now(); time.Since(start) < 30*time.Second; time.Sleep(50 * time.Millisecond) {
client, err = rpc.Dial(wsAddr)
if err == nil {
break
}
}
if client == nil {
return fmt.Errorf("could not establish rpc connection. node %s: %v", n.config.ID, err)
}
defer client.Close()
var swarminfo swarm.Info
err = client.Call(&swarminfo, "bzz_info")
if err != nil {
return fmt.Errorf("could not get info via rpc call. node %s: %v", n.config.ID, err)
}
var p2pinfo p2p.NodeInfo
err = client.Call(&p2pinfo, "admin_nodeInfo")
if err != nil {
return fmt.Errorf("could not get info via rpc call. node %s: %v", n.config.ID, err)
}
n.info = NodeInfo{
ID: n.config.ID,
Enode: p2pinfo.Enode,
BzzAddr: swarminfo.BzzKey,
RPCListen: wsAddr,
HTTPListen: fmt.Sprintf("http://%s/api/v1/namespaces/%s/pods/%s:%d/proxy",
adapter.proxy, adapter.config.Namespace, n.podName(), dockerHTTPPort),
PprofListen: fmt.Sprintf("http://%s/api/v1/namespaces/%s/pods/%s:%d/proxy",
adapter.proxy, adapter.config.Namespace, n.podName(), dockerPProfPort),
}
return nil
}
// Stop stops the node
func (n *KubernetesNode) Stop() error {
adapter := n.adapter
gracePeriod := int64(30)
deleteOpts := &metav1.DeleteOptions{
GracePeriodSeconds: &gracePeriod,
}
err := adapter.client.CoreV1().Pods(adapter.config.Namespace).Delete(n.podName(), deleteOpts)
if err != nil {
return fmt.Errorf("could not delete pod: %v", err)
}
return nil
}
// Snapshot returns a snapshot of the node
func (n *KubernetesNode) Snapshot() (NodeSnapshot, error) {
snap := NodeSnapshot{
Config: n.config,
}
adapterSnap := n.adapter.Snapshot()
snap.Adapter = &adapterSnap
return snap, nil
}
func (n *KubernetesNode) podName() string {
return fmt.Sprintf("sim-k8s-%s", n.config.ID)
}
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}
return os.Getenv("USERPROFILE") // windows
}
// proxyServer is a http.Handler which proxies Kubernetes APIs to remote API server.
type proxyServer struct {
handler http.Handler
}
// Listen is a simple wrapper around net.Listen.
func (s *proxyServer) Listen(address string, port int) (net.Listener, error) {
return net.Listen("tcp", fmt.Sprintf("%s:%d", address, port))
}
// ServeOnListener starts the server using given listener, loops forever.
func (s *proxyServer) ServeOnListener(l net.Listener) error {
server := http.Server{
Handler: s.handler,
}
return server.Serve(l)
}
func (s *proxyServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.handler.ServeHTTP(rw, req)
}
// newProxyServer creates a proxy server that can be used to proxy to the kubernetes API
func newProxyServer(cfg *rest.Config) (*proxyServer, error) {
target, err := url.Parse(cfg.Host)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(target)
transport, err := rest.TransportFor(cfg)
if err != nil {
return nil, err
}
proxy.Transport = transport
return &proxyServer{
handler: proxy,
}, nil
}

604
simulation/simulation.go Normal file
View File

@@ -0,0 +1,604 @@
package simulation
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"reflect"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/swarm/log"
"github.com/ethersphere/swarm/network"
"golang.org/x/sync/errgroup"
)
type nodeMap struct {
sync.RWMutex
internal map[NodeID]Node
}
func newNodeMap() *nodeMap {
return &nodeMap{
internal: make(map[NodeID]Node),
}
}
func (nm *nodeMap) Load(key NodeID) (value Node, ok bool) {
nm.RLock()
result, ok := nm.internal[key]
nm.RUnlock()
return result, ok
}
func (nm *nodeMap) LoadAll() []Node {
nm.RLock()
v := []Node{}
for _, node := range nm.internal {
v = append(v, node)
}
nm.RUnlock()
return v
}
func (nm *nodeMap) Store(key NodeID, value Node) {
nm.Lock()
nm.internal[key] = value
nm.Unlock()
}
// Simulation is used to simulate a network of nodes
type Simulation struct {
adapter Adapter
nodes *nodeMap
}
// NewSimulation creates a new simulation given an adapter
func NewSimulation(adapter Adapter) *Simulation {
sim := &Simulation{
adapter: adapter,
nodes: newNodeMap(),
}
return sim
}
func getAdapterFromSnapshotConfig(snapshot *AdapterSnapshot) (Adapter, error) {
if snapshot == nil {
return nil, errors.New("snapshot can't be nil")
}
var adapter Adapter
var err error
switch t := snapshot.Type; t {
case "exec":
adapter, err = NewExecAdapter(snapshot.Config.(ExecAdapterConfig))
case "docker":
adapter, err = NewDockerAdapter(snapshot.Config.(DockerAdapterConfig))
case "kubernetes":
adapter, err = NewKubernetesAdapter(snapshot.Config.(KubernetesAdapterConfig))
default:
return nil, fmt.Errorf("unknown adapter type: %s", t)
}
if err != nil {
return nil, fmt.Errorf("could not initialize %s adapter: %v", snapshot.Type, err)
}
return adapter, nil
}
// NewSimulationFromSnapshot creates a simulation from a snapshot
func NewSimulationFromSnapshot(snapshot *Snapshot) (*Simulation, error) {
// Create adapter
adapter, err := getAdapterFromSnapshotConfig(snapshot.DefaultAdapter)
if err != nil {
return nil, err
}
sim := &Simulation{
adapter: adapter,
nodes: newNodeMap(),
}
// Loop over nodes and add them
for _, n := range snapshot.Nodes {
if n.Adapter == nil {
if err := sim.Init(n.Config); err != nil {
return sim, fmt.Errorf("failed to initialize node %v", err)
}
} else {
adapter, err := getAdapterFromSnapshotConfig(n.Adapter)
if err != nil {
return sim, fmt.Errorf("could not read adapter configureation for node %s: %v", n.Config.ID, err)
}
if err := sim.InitWithAdapter(n.Config, adapter); err != nil {
return sim, fmt.Errorf("failed to initialize node %s: %v", n.Config.ID, err)
}
}
}
// Start all nodes
err = sim.StartAll()
if err != nil {
return sim, err
}
// Establish connections
m := make(map[string]Node)
for _, n := range sim.GetAll() {
enode := removeNetworkAddressFromEnode(n.Info().Enode)
m[enode] = n
}
for _, con := range snapshot.Connections {
from, ok := m[con.From]
if !ok {
return sim, fmt.Errorf("no node found with enode: %s", con.From)
}
to, ok := m[con.To]
if !ok {
return sim, fmt.Errorf("no node found with enode: %s", con.To)
}
client, err := sim.RPCClient(from.Info().ID)
if err != nil {
return sim, err
}
defer client.Close()
if err := client.Call(nil, "admin_addPeer", to.Info().Enode); err != nil {
return sim, err
}
}
return sim, nil
}
func (s *AdapterSnapshot) detectConfigurationType() error {
adapterconfig, err := json.Marshal(s.Config)
if err != nil {
return err
}
switch t := s.Type; t {
case "exec":
var config ExecAdapterConfig
err := json.Unmarshal(adapterconfig, &config)
if err != nil {
return err
}
s.Config = config
case "docker":
var config DockerAdapterConfig
err := json.Unmarshal(adapterconfig, &config)
if err != nil {
return err
}
s.Config = config
case "kubernetes":
var config KubernetesAdapterConfig
err := json.Unmarshal(adapterconfig, &config)
if err != nil {
return err
}
s.Config = config
default:
return fmt.Errorf("unknown adapter type: %s", t)
}
return nil
}
func unmarshalSnapshot(data []byte, snapshot *Snapshot) error {
err := json.Unmarshal(data, snapshot)
if err != nil {
return err
}
// snapshot.Adapter.Config will be of type map[string]interface{}
// we have to unmarshal it to the correct adapter configuration struct
if err := snapshot.DefaultAdapter.detectConfigurationType(); err != nil {
return err
}
for _, n := range snapshot.Nodes {
if n.Adapter != nil {
if err := n.Adapter.detectConfigurationType(); err != nil {
return err
}
}
}
return nil
}
// LoadSnapshotFromFile loads a snapshot from a given JSON file
func LoadSnapshotFromFile(filePath string) (*Snapshot, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
bytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
var snapshot Snapshot
err = unmarshalSnapshot(bytes, &snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
// Get returns a node by ID
func (s *Simulation) Get(id NodeID) (Node, error) {
node, ok := s.nodes.Load(id)
if !ok {
return nil, fmt.Errorf("a node with id %s does not exist", id)
}
return node, nil
}
// GetAll returns all nodes
func (s *Simulation) GetAll() []Node {
return s.nodes.LoadAll()
}
// DefaultAdapter returns the default adapter that the simulation was initialized with
func (s *Simulation) DefaultAdapter() Adapter {
return s.adapter
}
// Init initializes a node with the NodeConfig with the default Adapter
func (s *Simulation) Init(config NodeConfig) error {
return s.InitWithAdapter(config, s.DefaultAdapter())
}
// InitWithAdapter initializes a node with the NodeConfig and the given Adapter
func (s *Simulation) InitWithAdapter(config NodeConfig, adapter Adapter) error {
if _, ok := s.nodes.Load(config.ID); ok {
return fmt.Errorf("a node with id %s already exists", config.ID)
}
node := adapter.NewNode(config)
s.nodes.Store(config.ID, node)
return nil
}
// Start starts a node by ID
func (s *Simulation) Start(id NodeID) error {
node, ok := s.nodes.Load(id)
if !ok {
return fmt.Errorf("a node with id %s does not exist", id)
}
if err := node.Start(); err != nil {
return fmt.Errorf("could not start node: %v", err)
}
return nil
}
// Stop stops a node by ID
func (s *Simulation) Stop(id NodeID) error {
node, ok := s.nodes.Load(id)
if !ok {
return fmt.Errorf("a node with id %s does not exist", id)
}
if err := node.Stop(); err != nil {
return fmt.Errorf("could not stop node: %v", err)
}
return nil
}
// StartAll starts all nodes
func (s *Simulation) StartAll() error {
g, _ := errgroup.WithContext(context.Background())
for _, node := range s.nodes.LoadAll() {
g.Go(node.Start)
}
return g.Wait()
}
// StopAll stops all nodes
func (s *Simulation) StopAll() error {
g, _ := errgroup.WithContext(context.Background())
for _, node := range s.nodes.LoadAll() {
g.Go(node.Stop)
}
return g.Wait()
}
// RPCClient returns an RPC Client for a given node
func (s *Simulation) RPCClient(id NodeID) (*rpc.Client, error) {
node, ok := s.nodes.Load(id)
if !ok {
return nil, fmt.Errorf("a node with id %s does not exist", id)
}
info := node.Info()
var client *rpc.Client
var err error
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
client, err = rpc.Dial(info.RPCListen)
if err == nil {
break
}
}
if client == nil {
return nil, fmt.Errorf("could not establish rpc connection: %v", err)
}
return client, nil
}
// HTTPBaseAddr returns the address for the HTTP API
func (s *Simulation) HTTPBaseAddr(id NodeID) (string, error) {
node, ok := s.nodes.Load(id)
if !ok {
return "", fmt.Errorf("a node with id %s does not exist", id)
}
info := node.Info()
return info.HTTPListen, nil
}
// Snapshot returns a snapshot of the simulation
func (s *Simulation) Snapshot() (*Snapshot, error) {
snap := Snapshot{}
// Default adapter snapshot
asnap := s.DefaultAdapter().Snapshot()
snap.DefaultAdapter = &asnap
// Nodes snapshot
nodes := s.GetAll()
snap.Nodes = make([]NodeSnapshot, len(nodes))
snap.Connections = []ConnectionSnapshot{}
for idx, n := range nodes {
ns, err := n.Snapshot()
if err != nil {
return nil, fmt.Errorf("failed to get nodes snapshot %s: %v", n.Info().ID, err)
}
// Don't need to specify the node's adapter snapshot if it's
// the same as the default adapters snapshot
if reflect.DeepEqual(asnap, *ns.Adapter) {
ns.Adapter = nil
}
snap.Nodes[idx] = ns
// Get connections
client, err := s.RPCClient(n.Info().ID)
if err != nil {
return nil, err
}
defer client.Close()
var peers []*p2p.PeerInfo
err = client.Call(&peers, "admin_peers")
if err != nil {
return nil, err
}
for _, p := range peers {
// Only care about outbound connections
if !p.Network.Inbound {
snap.Connections = append(snap.Connections, ConnectionSnapshot{
// we need to remove network addresses from enodes
// because they will change between simulations
From: removeNetworkAddressFromEnode(n.Info().Enode),
To: removeNetworkAddressFromEnode(p.Enode),
})
}
}
}
return &snap, nil
}
// AddBootnode adds and starts a bootnode with the given id and arguments
func (s *Simulation) AddBootnode(id NodeID, args []string) (Node, error) {
a := []string{
"--bootnode-mode",
"--bootnodes", "",
}
a = append(a, args...)
return s.AddNode(id, a)
}
// AddNode adds and starts a node with the given id and arguments
func (s *Simulation) AddNode(id NodeID, args []string) (Node, error) {
bzzkey, err := randomHexKey()
if err != nil {
return nil, err
}
nodekey, err := randomHexKey()
if err != nil {
return nil, err
}
a := []string{
"--bzzkeyhex", bzzkey,
"--nodekeyhex", nodekey,
}
a = append(a, args...)
cfg := NodeConfig{
ID: id,
Args: a,
// TODO: Figure out how to handle logs when using AddNode(...)
Stdout: ioutil.Discard,
Stderr: ioutil.Discard,
}
err = s.Init(cfg)
if err != nil {
return nil, err
}
err = s.Start(id)
if err != nil {
return nil, err
}
node, err := s.Get(id)
if err != nil {
return nil, err
}
return node, nil
}
// AddNodes adds and starts 'count' nodes with a given ID prefix, arguments.
// If the idPrefix is "node" and count is 3 then the following nodes will be
// created: node-0, node-1, node-2
func (s *Simulation) AddNodes(idPrefix string, count int, args []string) ([]Node, error) {
g, _ := errgroup.WithContext(context.Background())
idFormat := "%s-%d"
for i := 0; i < count; i++ {
id := NodeID(fmt.Sprintf(idFormat, idPrefix, i))
g.Go(func() error {
node, err := s.AddNode(id, args)
if err != nil {
log.Warn("Failed to add node", "id", id, "err", err.Error())
} else {
log.Info("Added node", "id", id, "enode", node.Info().Enode)
}
return err
})
}
err := g.Wait()
if err != nil {
return nil, err
}
nodes := make([]Node, count)
for i := 0; i < count; i++ {
id := NodeID(fmt.Sprintf(idFormat, idPrefix, i))
nodes[i], err = s.Get(id)
if err != nil {
return nil, err
}
}
return nodes, nil
}
// CreateClusterWithBootnode adds and starts a bootnode. Afterwards it will add and start 'count' nodes that connect
// to the bootnode. All nodes can be provided by custom arguments.
// If the idPrefix is "node" and count is 3 then you will have the following nodes created:
// node-bootnode, node-0, node-1, node-2.
// The bootnode will be the first node on the returned Node slice.
func (s *Simulation) CreateClusterWithBootnode(idPrefix string, count int, args []string) ([]Node, error) {
bootnode, err := s.AddBootnode(NodeID(fmt.Sprintf("%s-bootnode", idPrefix)), args)
if err != nil {
return nil, err
}
nodeArgs := []string{
"--bootnodes", bootnode.Info().Enode,
}
nodeArgs = append(nodeArgs, args...)
n, err := s.AddNodes(idPrefix, count, nodeArgs)
if err != nil {
return nil, err
}
nodes := []Node{bootnode}
nodes = append(nodes, n...)
return nodes, nil
}
// WaitForHealthyNetwork will block until all the nodes are considered
// to have a healthy kademlia table
func (s *Simulation) WaitForHealthyNetwork() error {
nodes := s.GetAll()
// Generate RPC clients
var clients struct {
RPC []*rpc.Client
mu sync.Mutex
}
clients.RPC = make([]*rpc.Client, len(nodes))
g, _ := errgroup.WithContext(context.Background())
for idx, node := range nodes {
node := node
idx := idx
g.Go(func() error {
id := node.Info().ID
client, err := s.RPCClient(id)
if err != nil {
return err
}
clients.mu.Lock()
clients.RPC[idx] = client
clients.mu.Unlock()
return nil
})
}
if err := g.Wait(); err != nil {
return err
}
for _, c := range clients.RPC {
defer c.Close()
}
// Generate addresses for PotMap
addrs := [][]byte{}
for _, node := range nodes {
byteaddr, err := hexutil.Decode(node.Info().BzzAddr)
if err != nil {
return err
}
addrs = append(addrs, byteaddr)
}
ppmap := network.NewPeerPotMap(network.NewKadParams().NeighbourhoodSize, addrs)
log.Info("Waiting for healthy kademlia...")
// Check for healthInfo on all nodes
for {
g, _ = errgroup.WithContext(context.Background())
for i := 0; i < len(nodes)-1; i++ {
i := i
g.Go(func() error {
log.Debug("Checking hive_getHealthInfo", "node", nodes[i].Info().ID)
healthy := &network.Health{}
if err := clients.RPC[i].Call(&healthy, "hive_getHealthInfo", ppmap[nodes[i].Info().BzzAddr[2:]]); err != nil {
return err
}
if !healthy.Healthy() {
return fmt.Errorf("node %s is not healthy", nodes[i].Info().ID)
}
return nil
})
}
err := g.Wait()
if err == nil {
break
}
log.Info("Not healthy yet...", "msg", err.Error())
time.Sleep(500 * time.Millisecond)
}
log.Info("Healthy kademlia on all nodes")
return nil
}
func randomHexKey() (string, error) {
key, err := crypto.GenerateKey()
if err != nil {
return "", err
}
keyhex := hex.EncodeToString(crypto.FromECDSA(key))
return keyhex, nil
}
func removeNetworkAddressFromEnode(enode string) string {
if idx := strings.Index(enode, "@"); idx != -1 {
return enode[:idx]
}
return enode
}

81
simulation/types.go Normal file
View File

@@ -0,0 +1,81 @@
package simulation
import (
"io"
)
// Node is a node within a simulation
type Node interface {
Info() NodeInfo
// Start starts the node
Start() error
// Stop stops the node
Stop() error
// Snapshot returns a snapshot of the node
Snapshot() (NodeSnapshot, error)
}
// Adapter can handle Node creation
type Adapter interface {
// NewNode creates a new node based on the NodeConfig
NewNode(config NodeConfig) Node
// Snapshot returns a snapshot of the adapter
Snapshot() AdapterSnapshot
}
// NodeID is the node identifier within a simulation. This can be an arbitrary string.
type NodeID string
// NodeConfig is the configuration of a specific node
type NodeConfig struct {
// Arbitrary string used to identify a node
ID NodeID `json:"id"`
// Command line arguments
Args []string `json:"args"`
// Environment variables
Env []string `json:"env,omitempty"`
// Stdout and Stderr specify the nodes' standard output and error
Stdout io.Writer `json:"-"`
Stderr io.Writer `json:"-"`
}
// NodeInfo contains the nodes information and connections strings
type NodeInfo struct {
ID NodeID
Enode string
BzzAddr string
RPCListen string // RPC listener address. Should be a valid ipc or websocket path
HTTPListen string // HTTP listener address: e.g. http://localhost:8500
PprofListen string // PProf listener address: e.g http://localhost:6060
}
// Snapshot is a snapshot of a simulation. It contains snapshots of:
// - the default adapter that the simulation was initialized with
// - the list of nodes that were created within the simulation
// - the list of connections between nodes
type Snapshot struct {
DefaultAdapter *AdapterSnapshot `json:"defaultAdapter"`
Nodes []NodeSnapshot `json:"nodes"`
Connections []ConnectionSnapshot `json:"connections"`
}
// NodeSnapshot is a snapshot of the node, it contains the node configuration and an adapter snapshot
type NodeSnapshot struct {
Config NodeConfig `json:"config"`
Adapter *AdapterSnapshot `json:"adapter,omitempty"`
}
// ConnectionSnapshot is a snapshot of a connection between peers
type ConnectionSnapshot struct {
From string `json:"from"`
To string `json:"to"`
}
// AdapterSnapshot is a snapshot of the configuration of an adapter
// - The type can be an arbitrary strings, e.g. "exec", "docker", etc.
// - The config will depend on the type, as every adapter has different configuration options
type AdapterSnapshot struct {
Type string `json:"type"`
Config interface{} `json:"config"`
}