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:
416
simulation/docker.go
Normal file
416
simulation/docker.go
Normal 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
53
simulation/docker_test.go
Normal 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(),
|
||||
)
|
||||
}
|
1
simulation/examples/cluster/cluster.go
Normal file
1
simulation/examples/cluster/cluster.go
Normal file
@@ -0,0 +1 @@
|
||||
package cluster
|
132
simulation/examples/cluster/cluster_test.go
Normal file
132
simulation/examples/cluster/cluster_test.go
Normal 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)
|
||||
}
|
641
simulation/examples/snapshot/docker.json
Normal file
641
simulation/examples/snapshot/docker.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
55
simulation/examples/snapshot/docker_test.go
Normal file
55
simulation/examples/snapshot/docker_test.go
Normal 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)
|
||||
}
|
1
simulation/examples/snapshot/snapshot.go
Normal file
1
simulation/examples/snapshot/snapshot.go
Normal file
@@ -0,0 +1 @@
|
||||
package snapshot
|
228
simulation/exec.go
Normal file
228
simulation/exec.go
Normal 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
94
simulation/exec_test.go
Normal 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
467
simulation/kubernetes.go
Normal 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
604
simulation/simulation.go
Normal 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
81
simulation/types.go
Normal 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"`
|
||||
}
|
Reference in New Issue
Block a user