* 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
229 lines
5.4 KiB
Go
229 lines
5.4 KiB
Go
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))
|
|
}
|