cmd/swarm/swarm-snapshot: add binary to create network snapshots

This commit is contained in:
Elad Nachmias 2018-12-17 18:24:09 +05:30
parent 1636d9574b
commit bb8c0ac22c
12 changed files with 607 additions and 37 deletions

View file

@ -0,0 +1,165 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethereum/go-ethereum/swarm/network"
"github.com/ethereum/go-ethereum/swarm/network/simulation"
cli "gopkg.in/urfave/cli.v1"
)
const testMinProxBinSize = 2
const NoConnectionTimeout = 2 * time.Second
func create(ctx *cli.Context) error {
log.PrintOrigins(true)
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(verbosity), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
if len(ctx.Args()) < 1 {
return errors.New("argument should be the filename to verify or write-to")
}
filename, err := touchPath(ctx.Args()[0])
if err != nil {
return err
}
err = discoverySnapshot(filename, 10)
if err != nil {
utils.Fatalf("Simulation failed: %s", err)
}
return err
}
func discoverySnapshot(filename string, nodes int) error {
//disable discovery if topology is specified
discovery = topology == ""
log.Debug("discoverySnapshot", "filename", filename, "nodes", nodes, "discovery", discovery)
i := 0
var lock sync.Mutex
var pivotNodeID enode.ID
sim := simulation.New(map[string]simulation.ServiceFunc{
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
lock.Lock()
i++
if i == pivot {
pivotNodeID = ctx.Config.ID
}
lock.Unlock()
addr := network.NewAddr(ctx.Config.Node())
kp := network.NewKadParams()
kp.MinProxBinSize = testMinProxBinSize
kad := network.NewKademlia(addr.Over(), kp)
hp := network.NewHiveParams()
hp.KeepAliveInterval = time.Duration(200) * time.Millisecond
hp.Discovery = discovery
config := &network.BzzConfig{
OverlayAddr: addr.Over(),
UnderlayAddr: addr.Under(),
HiveParams: hp,
}
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
},
})
defer sim.Close()
_, err := sim.AddNodes(10)
if err != nil {
utils.Fatalf("%v", err)
}
events := make(chan *simulations.Event)
sub := sim.Net.Events().Subscribe(events)
select {
case ev := <-events:
//only catch node up events
if ev.Type == simulations.EventTypeConn {
utils.Fatalf("this shouldn't happen as connections weren't initiated yet")
}
case <-time.After(NoConnectionTimeout):
}
sub.Unsubscribe()
if len(sim.Net.Conns) > 0 {
utils.Fatalf("no connections should exist after just adding nodes")
}
err := sim.Net.ConnectNodesRing(nil)
if err != nil {
utils.Fatalf("had an error connecting the nodes in a %v topology: %v", topology, err)
}
if discovery {
ctx, cancelSimRun := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancelSimRun()
if _, err := sim.WaitTillHealthy(ctx, 2); err != nil {
utils.Fatalf("%v", err)
}
}
var snap *simulations.Snapshot
if len(services) > 0 {
var addServices []string
var removeServices []string
for _, osvc := range strings.Split(services, ",") {
if strings.Index(osvc, "+") == 0 {
addServices = append(addServices, osvc[1:])
} else if strings.Index(osvc, "-") == 0 {
removeServices = append(removeServices, osvc[1:])
} else {
panic("stick to the rules, you know what they are")
}
}
snap, err = sim.Net.SnapshotWithServices(addServices, removeServices)
} else {
snap, err = sim.Net.Snapshot()
}
if err != nil {
return errors.New("no shapshot dude")
}
jsonsnapshot, err := json.Marshal(snap)
if err != nil {
return fmt.Errorf("corrupt json snapshot: %v", err)
}
err = ioutil.WriteFile(filename, jsonsnapshot, 0666)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,75 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/log"
)
func init() {
log.PrintOrigins(true)
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(verbosity), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
}
//TestSnapshotCreate is a high level e2e test that tests for snapshot generation
func TestSnapshotCreate(t *testing.T) {
for _, v := range []struct {
name string
args []string
}{
{
name: "no topology - discovery enabled",
args: []string{
"c",
},
},
{
name: "yes topology - discovery disabled",
args: []string{
"--topology",
"ring",
"c",
},
},
} {
t.Run(v.name, func(t *testing.T) {
file, err := ioutil.TempFile("", "swarm-snapshot")
defer os.Remove(file.Name())
if err != nil {
t.Fatal(err)
}
file.Close()
snap := runSnapshot(t, append(v.args, file.Name())...)
snap.ExpectExit()
if snap.ExitStatus() != 0 {
t.Fatal("expected exit code 0")
}
_, err = os.Stat(file.Name())
if err != nil {
t.Fatal("could not stat snapshot json")
}
})
}
}

View file

@ -0,0 +1,40 @@
package main
import (
"os"
"path"
"path/filepath"
)
func touchPath(filename string) (string, error) {
if path.IsAbs(filename) {
if _, err := os.Stat(filename); err == nil {
// path exists, we will override the file
return filename, nil
}
}
d, f := path.Split(filename)
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return "", err
}
_, err = os.Stat(path.Join(dir, filename))
if err == nil {
// path exists, we will override
return filename, nil
}
dirPath := path.Join(dir, d)
filePath := path.Join(dirPath, f)
if d != "" {
err = os.MkdirAll(dirPath, os.ModeDir)
if err != nil {
return "", err
}
}
filename = filePath
return filename, nil
}

View file

@ -0,0 +1,110 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"os"
"sort"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/log"
cli "gopkg.in/urfave/cli.v1"
)
var (
gitCommit string // Git SHA1 commit hash of the release (set via linker flags)
)
var (
topology string
services string
pivot int
nodes int
verbosity int
)
var app = utils.NewApp("", "Swarm Snapshot Util")
var discovery = true
func init() {
log.PrintOrigins(true)
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(verbosity), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
app.Name = "swarm-snapshot"
app.Usage = ""
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "topology",
Value: "chain",
Usage: "the desired topology to connect the nodes in (star, ring, chain, full)",
Destination: &topology,
},
cli.IntFlag{
Name: "pivot",
Value: 0,
Usage: "pivot node zero-index",
Destination: &pivot,
},
cli.IntFlag{
Name: "nodes",
Value: 10,
Usage: "swarm nodes",
Destination: &nodes,
},
cli.IntFlag{
Name: "verbosity",
Value: 1,
Usage: "verbosity",
Destination: &verbosity,
},
cli.StringFlag{
Name: "services",
Value: "",
Usage: "comma separated list of services to boot the nodes with",
Destination: &services,
},
}
app.Commands = []cli.Command{
{
Name: "create",
Aliases: []string{"c"},
Usage: "create a swarm snapshot",
Action: create,
},
{
Name: "verify",
Aliases: []string{"v"},
Usage: "verify a swarm snapshot",
Action: verify,
},
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
}
func main() {
err := app.Run(os.Args)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
}

View file

@ -0,0 +1,49 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"os"
"testing"
"github.com/docker/docker/pkg/reexec"
"github.com/ethereum/go-ethereum/internal/cmdtest"
)
func init() {
reexec.Register("swarm-snapshot", func() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(0)
})
}
func runSnapshot(t *testing.T, args ...string) *cmdtest.TestCmd {
tt := cmdtest.NewTestCmd(t, nil)
tt.Run("swarm-snapshot", args...)
return tt
}
func TestMain(m *testing.M) {
if reexec.Init() {
return
}
os.Exit(m.Run())
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,85 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"errors"
"sync"
"time"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethereum/go-ethereum/swarm/network"
"github.com/ethereum/go-ethereum/swarm/network/simulation"
cli "gopkg.in/urfave/cli.v1"
)
func verify(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
return errors.New("argument should be the filename to verify or write-to")
}
filename, err := touchPath(ctx.Args()[0])
if err != nil {
return err
}
err = verifySnapshot(filename)
if err != nil {
utils.Fatalf("Simulation failed: %s", err)
}
return err
}
func verifySnapshot(filename string) error {
sim := simulation.New(map[string]simulation.ServiceFunc{
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
addr := network.NewAddr(ctx.Config.Node())
kp := network.NewKadParams()
kp.MinProxBinSize = testMinProxBinSize
kad := network.NewKademlia(addr.Over(), kp)
hp := network.NewHiveParams()
hp.KeepAliveInterval = time.Duration(200) * time.Millisecond
hp.Discovery = true //discovery
config := &network.BzzConfig{
OverlayAddr: addr.Over(),
UnderlayAddr: addr.Under(),
HiveParams: hp,
}
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
},
})
defer sim.Close()
err := sim.UploadSnapshot(filename)
if err != nil {
utils.Fatalf("%v", err)
}
ctx, cancelSimRun := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancelSimRun()
if _, err := sim.WaitTillHealthy(ctx, 2); err != nil {
utils.Fatalf("%v", err)
}
return nil
}

View file

@ -0,0 +1,33 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"testing"
)
func TestSnapshotVerify(t *testing.T) {
snap := runSnapshot(t,
"v",
"testdata/snapshot.json",
)
snap.ExpectExit()
if snap.ExitStatus() != 0 {
t.Fatal("expected exit code 0")
}
}

View file

@ -80,6 +80,43 @@ func (net *Network) Events() *event.Feed {
return &net.events return &net.events
} }
func triggerChecks(trigger chan enode.ID, net *Network, id enode.ID) error {
node := net.GetNode(id)
if node == nil {
return fmt.Errorf("unknown node: %s", id)
}
client, err := node.Client()
if err != nil {
return err
}
events := make(chan *p2p.PeerEvent)
sub, err := client.Subscribe(context.Background(), "admin", events, "peerEvents")
if err != nil {
return fmt.Errorf("error getting peer events for node %v: %s", id, err)
}
go func() {
defer sub.Unsubscribe()
tick := time.NewTicker(time.Second)
defer tick.Stop()
for {
select {
case <-events:
trigger <- id
case <-tick.C:
trigger <- id
case err := <-sub.Err():
if err != nil {
log.Error(fmt.Sprintf("error getting peer events for node %v", id), "err", err)
}
return
}
}
}()
return nil
}
// NewNodeWithConfig adds a new node to the network with the given config, // NewNodeWithConfig adds a new node to the network with the given config,
// returning an error if a node with the same ID or name already exists // returning an error if a node with the same ID or name already exists
func (net *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) { func (net *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) {

View file

@ -49,9 +49,11 @@ func (s *Simulation) Run(ctx context.Context, step *Step) (result *StepResult) {
defer stop() defer stop()
// perform the action // perform the action
if err := step.Action(ctx); err != nil { if step.Action != nil {
result.Error = err if err := step.Action(ctx); err != nil {
return result.Error = err
return
}
} }
// wait for all node expectations to either pass, error or timeout // wait for all node expectations to either pass, error or timeout

View file

@ -640,6 +640,8 @@ func (k *Kademlia) saturation() int {
}) })
// TODO evaluate whether this check cannot just as well be done within the eachbin // TODO evaluate whether this check cannot just as well be done within the eachbin
depth := depthForPot(k.conns, k.NeighbourhoodSize, k.base) depth := depthForPot(k.conns, k.NeighbourhoodSize, k.base)
// if in the iterator above we iterated deeper than the neighbourhood depth - return depth
if depth < prev { if depth < prev {
return depth return depth
} }

View file

@ -23,11 +23,9 @@ import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math/rand"
"os" "os"
"path" "path"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@ -247,25 +245,14 @@ func discoverySimulation(nodes, conns int, adapter adapters.NodeAdapter) (*simul
action := func(ctx context.Context) error { action := func(ctx context.Context) error {
return nil return nil
} }
wg := sync.WaitGroup{}
for i := range ids { for i := range ids {
// collect the overlay addresses, to // collect the overlay addresses, to
addrs = append(addrs, ids[i].Bytes()) addrs = append(addrs, ids[i].Bytes())
for j := 0; j < conns; j++ {
var k int
if j == 0 {
k = (i + 1) % len(ids)
} else {
k = rand.Intn(len(ids))
}
wg.Add(1)
go func(i, k int) {
defer wg.Done()
net.Connect(ids[i], ids[k])
}(i, k)
}
} }
wg.Wait() err := net.ConnectNodesChain(nil)
if err != nil {
return nil, err
}
log.Debug(fmt.Sprintf("nodes: %v", len(addrs))) log.Debug(fmt.Sprintf("nodes: %v", len(addrs)))
// construct the peer pot, so that kademlia health can be checked // construct the peer pot, so that kademlia health can be checked
ppmap := network.NewPeerPotMap(network.NewKadParams().NeighbourhoodSize, addrs) ppmap := network.NewPeerPotMap(network.NewKadParams().NeighbourhoodSize, addrs)
@ -457,23 +444,7 @@ func discoveryPersistenceSimulation(nodes, conns int, adapter adapters.NodeAdapt
return nil return nil
} }
//connects in a chain net.ConnectNodesChain(nil)
wg := sync.WaitGroup{}
//connects in a ring
for i := range ids {
for j := 1; j <= conns; j++ {
k := (i + j) % len(ids)
if k == i {
k = (k + 1) % len(ids)
}
wg.Add(1)
go func(i, k int) {
defer wg.Done()
net.Connect(ids[i], ids[k])
}(i, k)
}
}
wg.Wait()
log.Debug(fmt.Sprintf("nodes: %v", len(addrs))) log.Debug(fmt.Sprintf("nodes: %v", len(addrs)))
// construct the peer pot, so that kademlia health can be checked // construct the peer pot, so that kademlia health can be checked
check := func(ctx context.Context, id enode.ID) (bool, error) { check := func(ctx context.Context, id enode.ID) (bool, error) {