diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 70e000e..005c1f4 --- a/Makefile +++ b/Makefile @@ -1,7 +1,25 @@ -all: +CLEAN = + +all: iptb + +iptb: go build +plugins: + make -C plugins all + +install_plugins: + make -C plugins install + +CLEAN += iptb + +install: + go install + test: make -C sharness all -.PHONY: all test \ No newline at end of file +clean: + rm $(CLEAN) + +.PHONY: all test iptb install plugins clean diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 48886a7..c43dc64 --- a/README.md +++ b/README.md @@ -1,22 +1,19 @@ # IPTB -`iptb` is a program used to create and manage a cluster of sandboxed IPFS nodes locally on your computer. Spin up 1000s of nodes! It exposes various options, such as different bootstrapping patterns. `iptb` makes testing IPFS networks easy! +`iptb` is a program used to create and manage a cluster of sandboxed nodes +locally on your computer. Spin up 1000s of nodes! Using `iptb` makes testing +libp2p networks easy! ### Example ``` -$ iptb init -n 5 +$ iptb auto -count 5 >/dev/null $ iptb start -Started daemon 0, pid = 12396 -Started daemon 1, pid = 12406 -Started daemon 2, pid = 12415 -Started daemon 3, pid = 12424 -Started daemon 4, pid = 12434 $ iptb shell 0 $ echo $IPFS_PATH -/home/noffle/testbed/0 +/home/iptb/testbed/testbeds/default/0 $ echo 'hey!' | ipfs add -q QmNqugRcYjwh9pEQUK7MLuxvLjxDNZL1DH8PJJgWtQXxuF @@ -32,42 +29,50 @@ hey! ### Usage ``` -$ iptb --help - NAME: - iptb - The IPFS TestBed + iptb - iptb is a tool for managing test clusters of libp2p nodes USAGE: - iptb [global options] command [command options] [arguments...] + iptb [global options] command [command options] [arguments...] + +VERSION: + 0.0.0 COMMANDS: - init create and initialize testbed configuration - start start up all testbed nodes - kill, stop kill a specific node (or all nodes, if none specified) - restart kill all nodes, then restart - shell spawn a subshell with certain IPFS environment variables set - get get an attribute of the given node - connect connect two nodes together - dump-stack get a stack dump from the given daemon - help, h show a list of subcommands, or help for a specific subcommand + auto create default testbed and initialize + testbed manage testbeds + help, h Shows a list of commands or help for one command + ATTRIBUTES: + attr get, set, list attributes + CORE: + init initialize specified nodes (or all) + start start specified nodes (or all) + stop stop specified nodes (or all) + restart restart specified nodes (or all) + run run command on specified nodes (or all) + connect connect sets of nodes together (or all) + shell starts a shell within the context of node + METRICS: + logs show logs from specified nodes (or all) + events stream events from specified nodes (or all) + metric get metric from node GLOBAL OPTIONS: - --help, -h show help - --version, -v print the version + --testbed value Name of testbed to use under IPTB_ROOT (default: "default") [$IPTB_TESTBED] + --help, -h show help + --version, -v print the version ``` ### Install ``` -go get github.com/ipfs/iptb +$ go get github.com/ipfs/iptb ``` ### Configuration By default, `iptb` uses `$HOME/testbed` to store created nodes. This path is configurable via the environment variables `IPTB_ROOT`. - - ### License MIT diff --git a/Simulation.md b/Simulation.md deleted file mode 100644 index 49c189c..0000000 --- a/Simulation.md +++ /dev/null @@ -1,64 +0,0 @@ -### Measure the performance of IPFS using IPTB - -The simulation has two components: -- iptb make-topology: This creates a connection graph between the nodes (e.g. star topology, barbell topology). In the topology files empty lines and lines starting with # are disregarded. For non empty line the syntax is `origin:connection 1, connection 2 ...` where origin and connections are specified with their node ID. -- iptb dist -hash: The simulation here distributes a single file from node 0 to every other node in the network. Then it calculates the average time required to download the file, the standard deviation of the time, the maximum time, the minimum time, the duplicate blocks. The results are saved in a generated file called results.json. - - -You can use local node and run it as follows: -``` -for k in `seq $1 $2 $3` -do - # Initalize network - ./iptb init -n $k -f - # Start nodes - ./iptb start - # Create Network Topology - ./iptb make-topology - # Create a random file and add it to Node 0 - head -c $4 file.txt - file=$(./iptb run 0 ipfs add -Q file.txt) - # Remove the file since we no longer need it - rm file.txt - # Make the simulation - ./iptb dist -hash $file - # Lets not burn the CPU and clean up after - pkill ipfs -done -./bin/results_plotter.py -i results.json -size $4 -``` -If you want to simulate bad network conditions you need to use a docker type node. Depending on your docker permissions you may need to run these commands as root. -``` - # Initalize network - ./iptb init -n $k -f --type=docker - # Start nodes - ./iptb start - # Create Network Topology - ./iptb make-topology - # Create random file - head -c $4 file.txt - # Push the file to docker container - dockID=$(cat ~/testbed/0/dockerID) - docker cp file.txt $dockID:file.txt - # Add it to Node 0 - file=$(./iptb run 0 ipfs add -Q file.txt) - rm file.txt - # Simulate - ./iptb dist -hash $file - pkill dockerd -``` -Before running the iptb dist command specify the following network parameters to emulate a bad network: -``` -# add 50ms of latency to everything node 4 does -iptb set latency 50ms 4 - -# limit nodes 3-5 to 12Mbps (input parsing is bad here, i know) -iptb set bandwidth 12 [3-5] - -# set a 6% packet loss on node 9 -iptb set loss 6 9 - -# set latency jitter (+/-) of 7ms on nodes 0 through 9 -iptb set jitter 7ms [0-9] - -``` diff --git a/bin/results_plotter.py b/bin/results_plotter.py deleted file mode 100755 index a6db219..0000000 --- a/bin/results_plotter.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -import json -import matplotlib.pyplot as plt -import argparse - -def parse_single_line(line, filepath): - d = json.loads(line) - users = int(d['Users']) - if users == len(d['Results']): - return d['Users'], d['Avg_time'],d['Std_Time'], d['Delay_Max'], d['Delay_Min'],d['Results'] - else: - print("The file did not reach to all nodes") - -def parse_file(filepath): - user_no = [] - delay_avg = [] - delay_std = [] - delay_max = [] - delay_min = [] - results = [] - with open(filepath) as fp: - line = fp.readline() - while line: - res = parse_single_line(line,filepath) - if res is not None: - user_no.append(res[0]) - delay_avg.append(res[1]) - delay_std.append(res[2]) - delay_max.append(res[3]) - delay_min.append(res[4]) - results.append(res[5]) - line = fp.readline() - return user_no, delay_avg, delay_std, delay_max, delay_min, results - - -def plot(filepath,label,colour_,file_size): - user_no, delay_avg, delay_std, delay_max, delay_min,_ = parse_file(filepath) - plt.plot(user_no, delay_avg,'o--', color=colour_, label=label + " Average delay",ms=3) #, yerr = delay_std, fmt='o' ) - plt.plot(user_no, delay_max,'--', color=colour_, label=label + " Max delay",alpha=0.3) - plt.plot(user_no, delay_min,'--', color=colour_,label=label + " Min delay",alpha=0.3) - plt.fill_between(user_no, - delay_max, - delay_min, - color =colour_, - alpha=0.2 ) - plt.xlabel('Number of Nodes') - plt.ylabel('Average delay[sec]') - if file_size: - plt.title('Average time required to distribute a {} size file'.format(file_size)) - -# USAGE: ./evaluation_scripts/results_parser.py -o ipfs-vs-BitTorrent -IPFS -BitTorrent -save -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Command Line Interface') - parser.add_argument('-o', type=str, nargs='?', - help="Save output to the specified directory") - parser.add_argument('-i', type=str, nargs='?', - help="Specifiy input directory") - - parser.add_argument('-size', type=str, nargs='?', - help="Specifiy experiment file size") - - args = parser.parse_args() - fig, ax1 = plt.subplots() - if args.i == None: - print("No input file is specied") - plot(args.i, "IPFS",'blue',args.size) - plt.legend() - if args.o != None: - fig.savefig(args.o +'.png',bbox_inches='tight') - else: - plt.show() \ No newline at end of file diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile old mode 100644 new mode 100755 diff --git a/commands/attr.go b/commands/attr.go new file mode 100755 index 0000000..254f1f7 --- /dev/null +++ b/commands/attr.go @@ -0,0 +1,181 @@ +package commands + +import ( + "fmt" + "path" + "strconv" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var AttrCmd = cli.Command{ + Category: "ATTRIBUTES", + Name: "attr", + Usage: "get, set, list attributes", + Subcommands: []cli.Command{ + AttrSetCmd, + AttrGetCmd, + AttrListCmd, + }, +} + +var AttrSetCmd = cli.Command{ + Name: "set", + Usage: "set an attribute for a node", + ArgsUsage: " ", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "save", + Usage: "saves attribute value to nodespec", + }, + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagSave := c.Bool("save") + + if c.NArg() != 3 { + return NewUsageError("set takes exactly 3 argument") + } + + argNode := c.Args()[0] + argAttr := c.Args()[1] + argValue := c.Args()[2] + + i, err := strconv.Atoi(argNode) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + node, err := tb.Node(i) + if err != nil { + return err + } + + attrNode, ok := node.(testbedi.Attribute) + if !ok { + return fmt.Errorf("node does not implement attributes") + } + + if err := attrNode.SetAttr(argAttr, argValue); err != nil { + return err + } + + if flagSave { + specs, err := tb.Specs() + if err != nil { + return err + } + + specs[i].SetAttr(argAttr, argValue) + + if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil { + return err + } + } + + return nil + }, +} + +var AttrGetCmd = cli.Command{ + Name: "get", + Usage: "get an attribute for a node", + ArgsUsage: " ", + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + if c.NArg() != 2 { + return NewUsageError("get takes exactly 2 argument") + } + + argNode := c.Args()[0] + argAttr := c.Args()[1] + + i, err := strconv.Atoi(argNode) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + node, err := tb.Node(i) + if err != nil { + return err + } + + attrNode, ok := node.(testbedi.Attribute) + if !ok { + return fmt.Errorf("node does not implement attributes") + } + + value, err := attrNode.Attr(argAttr) + if err != nil { + return err + } + + _, err = fmt.Fprintf(c.App.Writer, "%s\n", value) + + return err + }, +} + +var AttrListCmd = cli.Command{ + Name: "list", + Usage: "list attributes available for a node", + ArgsUsage: "", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "type", + Usage: "look up attributes for node type", + }, + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagType := c.String("type") + + if !c.Args().Present() && len(flagType) == 0 { + return NewUsageError("specify a node, or a type") + } + + if c.Args().Present() { + i, err := strconv.Atoi(c.Args().First()) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + spec, err := tb.Spec(i) + if err != nil { + return err + } + + flagType = spec.Type + } + + plg, ok := testbed.GetPlugin(flagType) + if !ok { + return fmt.Errorf("Unknown plugin %s", flagType) + } + + attrList := plg.GetAttrList() + for _, a := range attrList { + desc, err := plg.GetAttrDesc(a) + if err != nil { + return fmt.Errorf("error getting attribute description: %s", err) + } + + fmt.Fprintf(c.App.Writer, "\t%s: %s\n", a, desc) + } + + return nil + }, +} diff --git a/commands/auto.go b/commands/auto.go new file mode 100755 index 0000000..84e952d --- /dev/null +++ b/commands/auto.go @@ -0,0 +1,113 @@ +package commands + +import ( + "context" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var AutoCmd = cli.Command{ + Name: "auto", + Usage: "create default testbed and initialize", + Description: ` +The auto command is a quick way to use iptb for simple configurations. + +The auto command is similar to 'testbed create' except in a few ways + + - No attr options can be passed in + - All nodes are initialize by default and ready to be started + - An optional --start flag can be passed to start all nodes + +The following two examples are equivalent + +$ iptb testbed create -count 5 -type -init +$ iptb auto -count 5 -type +`, + ArgsUsage: "--type ", + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "count", + Usage: "number of nodes to initialize", + Value: 1, + }, + cli.BoolFlag{ + Name: "force", + Usage: "force overwrite of existing nodespecs", + }, + cli.StringFlag{ + Name: "type", + Usage: "kind of nodes to initialize", + }, + cli.BoolFlag{ + Name: "start", + Usage: "starts nodes immediately", + }, + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagType := c.String("type") + flagStart := c.Bool("start") + flagCount := c.Int("count") + flagForce := c.Bool("force") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + if err := testbed.AlreadyInitCheck(tb.Dir(), flagForce); err != nil { + return err + } + + specs, err := testbed.BuildSpecs(tb.Dir(), flagCount, flagType, nil) + if err != nil { + return err + } + + if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil { + return err + } + + nodes, err := tb.Nodes() + if err != nil { + return err + } + + var list []int + for i, _ := range nodes { + list = append(list, i) + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + return node.Init(context.Background()) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + if err := buildReport(results); err != nil { + return err + } + + if flagStart { + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + return node.Start(context.Background(), true) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + if err := buildReport(results); err != nil { + return err + } + } + + return nil + }, +} diff --git a/commands/commands.go b/commands/commands.go new file mode 100755 index 0000000..db7faac --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,19 @@ +package commands + +import ( + "fmt" +) + +func NewUsageError(s string) error { + return &UsageError{ + s, + } +} + +type UsageError struct { + s string +} + +func (e *UsageError) Error() string { + return fmt.Sprintf("Usage Error: %s", e.s) +} diff --git a/commands/connect.go b/commands/connect.go new file mode 100755 index 0000000..43d7c55 --- /dev/null +++ b/commands/connect.go @@ -0,0 +1,125 @@ +package commands + +import ( + "context" + "fmt" + "path" + "time" + + "github.com/pkg/errors" + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" +) + +var ConnectCmd = cli.Command{ + Category: "CORE", + Name: "connect", + Usage: "connect sets of nodes together (or all)", + ArgsUsage: "[nodes] [nodes]", + Description: ` +The connect command allows for connecting sets of nodes together. + +Every node listed in the first set, will try to connect to every node +listed in the second set. + +There are three variants of the command. It can accept no arugments, +a single argument, or two arguments. The no argument and single argument +expands out to the two argument usage. + +$ iptb connect => iptb connect [0-C] [0-C] +$ iptb connect [n-m] => iptb connect [n-m] [n-m] +$ iptb connect [n-m] [i-k] + +Sets of nodes can be expressed in the following ways + +INPUT EXPANDED +0 0 +[0] 0 +[0-4] 0,1,2,3,4 +[0,2-4] 0,2,3,4 +[2-4,0] 2,3,4,0 +[0,2,4] 0,2,4 +`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "timeout", + Usage: "timeout on the command", + Value: "30s", + }, + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagTimeout := c.String("timeout") + + timeout, err := time.ParseDuration(flagTimeout) + if err != nil { + return err + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + args := c.Args() + + switch c.NArg() { + case 0: + nodes, err := tb.Nodes() + if err != nil { + return err + } + + fromto, err := parseRange(fmt.Sprintf("[0-%d]", len(nodes)-1)) + if err != nil { + return err + } + + return connectNodes(tb, fromto, fromto, timeout) + case 1: + fromto, err := parseRange(args[0]) + if err != nil { + return err + } + + return connectNodes(tb, fromto, fromto, timeout) + case 2: + from, err := parseRange(args[0]) + if err != nil { + return err + } + + to, err := parseRange(args[1]) + if err != nil { + return err + } + + return connectNodes(tb, from, to, timeout) + default: + return NewUsageError("connet accepts between 0 and 2 arguments") + } + }, +} + +func connectNodes(tb testbed.BasicTestbed, from, to []int, timeout time.Duration) error { + nodes, err := tb.Nodes() + if err != nil { + return err + } + + var results []Result + for _, f := range from { + for _, t := range to { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + err = nodes[f].Connect(ctx, nodes[t]) + + results = append(results, Result{ + Node: f, + Output: nil, + Error: errors.Wrapf(err, "node[%d] => node[%d]", f, t), + }) + } + } + + return buildReport(results) +} diff --git a/commands/events.go b/commands/events.go new file mode 100755 index 0000000..302501e --- /dev/null +++ b/commands/events.go @@ -0,0 +1,53 @@ +package commands + +import ( + "fmt" + "io" + "path" + "strconv" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var EventsCmd = cli.Command{ + Category: "METRICS", + Name: "events", + Usage: "stream events from specified nodes (or all)", + ArgsUsage: "[node]", + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + if !c.Args().Present() { + return NewUsageError("events takes exactly 1 argument") + } + + i, err := strconv.Atoi(c.Args().First()) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + node, err := tb.Node(i) + if err != nil { + return err + } + + mn, ok := node.(testbedi.Metric) + if !ok { + return fmt.Errorf("node does not implement metrics") + } + + el, err := mn.Events() + if err != nil { + return err + } + + _, err = io.Copy(c.App.Writer, el) + return err + }, +} diff --git a/commands/init.go b/commands/init.go new file mode 100755 index 0000000..b447cf6 --- /dev/null +++ b/commands/init.go @@ -0,0 +1,64 @@ +package commands + +import ( + "context" + "fmt" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var InitCmd = cli.Command{ + Category: "CORE", + Name: "init", + Usage: "initialize specified nodes (or all)", + ArgsUsage: "[nodes] -- [arguments...]", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "terminator", + Hidden: true, + }, + }, + Before: func(c *cli.Context) error { + if present := isTerminatorPresent(c); present { + return c.Set("terminator", "true") + } + + return nil + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + nodes, err := tb.Nodes() + if err != nil { + return err + } + + nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) + + if nodeRange == "" { + nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) + } + + list, err := parseRange(nodeRange) + if err != nil { + return fmt.Errorf("could not parse node range %s", nodeRange) + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + return node.Init(context.Background(), args...) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + return buildReport(results) + }, +} diff --git a/commands/logs.go b/commands/logs.go new file mode 100755 index 0000000..8ebd9af --- /dev/null +++ b/commands/logs.go @@ -0,0 +1,120 @@ +package commands + +import ( + "fmt" + "io" + "io/ioutil" + "path" + "strings" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var LogsCmd = cli.Command{ + Category: "METRICS", + Name: "logs", + Usage: "show logs from specified nodes (or all)", + ArgsUsage: "[nodes]", + Flags: []cli.Flag{ + cli.BoolTFlag{ + Name: "err, e", + Usage: "show stderr stream", + }, + cli.BoolTFlag{ + Name: "out, o", + Usage: "show stdout stream", + }, + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagErr := c.BoolT("err") + flagOut := c.BoolT("out") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + nodes, err := tb.Nodes() + if err != nil { + return err + } + + nodeRange := c.Args().First() + + if nodeRange == "" { + nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) + } + + list, err := parseRange(nodeRange) + if err != nil { + return err + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + metricNode, ok := node.(testbedi.Metric) + if !ok { + return nil, fmt.Errorf("node does not implement metrics") + } + + stdout := ioutil.NopCloser(strings.NewReader("")) + stderr := ioutil.NopCloser(strings.NewReader("")) + + if flagOut { + var err error + stdout, err = metricNode.StdoutReader() + if err != nil { + return nil, err + } + } + + if flagErr { + var err error + stderr, err = metricNode.StderrReader() + if err != nil { + return nil, err + } + } + + return NewOutput(stdout, stderr), nil + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + return buildReport(results) + }, +} + +func NewOutput(stdout, stderr io.ReadCloser) testbedi.Output { + return &Output{ + stdout: stdout, + stderr: stderr, + } +} + +type Output struct { + stdout io.ReadCloser + stderr io.ReadCloser +} + +func (o *Output) Args() []string { + return []string{} +} + +func (o *Output) Error() error { + return nil +} +func (o *Output) ExitCode() int { + return 0 +} + +func (o *Output) Stdout() io.ReadCloser { + return o.stdout +} + +func (o *Output) Stderr() io.ReadCloser { + return o.stderr +} diff --git a/commands/metric.go b/commands/metric.go new file mode 100755 index 0000000..c3cd2cc --- /dev/null +++ b/commands/metric.go @@ -0,0 +1,98 @@ +package commands + +import ( + "fmt" + "path" + "strconv" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var MetricCmd = cli.Command{ + Category: "METRICS", + Name: "metric", + Usage: "get metric from node", + ArgsUsage: " [metric]", + Action: func(c *cli.Context) error { + if c.NArg() == 1 { + return metricList(c) + } + + if c.NArg() == 2 { + return metricGet(c) + } + + return NewUsageError("metric takes 1 or 2 arguments only") + }, +} + +func metricList(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + i, err := strconv.Atoi(c.Args().First()) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + node, err := tb.Node(i) + if err != nil { + return err + } + + metricNode, ok := node.(testbedi.Metric) + if !ok { + return fmt.Errorf("node does not implement metrics") + } + + metricList := metricNode.GetMetricList() + for _, m := range metricList { + desc, err := metricNode.GetMetricDesc(m) + if err != nil { + return fmt.Errorf("error getting metric description: %s", err) + } + + fmt.Fprintf(c.App.Writer, "\t%s: %s\n", m, desc) + } + + return nil +} + +func metricGet(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + argNode := c.Args()[0] + argMetric := c.Args()[1] + + i, err := strconv.Atoi(argNode) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + node, err := tb.Node(i) + if err != nil { + return err + } + + metricNode, ok := node.(testbedi.Metric) + if !ok { + return fmt.Errorf("node does not implement metrics") + } + + value, err := metricNode.Metric(argMetric) + if err != nil { + return err + } + + _, err = fmt.Fprintf(c.App.Writer, "%s\n", value) + + return err +} diff --git a/commands/restart.go b/commands/restart.go new file mode 100755 index 0000000..16f1a11 --- /dev/null +++ b/commands/restart.go @@ -0,0 +1,73 @@ +package commands + +import ( + "context" + "fmt" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var RestartCmd = cli.Command{ + Category: "CORE", + Name: "restart", + Usage: "restart specified nodes (or all)", + ArgsUsage: "[nodes] -- [arguments...]", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "wait", + Usage: "wait for nodes to start before returning", + }, + cli.BoolFlag{ + Name: "terminator", + Hidden: true, + }, + }, + Before: func(c *cli.Context) error { + if present := isTerminatorPresent(c); present { + return c.Set("terminator", "true") + } + + return nil + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagWait := c.Bool("wait") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + nodes, err := tb.Nodes() + if err != nil { + return err + } + + nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) + + if nodeRange == "" { + nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) + } + + list, err := parseRange(nodeRange) + if err != nil { + return fmt.Errorf("could not parse node range %s", nodeRange) + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + if err := node.Stop(context.Background()); err != nil { + return nil, err + } + + return node.Start(context.Background(), flagWait, args...) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + return buildReport(results) + }, +} diff --git a/commands/run.go b/commands/run.go new file mode 100755 index 0000000..918b29e --- /dev/null +++ b/commands/run.go @@ -0,0 +1,64 @@ +package commands + +import ( + "context" + "fmt" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var RunCmd = cli.Command{ + Category: "CORE", + Name: "run", + Usage: "run command on specified nodes (or all)", + ArgsUsage: "[nodes] -- ", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "terminator", + Hidden: true, + }, + }, + Before: func(c *cli.Context) error { + if present := isTerminatorPresent(c); present { + return c.Set("terminator", "true") + } + + return nil + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + nodes, err := tb.Nodes() + if err != nil { + return err + } + + nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) + + if nodeRange == "" { + nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) + } + + list, err := parseRange(nodeRange) + if err != nil { + return fmt.Errorf("could not parse node range %s", nodeRange) + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + return node.RunCmd(context.Background(), nil, args...) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + return buildReport(results) + }, +} diff --git a/commands/shell.go b/commands/shell.go new file mode 100755 index 0000000..dbc6032 --- /dev/null +++ b/commands/shell.go @@ -0,0 +1,41 @@ +package commands + +import ( + "context" + "fmt" + "path" + "strconv" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" +) + +var ShellCmd = cli.Command{ + Category: "CORE", + Name: "shell", + Usage: "starts a shell within the context of node", + ArgsUsage: "", + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + if !c.Args().Present() { + return NewUsageError("shell takes exactly 1 argument") + } + + i, err := strconv.Atoi(c.Args().First()) + if err != nil { + return fmt.Errorf("parse err: %s", err) + } + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + nodes, err := tb.Nodes() + if err != nil { + return err + } + + return nodes[i].Shell(context.Background(), nodes) + }, +} diff --git a/commands/start.go b/commands/start.go new file mode 100755 index 0000000..de8b3d3 --- /dev/null +++ b/commands/start.go @@ -0,0 +1,69 @@ +package commands + +import ( + "context" + "fmt" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var StartCmd = cli.Command{ + Category: "CORE", + Name: "start", + Usage: "start specified nodes (or all)", + ArgsUsage: "[nodes] -- [arguments...]", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "wait", + Usage: "wait for nodes to start before returning", + }, + cli.BoolFlag{ + Name: "terminator", + Hidden: true, + }, + }, + Before: func(c *cli.Context) error { + if present := isTerminatorPresent(c); present { + return c.Set("terminator", "true") + } + + return nil + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagWait := c.Bool("wait") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + nodes, err := tb.Nodes() + if err != nil { + return err + } + + nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) + + if nodeRange == "" { + nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) + } + + list, err := parseRange(nodeRange) + if err != nil { + return fmt.Errorf("could not parse node range %s", nodeRange) + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + return node.Start(context.Background(), flagWait, args...) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + return buildReport(results) + }, +} diff --git a/commands/stop.go b/commands/stop.go new file mode 100755 index 0000000..6609b38 --- /dev/null +++ b/commands/stop.go @@ -0,0 +1,51 @@ +package commands + +import ( + "context" + "fmt" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" + "github.com/ipfs/iptb/testbed/interfaces" +) + +var StopCmd = cli.Command{ + Category: "CORE", + Name: "stop", + Usage: "stop specified nodes (or all)", + ArgsUsage: "[nodes]", + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + nodes, err := tb.Nodes() + if err != nil { + return err + } + + nodeRange := c.Args().First() + + if nodeRange == "" { + nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) + } + + list, err := parseRange(nodeRange) + if err != nil { + return fmt.Errorf("could not parse node range %s", nodeRange) + } + + runCmd := func(node testbedi.Core) (testbedi.Output, error) { + return nil, node.Stop(context.Background()) + } + + results, err := mapWithOutput(list, nodes, runCmd) + if err != nil { + return err + } + + return buildReport(results) + }, +} diff --git a/commands/testbed.go b/commands/testbed.go new file mode 100755 index 0000000..cd7ca11 --- /dev/null +++ b/commands/testbed.go @@ -0,0 +1,87 @@ +package commands + +import ( + "context" + "path" + + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed" +) + +var TestbedCmd = cli.Command{ + Name: "testbed", + Usage: "manage testbeds", + Subcommands: []cli.Command{ + TestbedCreateCmd, + }, +} + +var TestbedCreateCmd = cli.Command{ + Name: "create", + Usage: "create testbed", + ArgsUsage: "--type ", + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "count", + Usage: "number of nodes to initialize", + Value: 1, + }, + cli.BoolFlag{ + Name: "force", + Usage: "force overwrite of existing testbed", + }, + cli.StringFlag{ + Name: "type", + Usage: "kind of nodes to initialize", + }, + cli.StringSliceFlag{ + Name: "attr", + Usage: "specify addition attributes for nodes", + }, + cli.BoolFlag{ + Name: "init", + Usage: "initialize after creation (like calling `init` after create)", + }, + }, + Action: func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") + flagTestbed := c.GlobalString("testbed") + flagType := c.String("type") + flagInit := c.Bool("init") + flagCount := c.Int("count") + flagForce := c.Bool("force") + flagAttrs := c.StringSlice("attr") + + attrs := parseAttrSlice(flagAttrs) + tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) + + if err := testbed.AlreadyInitCheck(tb.Dir(), flagForce); err != nil { + return err + } + + specs, err := testbed.BuildSpecs(tb.Dir(), flagCount, flagType, attrs) + if err != nil { + return err + } + + if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil { + return err + } + + if flagInit { + nodes, err := tb.Nodes() + if err != nil { + return err + } + + for _, n := range nodes { + if _, err := n.Init(context.Background()); err != nil { + return err + } + } + } + + return nil + }, +} diff --git a/commands/utils.go b/commands/utils.go new file mode 100755 index 0000000..bbddfb7 --- /dev/null +++ b/commands/utils.go @@ -0,0 +1,207 @@ +package commands + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" + + "github.com/pkg/errors" + cli "github.com/urfave/cli" + + "github.com/ipfs/iptb/testbed/interfaces" +) + +// the flag terminator stops flag parsing, but it also swallowed if its the +// first argument into a command / subcommand. To find it, we have to look +// up to the parent command. +// iptb run 0 -- ipfs id => c.Args: 0 -- ipfs id +// iptb run -- ipfs id => c.Args: ipfs id +func isTerminatorPresent(c *cli.Context) bool { + argsParent := c.Parent().Args().Tail() + argsSelf := c.Args() + + ls := len(argsSelf) + lp := len(argsParent) + + term := lp - ls - 1 + + if lp > ls && argsParent[term] == "--" { + return true + } + + return false +} + +func parseAttrSlice(attrsraw []string) map[string]string { + attrs := make(map[string]string) + for _, attr := range attrsraw { + parts := strings.Split(attr, ",") + + if len(parts) == 1 { + attrs[parts[0]] = "true" + } else { + attrs[parts[0]] = strings.Join(parts[1:], ",") + } + } + + return attrs +} + +func parseCommand(args []string, terminator bool) (string, []string) { + if terminator { + return "", args + } + + if len(args) == 0 { + return "", []string{} + } + + if len(args) == 1 { + return args[0], []string{} + } + + arguments := args[1:] + + if arguments[0] == "--" { + arguments = arguments[1:] + } + + return args[0], arguments +} + +func parseRange(s string) ([]int, error) { + if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { + ranges := strings.Split(s[1:len(s)-1], ",") + var out []int + for _, r := range ranges { + rng, err := expandDashRange(r) + if err != nil { + return nil, err + } + + out = append(out, rng...) + } + return out, nil + } + i, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + + return []int{i}, nil +} + +func expandDashRange(s string) ([]int, error) { + parts := strings.Split(s, "-") + if len(parts) == 1 { + i, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + return []int{i}, nil + } + low, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + hi, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + var out []int + for i := low; i <= hi; i++ { + out = append(out, i) + } + return out, nil +} + +type Result struct { + Node int + Output testbedi.Output + Error error +} + +type outputFunc func(testbedi.Core) (testbedi.Output, error) + +func mapWithOutput(list []int, nodes []testbedi.Core, fn outputFunc) ([]Result, error) { + var wg sync.WaitGroup + var lk sync.Mutex + results := make([]Result, len(list)) + + if err := validRange(list, len(nodes)); err != nil { + return results, err + } + + for i, n := range list { + wg.Add(1) + go func(i, n int, node testbedi.Core) { + defer wg.Done() + out, err := fn(node) + + lk.Lock() + defer lk.Unlock() + + results[i] = Result{ + Node: n, + Output: out, + Error: errors.Wrapf(err, "node[%d]", n), + } + }(i, n, nodes[n]) + } + + wg.Wait() + + return results, nil + +} + +func validRange(list []int, total int) error { + max := 0 + for _, n := range list { + if max < n { + max = n + } + } + + if max >= total { + return fmt.Errorf("Node range contains value (%d) outside of valid range [0-%d]", max, total-1) + } + + return nil +} + +func buildReport(results []Result) error { + var errs []error + + for _, rs := range results { + if rs.Error != nil { + errs = append(errs, rs.Error) + } + + if rs.Output != nil { + fmt.Printf("node[%d] exit %d\n", rs.Node, rs.Output.ExitCode()) + if rs.Output.Error() != nil { + fmt.Printf("%s", rs.Output.Error()) + } + + fmt.Println() + + io.Copy(os.Stdout, rs.Output.Stdout()) + io.Copy(os.Stdout, rs.Output.Stderr()) + + fmt.Println() + } + + } + + if len(errs) != 0 { + return cli.NewMultiError(errs...) + } + + return nil +} diff --git a/commands/utils_test.go b/commands/utils_test.go new file mode 100755 index 0000000..9de29ec --- /dev/null +++ b/commands/utils_test.go @@ -0,0 +1,105 @@ +package commands + +import ( + "fmt" + "os" + "reflect" + "runtime" + "strings" + "testing" +) + +var ( + wd, _ = os.Getwd() +) + +func expect(t *testing.T, a interface{}, b interface{}) { + _, fn, line, _ := runtime.Caller(1) + fn = strings.Replace(fn, wd+"/", "", -1) + + if !reflect.DeepEqual(a, b) { + t.Errorf("(%s:%d) Expected %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} + +func TestParseRange(t *testing.T) { + cases := []struct { + input string + expectedList []int + expectedErr error + }{ + {"0", []int{0}, nil}, + {"[0-1]", []int{0, 1}, nil}, + {"[0-5]", []int{0, 1, 2, 3, 4, 5}, nil}, + {"[4-7]", []int{4, 5, 6, 7}, nil}, + {"[0,1]", []int{0, 1}, nil}, + {"[1,4]", []int{1, 4}, nil}, + {"[1,3,5-8]", []int{1, 3, 5, 6, 7, 8}, nil}, + } + + for _, c := range cases { + list, err := parseRange(c.input) + + expect(t, err, c.expectedErr) + expect(t, list, c.expectedList) + } +} + +func TestValidRange(t *testing.T) { + buildError := func(max, total int) error { + return fmt.Errorf("Node range contains value (%d) outside of valid range [0-%d]", max, total-1) + } + + cases := []struct { + inputList []int + inputTotal int + expectedErr error + }{ + {[]int{0, 1}, 2, nil}, + {[]int{0, 3}, 2, buildError(3, 2)}, + } + + for _, c := range cases { + err := validRange(c.inputList, c.inputTotal) + + expect(t, err, c.expectedErr) + } +} + +func TestParseCommand(t *testing.T) { + cases := []struct { + inputArgs []string + inputTerm bool + expectedRange string + expectedArgs []string + }{ + {[]string{"0", "--", "--foo", "--bar"}, false, "0", []string{"--foo", "--bar"}}, + {[]string{"--foo", "--bar"}, true, "", []string{"--foo", "--bar"}}, + } + + for _, c := range cases { + nodeRange, args := parseCommand(c.inputArgs, c.inputTerm) + + expect(t, nodeRange, c.expectedRange) + expect(t, args, c.expectedArgs) + } +} + +func TestParseAttrSlice(t *testing.T) { + cases := []struct { + inputArgs []string + expectedAttrs map[string]interface{} + }{ + {[]string{}, map[string]interface{}{}}, + {[]string{"foo"}, map[string]interface{}{"foo": "true"}}, + {[]string{"foo,bar"}, map[string]interface{}{"foo": "bar"}}, + {[]string{"foo,bar,thing"}, map[string]interface{}{"foo": "bar,thing"}}, + {[]string{"foo,bar", "one,two"}, map[string]interface{}{"foo": "bar", "one": "two"}}, + } + + for _, c := range cases { + attrs := parseAttrSlice(c.inputArgs) + + expect(t, attrs, c.expectedAttrs) + } +} diff --git a/main.go b/main.go old mode 100644 new mode 100755 index f8a105a..81ebb75 --- a/main.go +++ b/main.go @@ -1,831 +1,127 @@ package main import ( - "bufio" - "encoding/json" - "errors" "fmt" - "io" - "math" - "net/http" + "io/ioutil" "os" - "strconv" - "strings" - "time" + "path" + "path/filepath" - util "github.com/ipfs/iptb/util" cli "github.com/urfave/cli" -) - -type SimulationResults struct { - Avg_time float64 - Std_Time float64 - Delay_Min float64 - Delay_Max float64 - Users int - Date_Time time.Time - Results []float64 - DuplBlocks int -} - -func (res SimulationResults) ResultsSave() { - - resultsJSON, _ := json.Marshal(res) - f, err := os.OpenFile("results.json", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - panic(err) - } - defer f.Close() - if _, err = f.WriteString(string(resultsJSON) + "\n"); err != nil { - panic(err) - } -} -func getDupBlocksFromNode(n int) (int, error) { + "github.com/ipfs/iptb/commands" + "github.com/ipfs/iptb/testbed" +) - nd, err := util.LoadNodeN(n) - if err != nil { - return -1, err +func loadPlugins(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return nil } - bstat, err := nd.RunCmd("ipfs", "bitswap", "stat") + plugs, err := ioutil.ReadDir(dir) if err != nil { - return -1, err + return err } - lines := strings.Split(bstat, "\n") - for _, l := range lines { - if strings.Contains(l, "dup blocks") { - fs := strings.Fields(l) - n, err := strconv.Atoi(fs[len(fs)-1]) - if err != nil { - return -1, err - } - - return int(n), nil - } - } + for _, f := range plugs { + plg, err := testbed.LoadPlugin(path.Join(dir, f.Name())) - return -1, fmt.Errorf("no dup blocks field in output") -} - -func parseRange(s string) ([]int, error) { - if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { - ranges := strings.Split(s[1:len(s)-1], ",") - var out []int - for _, r := range ranges { - rng, err := expandDashRange(r) - if err != nil { - return nil, err - } - - out = append(out, rng...) - } - return out, nil - } else { - i, err := strconv.Atoi(s) if err != nil { - return nil, err + fmt.Fprintf(os.Stderr, "%s\n", err) + continue } - return []int{i}, nil - } -} + overloaded, err := testbed.RegisterPlugin(*plg, false) + if overloaded { + fmt.Fprintf(os.Stderr, "overriding built in plugin %s with %s\n", plg.PluginName, path.Join(dir, f.Name())) + } -func expandDashRange(s string) ([]int, error) { - parts := strings.Split(s, "-") - if len(parts) == 0 { - i, err := strconv.Atoi(s) if err != nil { - return nil, err + return err } - return []int{i}, nil - } - low, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, err } - hi, err := strconv.Atoi(parts[1]) - if err != nil { - return nil, err - } - - var out []int - for i := low; i <= hi; i++ { - out = append(out, i) - } - return out, nil -} - -func handleErr(s string, err error) { - if err != nil { - fmt.Fprintln(os.Stderr, s, err) - os.Exit(1) - } + return nil } func main() { app := cli.NewApp() - app.Usage = "iptb is a tool for managing test clusters of ipfs nodes" - app.Commands = []cli.Command{ - connectCmd, - dumpStacksCmd, - forEachCmd, - getCmd, - initCmd, - killCmd, - restartCmd, - setCmd, - shellCmd, - startCmd, - runCmd, - connGraphCmd, - distCmd, - logsCmd, - } - - err := app.Run(os.Args) - if err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -var initCmd = cli.Command{ - Name: "init", - Usage: "create and initialize testbed nodes", - Flags: []cli.Flag{ - cli.IntFlag{ - Name: "count, n", - Usage: "number of ipfs nodes to initialize", - }, - cli.IntFlag{ - Name: "port, p", - Usage: "port to start allocations from", - }, - cli.BoolFlag{ - Name: "force, f", - Usage: "force initialization (overwrite existing configs)", - }, - cli.BoolFlag{ - Name: "mdns", - Usage: "turn on mdns for nodes", - }, - cli.StringFlag{ - Name: "bootstrap", - Usage: "select bootstrapping style for cluster", - Value: "star", - }, - cli.BoolFlag{ - Name: "utp", - Usage: "use utp for addresses", - }, - cli.BoolFlag{ - Name: "ws", - Usage: "use websocket for addresses", - }, - cli.StringFlag{ - Name: "cfg", - Usage: "override default config with values from the given file", - }, + app.Usage = "iptb is a tool for managing test clusters of libp2p nodes" + app.Flags = []cli.Flag{ cli.StringFlag{ - Name: "type", - Usage: "select type of nodes to initialize", - }, - }, - Action: func(c *cli.Context) error { - if c.Int("count") == 0 { - fmt.Printf("please specify number of nodes: '%s init -n 10'\n", os.Args[0]) - os.Exit(1) - } - fmt.Println("Initializing users...") - cfg := &util.InitCfg{ - Bootstrap: c.String("bootstrap"), - Force: c.Bool("f"), - Count: c.Int("count"), - Mdns: c.Bool("mdns"), - Utp: c.Bool("utp"), - Websocket: c.Bool("ws"), - PortStart: c.Int("port"), - Override: c.String("cfg"), - NodeType: c.String("type"), - } - - err := util.IpfsInit(cfg) - handleErr("ipfs init err: ", err) - return nil - }, -} - -var startCmd = cli.Command{ - Name: "start", - Usage: "starts up all testbed nodes", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "wait", - Usage: "wait for nodes to fully come online before returning", + Name: "testbed", + Value: "default", + EnvVar: "IPTB_TESTBED", + Usage: "Name of testbed to use under IPTB_ROOT", }, cli.StringFlag{ - Name: "args", - Usage: "extra args to pass on to the ipfs daemon", + Name: "IPTB_ROOT", + EnvVar: "IPTB_ROOT", + Hidden: true, }, - }, - Action: func(c *cli.Context) error { - var extra []string - args := c.String("args") - fmt.Print("Starting...") - if len(args) > 0 { - extra = strings.Fields(args) - } - - if c.Args().Present() { - nodes, err := parseRange(c.Args()[0]) - if err != nil { - return err - } - - for _, n := range nodes { - nd, err := util.LoadNodeN(n) - if err != nil { - return fmt.Errorf("failed to load local node: %s\n", err) - } - - err = nd.Start(extra) - if err != nil { - fmt.Println("failed to start node: ", err) - } - } - return nil - } - - nodes, err := util.LoadNodes() - if err != nil { - return err - } - return util.IpfsStart(nodes, c.Bool("wait"), extra) - }, -} - -var killCmd = cli.Command{ - Name: "kill", - Usage: "kill a given node (or all nodes if none specified)", - Aliases: []string{"stop"}, - Action: func(c *cli.Context) error { - if c.Args().Present() { - nodes, err := parseRange(c.Args()[0]) - if err != nil { - return fmt.Errorf("failed to parse node number: %s", err) - } - - for _, n := range nodes { - nd, err := util.LoadNodeN(n) - if err != nil { - return fmt.Errorf("failed to load local node: %s\n", err) - } - - err = nd.Kill() - if err != nil { - fmt.Println("failed to kill node: ", err) - } - } - return nil - } - nodes, err := util.LoadNodes() - if err != nil { - return err - } - - err = util.IpfsKillAll(nodes) - handleErr("ipfs kill err: ", err) - return nil - }, -} - -var restartCmd = cli.Command{ - Name: "restart", - Usage: "kill all nodes, then restart", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "wait", - Usage: "wait for nodes to come online before returning", - }, - }, - Action: func(c *cli.Context) error { - if c.Args().Present() { - nodes, err := parseRange(c.Args()[0]) - if err != nil { - return err - } - - for _, n := range nodes { - nd, err := util.LoadNodeN(n) - if err != nil { - return fmt.Errorf("failed to load local node: %s\n", err) - } - - err = nd.Kill() - if err != nil { - fmt.Println("restart: failed to kill node: ", err) - } - - err = nd.Start(nil) - if err != nil { - fmt.Println("restart: failed to start node again: ", err) - } - } - return nil - } - nodes, err := util.LoadNodes() - if err != nil { - return err - } - - err = util.IpfsKillAll(nodes) - if err != nil { - return fmt.Errorf("ipfs kill err: %s", err) - } - - err = util.IpfsStart(nodes, c.Bool("wait"), nil) - handleErr("ipfs start err: ", err) - return nil - }, -} - -var shellCmd = cli.Command{ - Name: "shell", - Usage: "execs your shell with certain environment variables set", - Description: `Starts a new shell and sets some environment variables for you: - -IPFS_PATH - set to testbed node 'n's IPFS_PATH -NODE[x] - set to the peer ID of node x -`, - Action: func(c *cli.Context) error { - if !c.Args().Present() { - fmt.Println("please specify which node you want a shell for") - os.Exit(1) - } - i, err := strconv.Atoi(c.Args()[0]) - if err != nil { - return fmt.Errorf("parse err: %s", err) - } - - n, err := util.LoadNodeN(i) - if err != nil { - return err - } - err = n.Shell() - handleErr("ipfs shell err: ", err) - return nil - }, -} - -var connectCmd = cli.Command{ - Name: "connect", - Usage: "connect two nodes together", - Action: func(c *cli.Context) error { - if len(c.Args()) < 2 { - fmt.Println("iptb connect [node] [node]") - os.Exit(1) - } - - nodes, err := util.LoadNodes() - if err != nil { - return err - } - - from, err := parseRange(c.Args()[0]) - if err != nil { - return fmt.Errorf("failed to parse: %s", err) - } - - to, err := parseRange(c.Args()[1]) - if err != nil { - return fmt.Errorf("failed to parse: %s", err) - } - - timeout := c.String("timeout") - - for _, f := range from { - for _, t := range to { - err = util.ConnectNodes(nodes[f], nodes[t], timeout) - if err != nil { - return fmt.Errorf("failed to connect: %s", err) - } - } - } - return nil - }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "timeout", - Usage: "timeout on the command", - }, - }, -} - -var getCmd = cli.Command{ - Name: "get", - Usage: "get an attribute of the given node", - Description: `Given an attribute name and a node number, prints the value of the attribute for the given node. - -You can get the list of valid attributes by passing no arguments.`, - Action: func(c *cli.Context) error { - showUsage := func(w io.Writer) { - fmt.Fprintln(w, "iptb get [attr] [node]") - fmt.Fprintln(w, "Valid values of [attr] are:") - attr_list := util.GetListOfAttr() - for _, a := range attr_list { - desc, err := util.GetAttrDescr(a) - handleErr("error getting attribute description: ", err) - fmt.Fprintf(w, "\t%s: %s\n", a, desc) - } - } - switch len(c.Args()) { - case 0: - showUsage(os.Stdout) - case 2: - attr := c.Args().First() - num, err := strconv.Atoi(c.Args()[1]) - handleErr("error parsing node number: ", err) - - ln, err := util.LoadNodeN(num) - if err != nil { - return err - } - - val, err := ln.GetAttr(attr) - handleErr("error getting attribute: ", err) - fmt.Println(val) - default: - fmt.Fprintln(os.Stderr, "'iptb get' accepts exactly 0 or 2 arguments") - showUsage(os.Stderr) - os.Exit(1) - } - return nil - }, -} - -var setCmd = cli.Command{ - Name: "set", - Usage: "set an attribute of the given node", - Action: func(c *cli.Context) error { - switch len(c.Args()) { - case 3: - attr := c.Args().First() - val := c.Args()[1] - nodes, err := parseRange(c.Args()[2]) - handleErr("error parsing node number: ", err) - - for _, i := range nodes { - ln, err := util.LoadNodeN(i) - if err != nil { - return err - } - - err = ln.SetAttr(attr, val) - if err != nil { - return fmt.Errorf("error setting attribute: %s", err) - } - } - default: - fmt.Fprintln(os.Stderr, "'iptb set' accepts exactly 3 arguments") - os.Exit(1) - } - return nil - }, -} - -var dumpStacksCmd = cli.Command{ - Name: "dump-stack", - Usage: "get a stack dump from the given daemon", - Action: func(c *cli.Context) error { - if len(c.Args()) < 1 { - fmt.Println("iptb dump-stack [node]") - os.Exit(1) - } - - num, err := strconv.Atoi(c.Args()[0]) - handleErr("error parsing node number: ", err) - - ln, err := util.LoadNodeN(num) - if err != nil { - return err - } - - addr, err := ln.APIAddr() - if err != nil { - return fmt.Errorf("failed to get api addr: %s", err) - } - - resp, err := http.Get("http://" + addr + "/debug/pprof/goroutine?debug=2") - handleErr("GET stack dump failed: ", err) - defer resp.Body.Close() - - io.Copy(os.Stdout, resp.Body) - return nil - }, -} - -var forEachCmd = cli.Command{ - Name: "for-each", - Usage: "run a given command on each node", - SkipFlagParsing: true, - Action: func(c *cli.Context) error { - nodes, err := util.LoadNodes() - if err != nil { - return err - } - - for _, n := range nodes { - out, err := n.RunCmd(c.Args()...) - if err != nil { - return err - } - fmt.Print(out) - } - return nil - }, -} - -var runCmd = cli.Command{ - Name: "run", - Usage: "run a command on a given node", - SkipFlagParsing: true, - Action: func(c *cli.Context) error { - n, err := strconv.Atoi(c.Args()[0]) - if err != nil { - return err - } - - nd, err := util.LoadNodeN(n) - if err != nil { - return err - } - - out, err := nd.RunCmd(c.Args()[1:]...) - if err != nil { - return err - } - fmt.Print(out) - return nil - }, -} - -var connGraphCmd = cli.Command{ - Name: "make-topology", - Usage: "Connect nodes according to the connection graph", - Description: "Connect all nodes according to specified network topology", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "input-topology", - Usage: "Specify connection graph, if none is specified a star topology will be used with center node 0", - }, - }, - Action: func(c *cli.Context) error { - // Load all nodes - nodes, err := util.LoadNodes() - if err != nil { - return err - } - graphDir := c.String("input-topology") - // If no input topology is given make default connection and move on - if len(graphDir) == 0 { - fmt.Println("No connection graph is specified, creating default star topology") - for i := 1; i < len(nodes); i++ { - err = util.ConnectNodes(nodes[0], nodes[i], "") - if err != nil { - return err - } - } - return nil - } - // If input topology is given parse and construct it - // Scan Input file Line by Line // - inFile, err := os.Open(graphDir) - defer inFile.Close() - if err != nil { - return err - } - scanner := bufio.NewScanner(inFile) - scanner.Split(bufio.ScanLines) - lineNumber := 1 - - for scanner.Scan() { - var destinations []string - var lineTokenized []string - line := scanner.Text() - // Check if the line is a comment or empty and skip it// - if len(line) == 0 || line[0] == '#' { - lineNumber++ - continue - } else { - lineTokenized = strings.Split(line, ":") - // Check if the format is correct - if len(lineTokenized) == 1 { - return errors.New("Line " + strconv.Itoa(lineNumber) + " does not follow the correct format") - } - destinations = strings.Split(lineTokenized[1], ",") - } - // Parse origin in the line - origin, err := strconv.Atoi(lineTokenized[0]) - // Check if it can be casted to integer - if err != nil { - return errors.New("Line: " + strconv.Itoa(lineNumber) + " of connection graph, could not be parsed") - } - // Check if the node is out of range - if origin >= len(nodes) { - return errors.New("Node origin in line: " + strconv.Itoa(lineNumber) + " out of range") - } + } + app.Before = func(c *cli.Context) error { + flagRoot := c.GlobalString("IPTB_ROOT") - for _, destination := range destinations { - // Check if it can be casted to integer - target, err := strconv.Atoi(destination) - if err != nil { - return errors.New("Check line: " + strconv.Itoa(lineNumber) + " of connection graph, could not be parsed") - } - // Check if the node is out of range - if target >= len(nodes) { - return errors.New("Node target in line: " + strconv.Itoa(lineNumber) + " out of range") - } - // Establish the connection - err = util.ConnectNodes(nodes[origin], nodes[target], "") - if err != nil { - fmt.Println("Connection failed!!!!") - return err - } + if len(flagRoot) == 0 { + home := os.Getenv("HOME") + if len(home) == 0 { + return fmt.Errorf("environment variable HOME not set") } - lineNumber++ - } - return nil - }, -} - -var distCmd = cli.Command{ - Name: "dist", - Usage: "distribute a file to all other nodes", - Description: "Distribute a single file to all nodes and calculate the statistics", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "hash", - Usage: "Hash of the file", - }, - }, - Action: func(c *cli.Context) error { - // Get the file hash and check if its correct - hash := c.String("hash") - if len(hash) == 0 { - return errors.New("No file hash is specified") - } - // Trim whitespace just in case - hash = strings.TrimSpace(hash) - // Load all Nodes - nodes, err := util.LoadNodes() - if err != nil { - return err - } - // Make a Happy print statement - fmt.Printf("=========== Simulation Begins ================ \n") - // Create channels to start asynchronous requests - ch := make(chan float64) - errorCh := make(chan error) - // Create an asynchronous request from each node - for i := 1; i < len(nodes); i++ { - fmt.Printf("Downloading file: %d / %d \n", i, len(nodes)-1) - go util.GetFile(hash, nodes[i], ch, errorCh) - } + flagRoot = path.Join(home, "testbed") + } else { + var err error - // Parse results - var delay []float64 - duplBlocks := 0 - for i := 1; i < len(nodes); i++ { - val := <-ch - err := <-errorCh - if err != nil { - return err - } - // Get delay and duplicate blocks// - delay = append(delay, val) - dB, err := getDupBlocksFromNode(i) + flagRoot, err = filepath.Abs(flagRoot) if err != nil { - fmt.Println("Failed to Parse duplicate blocks") return err } - duplBlocks += dB } - // Calculate Average Delay - var sum float64 - DelayMin := delay[0] - DelayMax := delay[0] - for _, d := range delay { - sum += d - // Calculate Min - if d < DelayMin { - DelayMin = d - } - // Calculate Max - if d > DelayMax { - DelayMax = d - } - } - avg := sum / float64(len(delay)) - // Calculate Delay Std - var sumSq float64 - for _, f := range delay { - sumSq += math.Pow((avg - f), 2) - } - std := math.Sqrt(sumSq / float64(len(delay))) - fmt.Printf("Average Time to distribute file to all nodes: %.4f\nStd Time to distribute file to all nodes %.4f\nDuplicate Blocks: %d\n", avg, std, duplBlocks) - // Save results to file - res := SimulationResults{avg, std, DelayMin, DelayMax, len(nodes) - 1, time.Now().UTC(), delay, duplBlocks} - res.ResultsSave() - return nil - }, -} + c.Set("IPTB_ROOT", flagRoot) -var logsCmd = cli.Command{ - Name: "logs", - Usage: "shows logs of given node(s), use '*' for all nodes", - Flags: []cli.Flag{ - cli.BoolTFlag{ - Name: "err", - Usage: "show stderr stream", - }, - cli.BoolTFlag{ - Name: "out", - Usage: "show stdout stream", - }, - cli.BoolFlag{ - Name: "s", - Usage: "don't show additional info, just the log", - }, - }, - Action: func(c *cli.Context) error { - var nodes []util.IpfsNode - var err error + return loadPlugins(path.Join(flagRoot, "plugins")) + } + app.Commands = []cli.Command{ + commands.AutoCmd, + commands.TestbedCmd, + + commands.InitCmd, + commands.StartCmd, + commands.StopCmd, + commands.RestartCmd, + commands.RunCmd, + commands.ConnectCmd, + commands.ShellCmd, + + commands.AttrCmd, + + commands.LogsCmd, + commands.EventsCmd, + commands.MetricCmd, + } - if c.Args()[0] == "*" { - nodes, err = util.LoadNodes() - if err != nil { - return err - } - } else { - for _, is := range c.Args() { - i, err := strconv.Atoi(is) - if err != nil { - return err - } - n, err := util.LoadNodeN(i) - if err != nil { - return err - } - nodes = append(nodes, n) + // https://github.com/urfave/cli/issues/736 + // Currently unreleased + /* + app.ExitErrHandler = func(c *cli.Context, err error) { + switch err.(type) { + case *commands.UsageError: + fmt.Fprintf(c.App.ErrWriter, "%s\n\n", err) + cli.ShowCommandHelpAndExit(c, c.Command.Name, 1) + default: + cli.HandleExitCoder(err) } } + */ - silent := c.Bool("s") - stderr := c.BoolT("err") - stdout := c.BoolT("out") - - for _, ns := range nodes { - n, ok := ns.(*util.LocalNode) - if !ok { - return errors.New("logs are supported only with local nodes") - } - if stdout { - if !silent { - fmt.Printf(">>>> %s", n.Dir) - fmt.Println("/daemon.stdout") - } - st, err := n.StderrReader() - if err != nil { - return err - } - io.Copy(os.Stdout, st) - st.Close() - if !silent { - fmt.Println("<<<<") - } - } - if stderr { - if !silent { - fmt.Printf(">>>> %s", n.Dir) - fmt.Println("/daemon.stderr") - } - st, err := n.StderrReader() - if err != nil { - return err - } - io.Copy(os.Stdout, st) - st.Close() - if !silent { - fmt.Println("<<<<") - } - } - } + app.ErrWriter = os.Stderr + app.Writer = os.Stdout - return nil - }, + err := app.Run(os.Args) + if err != nil { + fmt.Fprintf(app.ErrWriter, "%s\n", err) + os.Exit(1) + } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 21a8f96..14e5830 --- a/package.json +++ b/package.json @@ -7,18 +7,6 @@ "dvcsimport": "github.com/ipfs/iptb" }, "gxDependencies": [ - { - "author": "whyrusleeping", - "hash": "QmYmsdtJ3HsodkePE3eU3TsCaP2YvPZJ4LoXnNkDE5Tpt7", - "name": "go-multiaddr", - "version": "1.3.0" - }, - { - "author": "jbenet", - "hash": "QmV6FjemM1K8oXjrvuq3wuVWWoU2TLDPmNnKrxHzY3v6Ai", - "name": "go-multiaddr-net", - "version": "1.6.3" - }, { "author": "urfave", "hash": "Qmc1AtgBdoUHP8oYSqU81NRYdzohmF45t5XNwVMvhCxsBA", @@ -27,15 +15,9 @@ }, { "author": "whyrusleeping", - "hash": "QmZLUtHGe9HDQrreAYkXCzzK6mHVByV4MRd8heXAtV5wyS", - "name": "stump", - "version": "0.0.0" - }, - { - "author": "whyrusleeping", - "hash": "QmfEZa44SyWfyXpkbVfi19H1QpY73DU6E5omK2HbKXwqR6", - "name": "go-ctrlnet", - "version": "0.1.0" + "hash": "QmVmDhyTTUcQXFD1rRQ64fGLMSAoaQvNH3hwuaCFAPq2hy", + "name": "errors", + "version": "0.0.1" } ], "gxVersion": "0.6.0", @@ -44,6 +26,6 @@ "license": "", "name": "iptb", "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", - "version": "1.3.3" + "version": "1.3.7" } diff --git a/plugins/Makefile b/plugins/Makefile new file mode 100755 index 0000000..a520fd3 --- /dev/null +++ b/plugins/Makefile @@ -0,0 +1,15 @@ +IPTB_ROOT ?= $(HOME)/testbed + +all: ipfs + +install: + mkdir -p $(IPTB_ROOT)/plugins + cp *.so $(IPTB_ROOT)/plugins + +ipfs: + make -C ipfs all + +clean: + rm *.so + +.PHONY: all ipfs clean diff --git a/plugins/ipfs/Makefile b/plugins/ipfs/Makefile new file mode 100755 index 0000000..ccd84f9 --- /dev/null +++ b/plugins/ipfs/Makefile @@ -0,0 +1,15 @@ +all: ipfslocal ipfsdocker + +ipfslocal: + gx-go rw + (cd local; go build -buildmode=plugin -o ../../localipfs.so) + gx-go uw +CLEAN += localipfs.so + +ipfsdocker: + gx-go rw + (cd docker; go build -buildmode=plugin -o ../../dockeripfs.so) + gx-go uw +CLEAN += dockeripfs.so + +.PHONY: all ipfslocal ipfsdocker diff --git a/plugins/ipfs/docker/dockeripfs.go b/plugins/ipfs/docker/dockeripfs.go new file mode 100755 index 0000000..f8bcf29 --- /dev/null +++ b/plugins/ipfs/docker/dockeripfs.go @@ -0,0 +1,610 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/ipfs/go-cid" + config "github.com/ipfs/go-ipfs-config" + serial "github.com/ipfs/go-ipfs-config/serialize" + "github.com/multiformats/go-multiaddr" + "github.com/pkg/errors" + cnet "github.com/whyrusleeping/go-ctrlnet" + + "github.com/ipfs/iptb/plugins/ipfs" + "github.com/ipfs/iptb/testbed/interfaces" + "github.com/ipfs/iptb/util" +) + +var ErrTimeout = errors.New("timeout") + +var PluginName = "dockeripfs" + +const ( + attrIfName = "ifname" +) + +type DockerIpfs struct { + image string + id string + dir string + repobuilder string + peerid *cid.Cid + apiaddr multiaddr.Multiaddr + swarmaddr multiaddr.Multiaddr + mdns bool +} + +var NewNode testbedi.NewNodeFunc +var GetAttrDesc testbedi.GetAttrDescFunc +var GetAttrList testbedi.GetAttrListFunc + +func init() { + NewNode = func(dir string, attrs map[string]string) (testbedi.Core, error) { + imagename := "ipfs/go-ipfs" + mdns := false + + apiaddr, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/5001") + if err != nil { + return nil, err + } + + swarmaddr, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/4001") + if err != nil { + return nil, err + } + + var repobuilder string + + if v, ok := attrs["image"]; ok { + imagename = v + } + + if v, ok := attrs["repobuilder"]; ok { + repobuilder = v + } else { + ipfspath, err := exec.LookPath("ipfs") + if err != nil { + return nil, fmt.Errorf("No `repobuilder` provided, could not find ipfs in path") + } + + repobuilder = ipfspath + } + + if apiaddrstr, ok := attrs["apiaddr"]; ok { + var err error + apiaddr, err = multiaddr.NewMultiaddr(apiaddrstr) + + if err != nil { + return nil, err + } + } + + if swarmaddrstr, ok := attrs["swarmaddr"]; ok { + var err error + swarmaddr, err = multiaddr.NewMultiaddr(swarmaddrstr) + + if err != nil { + return nil, err + } + } + + if _, ok := attrs["mdns"]; ok { + mdns = true + } + + return &DockerIpfs{ + dir: dir, + image: imagename, + repobuilder: repobuilder, + apiaddr: apiaddr, + swarmaddr: swarmaddr, + mdns: mdns, + }, nil + } + + GetAttrList = func() []string { + return append(ipfs.GetAttrList(), attrIfName) + } + + GetAttrDesc = func(attr string) (string, error) { + switch attr { + case attrIfName: + return "docker ifname", nil + } + + return ipfs.GetAttrDesc(attr) + } +} + +func GetMetricList() []string { + return ipfs.GetMetricList() +} + +func GetMetricDesc(attr string) (string, error) { + return ipfs.GetMetricDesc(attr) +} + +/// Core Interface + +func (l *DockerIpfs) Init(ctx context.Context, agrs ...string) (testbedi.Output, error) { + env, err := l.env() + if err != nil { + return nil, fmt.Errorf("error getting env: %s", err) + } + + cmd := exec.CommandContext(ctx, l.repobuilder, "init") + cmd.Env = env + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%s: %s", err, string(out)) + } + + icfg, err := l.Config() + if err != nil { + return nil, err + } + + lcfg, ok := icfg.(*config.Config) + if !ok { + return nil, fmt.Errorf("Error: Config() is not an ipfs config") + } + + lcfg.Bootstrap = nil + lcfg.Addresses.Swarm = []string{l.swarmaddr.String()} + lcfg.Addresses.API = l.apiaddr.String() + lcfg.Addresses.Gateway = "" + lcfg.Discovery.MDNS.Enabled = l.mdns + + err = l.WriteConfig(lcfg) + if err != nil { + return nil, err + } + + return nil, err +} + +func (l *DockerIpfs) Start(ctx context.Context, wait bool, args ...string) (testbedi.Output, error) { + alive, err := l.isAlive() + if err != nil { + return nil, err + } + + if alive { + return nil, fmt.Errorf("node is already running") + } + + fargs := []string{"run", "-d", "-v", l.dir + ":/data/ipfs", l.image} + fargs = append(fargs, args...) + cmd := exec.Command("docker", fargs...) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%s: %s", err, string(out)) + } + + id := bytes.TrimSpace(out) + l.id = string(id) + + idfile := filepath.Join(l.dir, "dockerid") + err = ioutil.WriteFile(idfile, id, 0664) + + if err != nil { + killErr := l.killContainer() + if killErr != nil { + return nil, combineErrors(err, killErr) + } + return nil, err + } + + return nil, nil +} + +func (l *DockerIpfs) Stop(ctx context.Context) error { + err := l.killContainer() + if err != nil { + return err + } + return os.Remove(filepath.Join(l.dir, "dockerid")) +} + +func (l *DockerIpfs) RunCmd(ctx context.Context, stdin io.Reader, args ...string) (testbedi.Output, error) { + id, err := l.getID() + if err != nil { + return nil, err + } + + if stdin != nil { + args = append([]string{"exec", "-i", id}, args...) + } else { + args = append([]string{"exec", id}, args...) + } + + cmd := exec.CommandContext(ctx, "docker", args...) + cmd.Stdin = stdin + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + stderrbytes, err := ioutil.ReadAll(stderr) + if err != nil { + return nil, err + } + + stdoutbytes, err := ioutil.ReadAll(stdout) + if err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + + exiterr := cmd.Wait() + + var exitcode = 0 + switch oerr := exiterr.(type) { + case *exec.ExitError: + if ctx.Err() == context.DeadlineExceeded { + err = errors.Wrapf(oerr, "context deadline exceeded for command: %q", strings.Join(cmd.Args, " ")) + } + + exitcode = 1 + case nil: + err = oerr + } + + return iptbutil.NewOutput(args, stdoutbytes, stderrbytes, exitcode, err), nil +} + +func (l *DockerIpfs) Connect(ctx context.Context, n testbedi.Core) error { + swarmaddrs, err := n.SwarmAddrs() + if err != nil { + return err + } + + output, err := l.RunCmd(ctx, nil, "ipfs", "swarm", "connect", swarmaddrs[1]) + + if err != nil { + return err + } + + if output.ExitCode() != 0 { + out, err := ioutil.ReadAll(output.Stderr()) + if err != nil { + return err + } + + return fmt.Errorf("%s", string(out)) + } + + return nil +} + +func (l *DockerIpfs) Shell(ctx context.Context, nodes []testbedi.Core) error { + id, err := l.getID() + if err != nil { + return err + } + + nenvs := []string{} + for i, n := range nodes { + peerid, err := n.PeerID() + + if err != nil { + return err + } + + nenvs = append(nenvs, fmt.Sprintf("NODE%d=%s", i, peerid)) + } + + args := []string{"exec", "-it"} + for _, e := range nenvs { + args = append(args, "-e", e) + } + + args = append(args, id, "/bin/sh") + cmd := exec.Command("docker", args...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + + return cmd.Run() +} + +func (l *DockerIpfs) String() string { + pcid, err := l.PeerID() + if err != nil { + return fmt.Sprintf("%s", l.Type()) + } + return fmt.Sprintf("%s", pcid[0:12]) +} + +func (l *DockerIpfs) APIAddr() (string, error) { + return ipfs.GetAPIAddrFromRepo(l.dir) +} + +func (l *DockerIpfs) SwarmAddrs() ([]string, error) { + return ipfs.SwarmAddrs(l) +} + +func (l *DockerIpfs) Dir() string { + return l.dir +} + +func (l *DockerIpfs) PeerID() (string, error) { + if l.peerid != nil { + return l.peerid.String(), nil + } + + var err error + l.peerid, err = ipfs.GetPeerID(l) + + if err != nil { + return "", err + } + + return l.peerid.String(), nil +} + +// Metric Interface + +func (l *DockerIpfs) GetMetricList() []string { + return GetMetricList() +} + +func (l *DockerIpfs) GetMetricDesc(attr string) (string, error) { + return GetMetricDesc(attr) +} + +func (l *DockerIpfs) Metric(metric string) (string, error) { + return ipfs.GetMetric(l, metric) +} + +func (l *DockerIpfs) Heartbeat() (map[string]string, error) { + return nil, nil +} + +func (l *DockerIpfs) Events() (io.ReadCloser, error) { + return ipfs.ReadLogs(l) +} + +func (l *DockerIpfs) Logs() (io.ReadCloser, error) { + return nil, fmt.Errorf("not implemented") +} + +// Attribute Interface + +func (l *DockerIpfs) GetAttrList() []string { + return GetAttrList() +} + +func (l *DockerIpfs) GetAttrDesc(attr string) (string, error) { + return GetAttrDesc(attr) +} + +func (l *DockerIpfs) Attr(attr string) (string, error) { + switch attr { + case attrIfName: + return l.getInterfaceName() + } + + return ipfs.GetAttr(l, attr) +} + +func (l *DockerIpfs) SetAttr(attr string, val string) error { + switch attr { + case "latency": + return l.setLatency(val) + case "bandwidth": + return l.setBandwidth(val) + case "jitter": + return l.setJitter(val) + case "loss": + return l.setPacketLoss(val) + default: + return fmt.Errorf("no attribute named: %s", attr) + } +} + +func (l *DockerIpfs) StderrReader() (io.ReadCloser, error) { + return nil, fmt.Errorf("Not implemented") +} + +func (l *DockerIpfs) StdoutReader() (io.ReadCloser, error) { + return nil, fmt.Errorf("Not implemented") +} + +func (l *DockerIpfs) Config() (interface{}, error) { + return serial.Load(filepath.Join(l.dir, "config")) +} + +func (l *DockerIpfs) WriteConfig(cfg interface{}) error { + return serial.WriteConfigFile(filepath.Join(l.dir, "config"), cfg) +} + +func (l *DockerIpfs) Type() string { + return "ipfs" +} + +func (l *DockerIpfs) Deployment() string { + return "docker" +} + +func (l *DockerIpfs) getID() (string, error) { + if len(l.id) != 0 { + return l.id, nil + } + + b, err := ioutil.ReadFile(filepath.Join(l.dir, "dockerid")) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (l *DockerIpfs) isAlive() (bool, error) { + return false, nil +} + +func (l *DockerIpfs) env() ([]string, error) { + envs := os.Environ() + ipfspath := "IPFS_PATH=" + l.dir + + for i, e := range envs { + if strings.HasPrefix(e, "IPFS_PATH=") { + envs[i] = ipfspath + return envs, nil + } + } + return append(envs, ipfspath), nil +} + +func (l *DockerIpfs) killContainer() error { + id, err := l.getID() + if err != nil { + return err + } + out, err := exec.Command("docker", "kill", "--signal=INT", id).CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %s", err, string(out)) + } + return nil +} + +func (l *DockerIpfs) getInterfaceName() (string, error) { + out, err := l.RunCmd(context.TODO(), nil, "ip", "link") + if err != nil { + return "", err + } + + stdout, err := ioutil.ReadAll(out.Stdout()) + if err != nil { + return "", err + } + + var cside string + for _, l := range strings.Split(string(stdout), "\n") { + if strings.Contains(l, "@if") { + ifnum := strings.Split(strings.Split(l, " ")[1], "@")[1] + cside = ifnum[2 : len(ifnum)-1] + break + } + } + + if cside == "" { + return "", fmt.Errorf("container-side interface not found") + } + + localout, err := exec.Command("ip", "link").CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s: %s", err, localout) + } + + for _, l := range strings.Split(string(localout), "\n") { + if strings.HasPrefix(l, cside+": ") { + return strings.Split(strings.Fields(l)[1], "@")[0], nil + } + } + + return "", fmt.Errorf("could not determine interface") +} + +func (l *DockerIpfs) setLatency(val string) error { + dur, err := time.ParseDuration(val) + if err != nil { + return err + } + + ifn, err := l.getInterfaceName() + if err != nil { + return err + } + + settings := &cnet.LinkSettings{ + Latency: uint(dur.Nanoseconds() / 1000000), + } + + return cnet.SetLink(ifn, settings) +} + +func (l *DockerIpfs) setJitter(val string) error { + dur, err := time.ParseDuration(val) + if err != nil { + return err + } + + ifn, err := l.getInterfaceName() + if err != nil { + return err + } + + settings := &cnet.LinkSettings{ + Jitter: uint(dur.Nanoseconds() / 1000000), + } + + return cnet.SetLink(ifn, settings) +} + +// set bandwidth (expects Mbps) +func (l *DockerIpfs) setBandwidth(val string) error { + bw, err := strconv.ParseFloat(val, 32) + if err != nil { + return err + } + + ifn, err := l.getInterfaceName() + if err != nil { + return err + } + + settings := &cnet.LinkSettings{ + Bandwidth: uint(bw * 1000000), + } + + return cnet.SetLink(ifn, settings) +} + +// set packet loss percentage (dropped / total) +func (l *DockerIpfs) setPacketLoss(val string) error { + ratio, err := strconv.ParseUint(val, 10, 8) + if err != nil { + return err + } + + ifn, err := l.getInterfaceName() + if err != nil { + return err + } + + settings := &cnet.LinkSettings{ + PacketLoss: uint8(ratio), + } + + return cnet.SetLink(ifn, settings) +} + +func combineErrors(err1, err2 error) error { + return fmt.Errorf("%v\nwhile handling the above error, the following error occurred:\n%v", err1, err2) +} diff --git a/plugins/ipfs/local/localipfs.go b/plugins/ipfs/local/localipfs.go new file mode 100755 index 0000000..4e64f6c --- /dev/null +++ b/plugins/ipfs/local/localipfs.go @@ -0,0 +1,512 @@ +package main + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/ipfs/go-cid" + config "github.com/ipfs/go-ipfs-config" + serial "github.com/ipfs/go-ipfs-config/serialize" + "github.com/ipfs/iptb/plugins/ipfs" + "github.com/ipfs/iptb/testbed/interfaces" + "github.com/ipfs/iptb/util" + "github.com/multiformats/go-multiaddr" + "github.com/pkg/errors" +) + +var errTimeout = errors.New("timeout") + +var PluginName = "localipfs" + +type LocalIpfs struct { + dir string + peerid *cid.Cid + apiaddr multiaddr.Multiaddr + swarmaddr multiaddr.Multiaddr + mdns bool +} + +var NewNode testbedi.NewNodeFunc +var GetAttrDesc testbedi.GetAttrDescFunc +var GetAttrList testbedi.GetAttrListFunc + +func init() { + NewNode = func(dir string, attrs map[string]string) (testbedi.Core, error) { + mdns := false + + if _, err := exec.LookPath("ipfs"); err != nil { + return nil, err + } + + apiaddr, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/0") + if err != nil { + return nil, err + } + + swarmaddr, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/0") + if err != nil { + return nil, err + } + + if apiaddrstr, ok := attrs["apiaddr"]; ok { + var err error + apiaddr, err = multiaddr.NewMultiaddr(apiaddrstr) + + if err != nil { + return nil, err + } + } + + if swarmaddrstr, ok := attrs["swarmaddr"]; ok { + var err error + swarmaddr, err = multiaddr.NewMultiaddr(swarmaddrstr) + + if err != nil { + return nil, err + } + } + + if _, ok := attrs["mdns"]; ok { + mdns = true + } + + return &LocalIpfs{ + dir: dir, + apiaddr: apiaddr, + swarmaddr: swarmaddr, + mdns: mdns, + }, nil + + } + + GetAttrList = func() []string { + return ipfs.GetAttrList() + } + + GetAttrDesc = func(attr string) (string, error) { + return ipfs.GetAttrDesc(attr) + } + +} + +func GetMetricList() []string { + return ipfs.GetMetricList() +} + +func GetMetricDesc(attr string) (string, error) { + return ipfs.GetMetricDesc(attr) +} + +/// TestbedNode Interface + +func (l *LocalIpfs) Init(ctx context.Context, agrs ...string) (testbedi.Output, error) { + agrs = append([]string{"ipfs", "init"}, agrs...) + output, oerr := l.RunCmd(ctx, nil, agrs...) + if oerr != nil { + return nil, oerr + } + + icfg, err := l.Config() + if err != nil { + return nil, err + } + + lcfg := icfg.(*config.Config) + + lcfg.Bootstrap = nil + lcfg.Addresses.Swarm = []string{l.swarmaddr.String()} + lcfg.Addresses.API = l.apiaddr.String() + lcfg.Addresses.Gateway = "" + lcfg.Discovery.MDNS.Enabled = l.mdns + + err = l.WriteConfig(lcfg) + if err != nil { + return nil, err + } + + return output, oerr +} + +func (l *LocalIpfs) Start(ctx context.Context, wait bool, args ...string) (testbedi.Output, error) { + alive, err := l.isAlive() + if err != nil { + return nil, err + } + + if alive { + return nil, fmt.Errorf("node is already running") + } + + dir := l.dir + dargs := append([]string{"daemon"}, args...) + cmd := exec.Command("ipfs", dargs...) + cmd.Dir = dir + + cmd.Env, err = l.env() + if err != nil { + return nil, err + } + + iptbutil.SetupOpt(cmd) + + stdout, err := os.Create(filepath.Join(dir, "daemon.stdout")) + if err != nil { + return nil, err + } + + stderr, err := os.Create(filepath.Join(dir, "daemon.stderr")) + if err != nil { + return nil, err + } + + cmd.Stdout = stdout + cmd.Stderr = stderr + + err = cmd.Start() + if err != nil { + return nil, err + } + + pid := cmd.Process.Pid + + err = ioutil.WriteFile(filepath.Join(dir, "daemon.pid"), []byte(fmt.Sprint(pid)), 0666) + if err != nil { + return nil, err + } + + if wait { + return nil, ipfs.WaitOnAPI(l) + } + + return nil, nil +} + +func (l *LocalIpfs) Stop(ctx context.Context) error { + pid, err := l.getPID() + if err != nil { + return fmt.Errorf("error killing daemon %s: %s", l.dir, err) + } + + p, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("error killing daemon %s: %s", l.dir, err) + } + + waitch := make(chan struct{}, 1) + go func() { + p.Wait() + waitch <- struct{}{} + }() + + defer func() { + err := os.Remove(filepath.Join(l.dir, "daemon.pid")) + if err != nil && !os.IsNotExist(err) { + panic(fmt.Errorf("error removing pid file for daemon at %s: %s", l.dir, err)) + } + }() + + if err := l.signalAndWait(p, waitch, syscall.SIGTERM, 1*time.Second); err != errTimeout { + return err + } + + if err := l.signalAndWait(p, waitch, syscall.SIGTERM, 2*time.Second); err != errTimeout { + return err + } + + if err := l.signalAndWait(p, waitch, syscall.SIGQUIT, 5*time.Second); err != errTimeout { + return err + } + + if err := l.signalAndWait(p, waitch, syscall.SIGKILL, 5*time.Second); err != errTimeout { + return err + } + + return fmt.Errorf("Could not stop localipfs node with pid %d", pid) +} + +func (l *LocalIpfs) RunCmd(ctx context.Context, stdin io.Reader, args ...string) (testbedi.Output, error) { + env, err := l.env() + + if err != nil { + return nil, fmt.Errorf("error getting env: %s", err) + } + + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + cmd.Env = env + cmd.Stdin = stdin + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + stderrbytes, err := ioutil.ReadAll(stderr) + if err != nil { + return nil, err + } + + stdoutbytes, err := ioutil.ReadAll(stdout) + if err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + + exiterr := cmd.Wait() + + var exitcode = 0 + switch oerr := exiterr.(type) { + case *exec.ExitError: + if ctx.Err() == context.DeadlineExceeded { + err = errors.Wrapf(oerr, "context deadline exceeded for command: %q", strings.Join(cmd.Args, " ")) + } + + exitcode = 1 + case nil: + err = oerr + } + + return iptbutil.NewOutput(args, stdoutbytes, stderrbytes, exitcode, err), nil +} + +func (l *LocalIpfs) Connect(ctx context.Context, tbn testbedi.Core) error { + swarmaddrs, err := tbn.SwarmAddrs() + if err != nil { + return err + } + + output, err := l.RunCmd(ctx, nil, "ipfs", "swarm", "connect", swarmaddrs[0]) + + if err != nil { + return err + } + + if output.ExitCode() != 0 { + out, err := ioutil.ReadAll(output.Stderr()) + if err != nil { + return err + } + + return fmt.Errorf("%s", string(out)) + } + + return nil +} + +func (l *LocalIpfs) Shell(ctx context.Context, nodes []testbedi.Core) error { + shell := os.Getenv("SHELL") + if shell == "" { + return fmt.Errorf("no shell found") + } + + if len(os.Getenv("IPFS_PATH")) != 0 { + // If the users shell sets IPFS_PATH, it will just be overridden by the shell again + return fmt.Errorf("shell has IPFS_PATH set, please unset before trying to use iptb shell") + } + + nenvs, err := l.env() + if err != nil { + return err + } + + // TODO(tperson): It would be great if we could guarantee that the shell + // is using the same binary. However, the users shell may prepend anything + // we change in the PATH + + for i, n := range nodes { + peerid, err := n.PeerID() + + if err != nil { + return err + } + + nenvs = append(nenvs, fmt.Sprintf("NODE%d=%s", i, peerid)) + } + + return syscall.Exec(shell, []string{shell}, nenvs) +} + +func (l *LocalIpfs) String() string { + pcid, err := l.PeerID() + if err != nil { + return fmt.Sprintf("%s", l.Type()) + } + return fmt.Sprintf("%s", pcid[0:12]) +} + +func (l *LocalIpfs) APIAddr() (string, error) { + return ipfs.GetAPIAddrFromRepo(l.dir) +} + +func (l *LocalIpfs) SwarmAddrs() ([]string, error) { + return ipfs.SwarmAddrs(l) +} + +func (l *LocalIpfs) Dir() string { + return l.dir +} + +func (l *LocalIpfs) PeerID() (string, error) { + if l.peerid != nil { + return l.peerid.String(), nil + } + + var err error + l.peerid, err = ipfs.GetPeerID(l) + + if err != nil { + return "", err + } + + return l.peerid.String(), nil +} + +/// Metric Interface + +func (l *LocalIpfs) GetMetricList() []string { + return GetMetricList() +} + +func (l *LocalIpfs) GetMetricDesc(attr string) (string, error) { + return GetMetricDesc(attr) +} + +func (l *LocalIpfs) Metric(metric string) (string, error) { + return ipfs.GetMetric(l, metric) +} + +func (l *LocalIpfs) Heartbeat() (map[string]string, error) { + return nil, nil +} + +func (l *LocalIpfs) Events() (io.ReadCloser, error) { + return ipfs.ReadLogs(l) +} + +func (l *LocalIpfs) Logs() (io.ReadCloser, error) { + return nil, fmt.Errorf("not implemented") +} + +// Attribute Interface + +func (l *LocalIpfs) GetAttrList() []string { + return GetAttrList() +} + +func (l *LocalIpfs) GetAttrDesc(attr string) (string, error) { + return GetAttrDesc(attr) +} + +func (l *LocalIpfs) GetAttr(attr string) (string, error) { + return ipfs.GetAttr(l, attr) +} + +func (l *LocalIpfs) SetAttr(string, string) error { + return fmt.Errorf("no attribute to set") +} + +func (l *LocalIpfs) StderrReader() (io.ReadCloser, error) { + return l.readerFor("daemon.stderr") +} + +func (l *LocalIpfs) StdoutReader() (io.ReadCloser, error) { + return l.readerFor("daemon.stdout") +} + +func (l *LocalIpfs) Config() (interface{}, error) { + return serial.Load(filepath.Join(l.dir, "config")) +} + +func (l *LocalIpfs) WriteConfig(cfg interface{}) error { + return serial.WriteConfigFile(filepath.Join(l.dir, "config"), cfg) +} + +func (l *LocalIpfs) Type() string { + return "ipfs" +} + +func (l *LocalIpfs) Deployment() string { + return "local" +} + +func (l *LocalIpfs) readerFor(file string) (io.ReadCloser, error) { + return os.OpenFile(filepath.Join(l.dir, file), os.O_RDONLY, 0) +} + +func (l *LocalIpfs) signalAndWait(p *os.Process, waitch <-chan struct{}, signal os.Signal, t time.Duration) error { + err := p.Signal(signal) + if err != nil { + return fmt.Errorf("error killing daemon %s: %s", l.dir, err) + } + + select { + case <-waitch: + return nil + case <-time.After(t): + return errTimeout + } +} + +func (l *LocalIpfs) getPID() (int, error) { + b, err := ioutil.ReadFile(filepath.Join(l.dir, "daemon.pid")) + if err != nil { + return -1, err + } + + return strconv.Atoi(string(b)) +} + +func (l *LocalIpfs) isAlive() (bool, error) { + pid, err := l.getPID() + if os.IsNotExist(err) { + return false, nil + } else if err != nil { + return false, err + } + + proc, err := os.FindProcess(pid) + if err != nil { + return false, nil + } + + err = proc.Signal(syscall.Signal(0)) + if err == nil { + return true, nil + } + + return false, nil +} + +func (l *LocalIpfs) env() ([]string, error) { + envs := os.Environ() + ipfspath := "IPFS_PATH=" + l.dir + + for i, e := range envs { + if strings.HasPrefix(e, "IPFS_PATH=") { + envs[i] = ipfspath + return envs, nil + } + } + return append(envs, ipfspath), nil +} diff --git a/plugins/ipfs/package.json b/plugins/ipfs/package.json new file mode 100755 index 0000000..f28efdf --- /dev/null +++ b/plugins/ipfs/package.json @@ -0,0 +1,61 @@ +{ + "author": "whyrusleeping", + "bugs": { + "url": "https://github.com/ipfs/iptb" + }, + "gx": { + "dvcsimport": "github.com/ipfs/iptb/plugins/ipfs" + }, + "gxDependencies": [ + { + "author": "whyrusleeping", + "hash": "QmYmsdtJ3HsodkePE3eU3TsCaP2YvPZJ4LoXnNkDE5Tpt7", + "name": "go-multiaddr", + "version": "1.3.0" + }, + { + "author": "jbenet", + "hash": "QmV6FjemM1K8oXjrvuq3wuVWWoU2TLDPmNnKrxHzY3v6Ai", + "name": "go-multiaddr-net", + "version": "1.6.3" + }, + { + "author": "whyrusleeping", + "hash": "QmY51bqSM5XgxQZqsBrQcRkKTnCb8EKpJpR9K6Qax7Njco", + "name": "go-libp2p", + "version": "6.0.6" + }, + { + "author": "whyrusleeping", + "hash": "QmfEZa44SyWfyXpkbVfi19H1QpY73DU6E5omK2HbKXwqR6", + "name": "go-ctrlnet", + "version": "0.1.0" + }, + { + "author": "magik6k", + "hash": "QmYyFh6g1C9uieTpH8CR8PpWBUQjvMDJTsRhJWx5qkXy39", + "name": "go-ipfs-config", + "version": "0.2.2" + }, + { + "author": "whyrusleeping", + "hash": "QmVmDhyTTUcQXFD1rRQ64fGLMSAoaQvNH3hwuaCFAPq2hy", + "name": "errors", + "version": "0.0.1" + }, + { + "author": "whyrusleeping", + "hash": "QmYVNvtQkeZ6AKSwDrjQTs432QtL6umrrK41EBq3cu7iSP", + "name": "go-cid", + "version": "0.7.22" + } + ], + "gxVersion": "0.6.0", + "issues_url": "", + "language": "go", + "license": "", + "name": "ipfs-iptb-plugins", + "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", + "version": "0.1.0" +} + diff --git a/plugins/ipfs/util.go b/plugins/ipfs/util.go new file mode 100755 index 0000000..cbf70b2 --- /dev/null +++ b/plugins/ipfs/util.go @@ -0,0 +1,289 @@ +package ipfs + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/ipfs/go-cid" + config "github.com/ipfs/go-ipfs-config" + _ "github.com/libp2p/go-libp2p" + "github.com/multiformats/go-multiaddr" + "github.com/pkg/errors" + + "github.com/ipfs/iptb/testbed/interfaces" +) + +const ( + attrID = "id" + attrPath = "path" + + metricBwIn = "bw_in" + metricBwOut = "bw_out" +) + +func InitIpfs(l testbedi.Core) error { + return nil +} + +func GetAttr(l testbedi.Core, attr string) (string, error) { + switch attr { + case attrID: + pcid, err := l.PeerID() + if err != nil { + return "", err + } + return pcid, nil + case attrPath: + return l.Dir(), nil + default: + return "", errors.New("unrecognized attribute: " + attr) + } +} + +func GetMetric(l testbedi.Core, metric string) (string, error) { + switch metric { + case metricBwIn: + bw, err := GetBW(l) + if err != nil { + return "", err + } + return fmt.Sprint(bw.TotalIn), nil + case metricBwOut: + bw, err := GetBW(l) + if err != nil { + return "", err + } + return fmt.Sprint(bw.TotalOut), nil + default: + return "", errors.New("unrecognized metric: " + metric) + } +} + +func GetPeerID(l testbedi.Config) (*cid.Cid, error) { + icfg, err := l.Config() + if err != nil { + return nil, err + } + + lcfg, ok := icfg.(*config.Config) + if !ok { + return nil, fmt.Errorf("Error: GetConfig() is not an ipfs config") + } + + pcid, err := cid.Decode(lcfg.Identity.PeerID) + if err != nil { + return nil, err + } + + return pcid, nil +} + +func GetMetricList() []string { + return []string{metricBwIn, metricBwOut} +} + +func GetMetricDesc(metric string) (string, error) { + switch metric { + case metricBwIn: + return "node input bandwidth", nil + case metricBwOut: + return "node output bandwidth", nil + default: + return "", errors.New("unrecognized metric") + } +} + +func GetAttrList() []string { + return []string{attrID, attrPath} +} + +func GetAttrDesc(attr string) (string, error) { + switch attr { + case attrID: + return "node ID", nil + case attrPath: + return "node IPFS_PATH", nil + default: + return "", errors.New("unrecognized attribute") + } +} + +func ReadLogs(l testbedi.Libp2p) (io.ReadCloser, error) { + addrStr, err := l.APIAddr() + if err != nil { + return nil, err + } + + addr, err := multiaddr.NewMultiaddr(addrStr) + if err != nil { + return nil, err + } + + //TODO(tperson) ipv6 + ip, err := addr.ValueForProtocol(multiaddr.P_IP4) + if err != nil { + return nil, err + } + pt, err := addr.ValueForProtocol(multiaddr.P_TCP) + if err != nil { + return nil, err + } + + resp, err := http.Get(fmt.Sprintf("http://%s:%s/api/v0/log/tail", ip, pt)) + if err != nil { + return nil, err + } + + return resp.Body, nil +} + +type BW struct { + TotalIn int + TotalOut int +} + +func GetBW(l testbedi.Libp2p) (*BW, error) { + addrStr, err := l.APIAddr() + if err != nil { + return nil, err + } + + addr, err := multiaddr.NewMultiaddr(addrStr) + if err != nil { + return nil, err + } + + //TODO(tperson) ipv6 + ip, err := addr.ValueForProtocol(multiaddr.P_IP4) + if err != nil { + return nil, err + } + pt, err := addr.ValueForProtocol(multiaddr.P_TCP) + if err != nil { + return nil, err + } + + resp, err := http.Get(fmt.Sprintf("http://%s:%s/api/v0/stats/bw", ip, pt)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var bw BW + err = json.NewDecoder(resp.Body).Decode(&bw) + if err != nil { + return nil, err + } + + return &bw, nil +} + +func GetAPIAddrFromRepo(dir string) (string, error) { + out, err := ioutil.ReadFile(filepath.Join(dir, "api")) + return string(out), err +} + +func SwarmAddrs(l testbedi.Core) ([]string, error) { + pcid, err := l.PeerID() + if err != nil { + return nil, err + } + + output, err := l.RunCmd(context.TODO(), nil, "ipfs", "swarm", "addrs", "local") + if err != nil { + return nil, err + } + + bs, err := ioutil.ReadAll(output.Stdout()) + if err != nil { + return nil, err + } + + straddrs := strings.Split(string(bs), "\n") + + var maddrs []string + for _, straddr := range straddrs { + fstraddr := fmt.Sprintf("%s/ipfs/%s", straddr, pcid) + maddrs = append(maddrs, fstraddr) + } + + return maddrs, nil +} + +func WaitOnAPI(l testbedi.Libp2p) error { + for i := 0; i < 50; i++ { + err := tryAPICheck(l) + if err == nil { + return nil + } + time.Sleep(time.Millisecond * 400) + } + + pcid, err := l.PeerID() + if err != nil { + return err + } + + return fmt.Errorf("node %s failed to come online in given time period", pcid) +} + +func tryAPICheck(l testbedi.Libp2p) error { + addrStr, err := l.APIAddr() + if err != nil { + return err + } + + addr, err := multiaddr.NewMultiaddr(addrStr) + if err != nil { + return err + } + + //TODO(tperson) ipv6 + ip, err := addr.ValueForProtocol(multiaddr.P_IP4) + if err != nil { + return err + } + pt, err := addr.ValueForProtocol(multiaddr.P_TCP) + if err != nil { + return err + } + + resp, err := http.Get(fmt.Sprintf("http://%s:%s/api/v0/id", ip, pt)) + if err != nil { + return err + } + + out := make(map[string]interface{}) + err = json.NewDecoder(resp.Body).Decode(&out) + if err != nil { + return fmt.Errorf("liveness check failed: %s", err) + } + + id, ok := out["ID"] + if !ok { + return fmt.Errorf("liveness check failed: ID field not present in output") + } + + pcid, err := l.PeerID() + if err != nil { + return err + } + + idstr, ok := id.(string) + if !ok { + return fmt.Errorf("liveness check failed: ID field is unexpected type") + } + + if idstr != pcid { + return fmt.Errorf("liveness check failed: unexpected peer at endpoint") + } + + return nil +} diff --git a/sharness/.gitignore b/sharness/.gitignore old mode 100644 new mode 100755 diff --git a/sharness/Makefile b/sharness/Makefile old mode 100644 new mode 100755 index 6049c29..f84f587 --- a/sharness/Makefile +++ b/sharness/Makefile @@ -24,6 +24,7 @@ all: aggregate clean: clean-test-results @echo "*** $@ ***" -rm -rf $(BINS) + -rm -rf plugins clean-test-results: @echo "*** $@ ***" @@ -38,7 +39,7 @@ aggregate: clean-test-results $(T) ls test-results/t*-*.sh.*.counts | $(AGGREGATE) # Needed dependencies. -deps: sharness $(BINS) +deps: sharness $(BINS) plugins sharness: @echo "*** checking $@ ***" @@ -50,6 +51,10 @@ bin/iptb: $(call find_go_files, $(IPTB_SRC)) BUILD-OPTIONS @echo "*** installing $@ ***" go build $(GOFLAGS) -o $@ $(IPTB_SRC) +plugins: + make -C ../plugins all + make -C ../plugins install IPTB_ROOT=$(shell pwd) + race: make GOFLAGS=-race all diff --git a/sharness/lib/test-lib.sh b/sharness/lib/test-lib.sh old mode 100644 new mode 100755 diff --git a/sharness/t0020-stop.sh b/sharness/t0020-stop.sh index 70fd139..03b0daf 100755 --- a/sharness/t0020-stop.sh +++ b/sharness/t0020-stop.sh @@ -4,23 +4,24 @@ test_description="iptb stop tests" . lib/test-lib.sh -IPTB_ROOT=. +export IPTB_ROOT=. +ln -s ../plugins $IPTB_ROOT/plugins -test_expect_success "iptb init works" ' - ../bin/iptb init -n 3 +test_expect_success "iptb auto works" ' + ../bin/iptb auto -count 3 -type localipfs ' test_expect_success "iptb start works" ' - ../bin/iptb start --args --debug + ../bin/iptb start --wait -- --debug ' test_expect_success "iptb stop works" ' - ../bin/iptb stop + ../bin/iptb stop && sleep 1 ' for i in {0..2}; do test_expect_success "daemon '$i' was shut down gracefully" ' - cat testbed/'$i'/daemon.stderr | tail -1 | grep "Gracefully shut down daemon" + cat testbeds/default/'$i'/daemon.stderr | tail -1 | grep "Gracefully shut down daemon" ' done diff --git a/simulate_docker.sh b/simulate_docker.sh deleted file mode 100644 index 5f027d9..0000000 --- a/simulate_docker.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash - -# ============== USAGE ============== -# Example Usage: ./simulate.sh 5 1 50 5 -# $1 Number of Nodes Start -# $2 Number of Nodes Increment -# $3 Number of Nodes End -# $4 File Size[Bytes] #E.g.10 MB file: 10485760 Bytes - -for k in `seq $1 $2 $3` -do - # Initalize network - ./iptb init -n $k -f --type=docker - # Start nodes - ./iptb start - # Create Network Topology - ./iptb make-topology - # Create random file - head -c $4 file.txt - # Push the file to docker container - dockID=$(cat ~/testbed/0/dockerID) - docker cp file.txt $dockID:file.txt - # Add it to Node 0 - file=$(./iptb run 0 ipfs add -Q file.txt) - rm file.txt - # Simulate - ./iptb dist -hash $file - pkill dockerd -done -# Plot results -./bin/results_plotter.py -i results.json -size $4 \ No newline at end of file diff --git a/simulate_local.sh b/simulate_local.sh deleted file mode 100755 index d032ced..0000000 --- a/simulate_local.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -# ============== USAGE ============== -# Example Usage: ./simulate.sh 5 1 50 5 -# $1 Number of Nodes Start -# $2 Number of Nodes Increment -# $3 Number of Nodes End -# $4 File Size[Bytes] #E.g.10 MB file: 10485760 Bytes - -for k in `seq $1 $2 $3` -do - # Initalize network - ./iptb init -n $k -f - # Start nodes - ./iptb start - # Create Network Topology - ./iptb make-topology - # Create a random file and add it to Node 0 - head -c $4 file.txt - file=$(./iptb run 0 ipfs add -Q file.txt) - # Remove the file since we no longer need it - rm file.txt - # Make the simulation - ./iptb dist -hash $file - # Lets not burn the CPU and clean up after - pkill ipfs -done -./bin/results_plotter.py -i results.json -size $4 \ No newline at end of file diff --git a/testbed/interfaces/node.go b/testbed/interfaces/node.go new file mode 100755 index 0000000..aaae2e3 --- /dev/null +++ b/testbed/interfaces/node.go @@ -0,0 +1,141 @@ +package testbedi + +import ( + "context" + "io" +) + +// NewNodeFunc constructs a node implementing the Core interface. It is provided +// a path to an already created directory `dir`, as well as a map of attributes +// which can be supplied to shape process execution. +// Examples of attributes include: which binary to use, docker image, cpu/ram +// limits, or any other information that may be required to property setup or +// manage the node. +type NewNodeFunc func(dir string, attrs map[string]string) (Core, error) + +// GetAttrListFunc returns a list of attribute names that can be queried from +// the node. These attributes may include those can be set from the NewNodeFunc, +// or additional attributes at may only be available after initialization. +// Attributes returned should be queriable through the Attribute interface. +// Examples include: api address, peerid, cpu/ram limits, jitter. +type GetAttrListFunc func() []string + +// GetAttrDescFunc returns the description of the attribute `attr` +type GetAttrDescFunc func(attr string) (string, error) + +type Libp2p interface { + // PeerID returns the peer id + PeerID() (string, error) + // APIAddr returns the multiaddr for the api + APIAddr() (string, error) + // SwarmAddrs returns the swarm addrs for the node + SwarmAddrs() ([]string, error) +} + +type Config interface { + Core + // Config returns the configuration of the node + Config() (interface{}, error) + // WriteConfig writes the configuration of the node + WriteConfig(interface{}) error +} + +// Attributes are ways to shape process execution and additional information that alters the +// environment the process executes in +type Attribute interface { + Core + // Attr returns the value of attr + Attr(attr string) (string, error) + // SetAttr sets the attr to val + SetAttr(attr string, val string) error + // GetAttrList returns a list of attrs that can be retrieved + GetAttrList() []string + // GetAttrDesc returns the description of attr + GetAttrDesc(attr string) (string, error) + /* Example: + + * Network: + - Bandwidth + - Jitter + - Latency + - Packet_Loss + + * CPU + - Limit + + * RAM + - Limit + + */ +} + +// Metrics are ways to gather information during process execution +type Metric interface { + Core + // Events returns reader for events + Events() (io.ReadCloser, error) + // StderrReader returns reader of stderr for the node + StderrReader() (io.ReadCloser, error) + // StdoutReader returns reader of stdout for the node + StdoutReader() (io.ReadCloser, error) + + // Heartbeat returns key values pairs of a defined set of metrics + Heartbeat() (map[string]string, error) + // Metric returns metric value at key + Metric(key string) (string, error) + // GetMetricList returns list of metrics + GetMetricList() []string + // GetMetricDesc returns description of metrics + GetMetricDesc(key string) (string, error) + /* Examples: + + * Filesystem: + - device_name + - swap + - mount_point + - total + - pct_used + + * CPU + - cores + - iowait + - pct_used + + * RAM + - total + - pct_used + + * Network + - bwout + - bwin + - ping + + */ +} + +// Core specifies the interface to a process controlled by iptb +type Core interface { + Libp2p + // Allows a node to run any initialization it may require + // Ex: Installing additional dependencies / setting up configuration + Init(ctx context.Context, args ...string) (Output, error) + // Starts the node, wait can be used to delay the return till the node is ready + // to accept commands + Start(ctx context.Context, wait bool, args ...string) (Output, error) + // Stops the node + Stop(ctx context.Context) error + // Runs a command in the context of the node + RunCmd(ctx context.Context, stdin io.Reader, args ...string) (Output, error) + // Connect the node to another + Connect(ctx context.Context, n Core) error + // Starts a shell in the context of the node + Shell(ctx context.Context, ns []Core) error + + // Dir returns the iptb directory assigned to the node + Dir() string + // Type returns a string that identifies the implementation + // Examples localipfs, dockeripfs, etc. + Type() string + + String() string +} diff --git a/testbed/interfaces/output.go b/testbed/interfaces/output.go new file mode 100755 index 0000000..00b2a67 --- /dev/null +++ b/testbed/interfaces/output.go @@ -0,0 +1,20 @@ +package testbedi + +import ( + "io" +) + +// Output manages running, inprocess, a process +type Output interface { + // Args is the cleaned up version of the input. + Args() []string + + // Error is the error returned from the command, after it exited. + Error() error + + // Code is the unix style exit code, set after the command exited. + ExitCode() int + + Stdout() io.ReadCloser + Stderr() io.ReadCloser +} diff --git a/testbed/spec.go b/testbed/spec.go new file mode 100755 index 0000000..8973002 --- /dev/null +++ b/testbed/spec.go @@ -0,0 +1,163 @@ +package testbed + +import ( + "fmt" + "plugin" + + "github.com/ipfs/iptb/testbed/interfaces" +) + +// NodeSpec represents a node's specification +type NodeSpec struct { + Type string + Dir string + Attrs map[string]string +} + +// IptbPlugin contains exported symbols from loaded plugins +type IptbPlugin struct { + From string + NewNode testbedi.NewNodeFunc + GetAttrList testbedi.GetAttrListFunc + GetAttrDesc testbedi.GetAttrDescFunc + PluginName string + BuiltIn bool +} + +var plugins map[string]IptbPlugin + +func init() { + plugins = make(map[string]IptbPlugin) +} + +// GetPlugin returns a plugin registered with RegisterPlugin +func GetPlugin(name string) (IptbPlugin, bool) { + plg, ok := plugins[name] + return plg, ok +} + +// RegisterPlugin registers a plugin, the `force` flag can be passed to +// override any plugin registered under the same IptbPlugin.PluginName +func RegisterPlugin(plg IptbPlugin, force bool) (bool, error) { + overloaded := false + + if pl, exists := plugins[plg.PluginName]; exists && !force { + if pl.BuiltIn { + overloaded = true + } else { + return false, fmt.Errorf("plugin %s already loaded from %s", pl.PluginName, pl.From) + } + } + + plugins[plg.PluginName] = plg + + return overloaded, nil + +} + +// LoadPlugin loads a plugin from `path` +func LoadPlugin(path string) (*IptbPlugin, error) { + return loadPlugin(path) +} + +// LoadPluginCore loads core symbols from a golang plugin into an IptbPlugin +func loadPluginCore(pl *plugin.Plugin, plg *IptbPlugin) error { + NewNodeSym, err := pl.Lookup("NewNode") + if err != nil { + return err + } + + NewNode, ok := NewNodeSym.(*testbedi.NewNodeFunc) + if !ok { + return fmt.Errorf("Error: could not cast `NewNode` of %s", pl) + } + + PluginNameSym, err := pl.Lookup("PluginName") + if err != nil { + return err + } + + PluginName, ok := PluginNameSym.(*string) + if !ok { + return fmt.Errorf("Error: could not cast `PluginName` of %s", pl) + } + + plg.PluginName = *PluginName + plg.NewNode = *NewNode + + return nil +} + +// LoadPluginCore loads attr symbols from a golang plugin into an IptbPlugin +func loadPluginAttr(pl *plugin.Plugin, plg *IptbPlugin) (bool, error) { + GetAttrListSym, err := pl.Lookup("GetAttrList") + if err != nil { + return false, err + } + + GetAttrList, ok := GetAttrListSym.(*testbedi.GetAttrListFunc) + if !ok { + return true, fmt.Errorf("Error: could not cast `GetAttrList` of %s", pl) + } + + GetAttrDescSym, err := pl.Lookup("GetAttrDesc") + if err != nil { + return false, err + } + + GetAttrDesc, ok := GetAttrDescSym.(*testbedi.GetAttrDescFunc) + if !ok { + return true, fmt.Errorf("Error: could not cast `GetAttrDesc` of %s", pl) + } + + plg.GetAttrList = *GetAttrList + plg.GetAttrDesc = *GetAttrDesc + + return true, nil +} + +func loadPlugin(path string) (*IptbPlugin, error) { + pl, err := plugin.Open(path) + + if err != nil { + return nil, err + } + + var plg IptbPlugin + + if err := loadPluginCore(pl, &plg); err != nil { + return nil, err + } + + if ok, err := loadPluginAttr(pl, &plg); ok && err != nil { + return nil, err + } + + return &plg, nil +} + +// Load uses plugins registered with RegisterPlugin to construct a Core node +// from the NodeSpec +func (ns *NodeSpec) Load() (testbedi.Core, error) { + pluginName := ns.Type + + if plg, ok := plugins[pluginName]; ok { + return plg.NewNode(ns.Dir, ns.Attrs) + } + + return nil, fmt.Errorf("Could not find plugin %s", pluginName) +} + +// SetAttr sets an attribute on the NodeSpec +func (ns *NodeSpec) SetAttr(attr string, val string) { + ns.Attrs[attr] = val +} + +// GetAttr gets an attribute from the NodeSpec +func (ns *NodeSpec) GetAttr(attr string) (string, error) { + if v, ok := ns.Attrs[attr]; ok { + return v, nil + } + + return "", fmt.Errorf("Attr not set") +} diff --git a/testbed/testbed.go b/testbed/testbed.go new file mode 100755 index 0000000..7f104ed --- /dev/null +++ b/testbed/testbed.go @@ -0,0 +1,195 @@ +package testbed + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/ipfs/iptb/testbed/interfaces" + "github.com/ipfs/iptb/util" +) + +type Testbed interface { + Name() string + + // Spec returns a spec for node n + Spec(n int) (*NodeSpec, error) + + // Specs returns all specs + Specs() ([]*NodeSpec, error) + + // Node returns node n, specified by spec n + Node(n int) (testbedi.Core, error) + + // Node returns all nodes, specified by all specs + Nodes() ([]testbedi.Core, error) + + /****************/ + /* Future Ideas */ + + // Would be neat to have a TestBed Config interface + // The node interface GetAttr and SetAttr should be a shortcut into this + // Config() (map[interface{}]interface{}, error) + +} + +type BasicTestbed struct { + dir string + specs []*NodeSpec + nodes []testbedi.Core +} + +func NewTestbed(dir string) BasicTestbed { + return BasicTestbed{ + dir: dir, + } +} + +func (tb *BasicTestbed) Dir() string { + return tb.dir +} + +func (tb BasicTestbed) Name() string { + return tb.dir +} + +func AlreadyInitCheck(dir string, force bool) error { + if _, err := os.Stat(filepath.Join(dir, "nodespec.json")); !os.IsNotExist(err) { + if !force && !iptbutil.YesNoPrompt("testbed nodes already exist, overwrite? [y/n]") { + return nil + } + + return os.RemoveAll(dir) + } + + return nil +} + +func BuildSpecs(base string, count int, typ string, attrs map[string]string) ([]*NodeSpec, error) { + var specs []*NodeSpec + + for i := 0; i < count; i++ { + dir := path.Join(base, fmt.Sprint(i)) + + if err := os.MkdirAll(dir, 0775); err != nil { + return nil, err + } + + spec := &NodeSpec{ + Type: typ, + Dir: dir, + Attrs: attrs, + } + + specs = append(specs, spec) + } + + return specs, nil +} + +func (tb *BasicTestbed) Spec(n int) (*NodeSpec, error) { + specs, err := tb.Specs() + + if err != nil { + return nil, err + } + + if n >= len(specs) { + return nil, fmt.Errorf("Spec index out of range") + } + + return specs[n], err +} + +func (tb *BasicTestbed) Specs() ([]*NodeSpec, error) { + if tb.specs != nil { + return tb.specs, nil + } + + return tb.loadSpecs() +} + +func (tb *BasicTestbed) Node(n int) (testbedi.Core, error) { + nodes, err := tb.Nodes() + + if err != nil { + return nil, err + } + + if n >= len(nodes) { + return nil, fmt.Errorf("Node index out of range") + } + + return nodes[n], err +} + +func (tb *BasicTestbed) Nodes() ([]testbedi.Core, error) { + if tb.nodes != nil { + return tb.nodes, nil + } + + return tb.loadNodes() +} + +func (tb *BasicTestbed) loadSpecs() ([]*NodeSpec, error) { + specs, err := ReadNodeSpecs(tb.dir) + if err != nil { + return nil, err + } + + return specs, nil +} + +func (tb *BasicTestbed) loadNodes() ([]testbedi.Core, error) { + specs, err := tb.Specs() + if err != nil { + return nil, err + } + + return NodesFromSpecs(specs) +} + +func NodesFromSpecs(specs []*NodeSpec) ([]testbedi.Core, error) { + var out []testbedi.Core + for _, s := range specs { + nd, err := s.Load() + if err != nil { + return nil, err + } + out = append(out, nd) + } + return out, nil +} + +func ReadNodeSpecs(dir string) ([]*NodeSpec, error) { + data, err := ioutil.ReadFile(filepath.Join(dir, "nodespec.json")) + if err != nil { + return nil, err + } + + var specs []*NodeSpec + err = json.Unmarshal(data, &specs) + if err != nil { + return nil, err + } + + return specs, nil +} + +func WriteNodeSpecs(dir string, specs []*NodeSpec) error { + err := os.MkdirAll(dir, 0775) + if err != nil { + return err + } + + fi, err := os.Create(filepath.Join(dir, "nodespec.json")) + if err != nil { + return err + } + + defer fi.Close() + return json.NewEncoder(fi).Encode(specs) +} diff --git a/topologies/barbell.txt b/topologies/barbell.txt deleted file mode 100644 index b48bb1f..0000000 --- a/topologies/barbell.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Node Origin:Connection 1 Connection 2 .... Connection n -0:1,2,3,4 -1:0,2,3,4 -2:0,1,3,4 -3:0,1,2,4 -4:0,1,2,3,5 -5:4,6 -6:5,7 -7:8,6 -8:9,7 -9:8,10 -10:9,11,12,13,14 -11:10,12,13,14 -12:10,11,13,14 -13:10,11,12,14 -14:10,11,12,13 \ No newline at end of file diff --git a/topologies/complete.txt b/topologies/complete.txt deleted file mode 100644 index f50ec61..0000000 --- a/topologies/complete.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Node Origin:Connection 1 Connection 2 .... Connection n -0:1,2,3,4 -1:0,2,3,4 -2:0,1,3,4 -3:0,1,2,4 -4:0,1,2,3 \ No newline at end of file diff --git a/topologies/star.txt b/topologies/star.txt deleted file mode 100644 index 7698f60..0000000 --- a/topologies/star.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Node Origin:Connection 1 Connection 2 .... Connection n -0:1,2,3,4,5 -1:0 -2:0 -3:0 -4:0 -5:0 \ No newline at end of file diff --git a/util/dockernode.go b/util/dockernode.go deleted file mode 100644 index 2397121..0000000 --- a/util/dockernode.go +++ /dev/null @@ -1,315 +0,0 @@ -package iptbutil - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "time" - - cnet "github.com/whyrusleeping/go-ctrlnet" - "github.com/whyrusleeping/stump" -) - -// DockerNode is an IPFS node in a docker container controlled -// by IPTB -type DockerNode struct { - ImageName string - ID string - - apiAddr string - - LocalNode -} - -// assert DockerNode satisfies the testbed IpfsNode interface -var _ IpfsNode = (*DockerNode)(nil) - -func (dn *DockerNode) Start(args []string) error { - if len(args) > 0 { - return fmt.Errorf("cannot yet pass daemon args to docker nodes") - } - - cmd := exec.Command("docker", "run", "-d", "-v", dn.Dir+":/data/ipfs", dn.ImageName) - out, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("%s: %s", err, string(out)) - } - - id := bytes.TrimSpace(out) - dn.ID = string(id) - idfile := filepath.Join(dn.Dir, "dockerID") - err = ioutil.WriteFile(idfile, id, 0664) - - if err != nil { - killErr := dn.killContainer() - if killErr != nil { - return combineErrors(err, killErr) - } - return err - } - - err = waitOnAPI(dn) - if err != nil { - killErr := dn.Kill() - if killErr != nil { - return combineErrors(err, killErr) - } - return err - } - - return nil -} - -func combineErrors(err1, err2 error) error { - return fmt.Errorf("%v\nwhile handling the above error, the following error occurred:\n%v", err1, err2) -} - -func (dn *DockerNode) setAPIAddr() error { - internal, err := dn.LocalNode.APIAddr() - if err != nil { - return err - } - - port := strings.Split(internal, ":")[1] - - dip, err := dn.getDockerIP() - if err != nil { - return err - } - - dn.apiAddr = dip + ":" + port - - maddr := []byte("/ip4/" + dip + "/tcp/" + port) - return ioutil.WriteFile(filepath.Join(dn.Dir, "api"), maddr, 0644) -} - -func (dn *DockerNode) APIAddr() (string, error) { - if dn.apiAddr == "" { - if err := dn.setAPIAddr(); err != nil { - return "", err - } - } - - return dn.apiAddr, nil -} - -func (dn *DockerNode) getDockerIP() (string, error) { - cmd := exec.Command("docker", "inspect", dn.ID) - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("%s: %s", err, string(out)) - } - - var info []interface{} - if err := json.Unmarshal(out, &info); err != nil { - return "", err - } - - if len(info) == 0 { - return "", fmt.Errorf("got no inspect data") - } - - cinfo := info[0].(map[string]interface{}) - netinfo := cinfo["NetworkSettings"].(map[string]interface{}) - return netinfo["IPAddress"].(string), nil -} - -func (dn *DockerNode) Kill() error { - err := dn.killContainer() - if err != nil { - return err - } - return os.Remove(filepath.Join(dn.Dir, "dockerID")) -} - -func (dn *DockerNode) killContainer() error { - out, err := exec.Command("docker", "kill", "--signal=INT", dn.ID).CombinedOutput() - if err != nil { - return fmt.Errorf("%s: %s", err, string(out)) - } - return nil -} - -func (dn *DockerNode) String() string { - return "docker:" + dn.PeerID -} - -func (dn *DockerNode) RunCmd(args ...string) (string, error) { - if dn.ID == "" { - return "", fmt.Errorf("no docker id set on node") - } - - args = append([]string{"exec", "-ti", dn.ID}, args...) - cmd := exec.Command("docker", args...) - cmd.Stdin = os.Stdin - - stump.VLog("running: ", cmd.Args) - - out, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("%s: %s", err, string(out)) - } - - return string(out), nil -} - -func (dn *DockerNode) Shell() error { - nodes, err := LoadNodes() - if err != nil { - return err - } - - nenvs := os.Environ() - for i, n := range nodes { - peerid := n.GetPeerID() - if peerid == "" { - return fmt.Errorf("failed to check peerID") - } - - nenvs = append(nenvs, fmt.Sprintf("NODE%d=%s", i, peerid)) - } - - cmd := exec.Command("docker", "exec", "-ti", dn.ID, "/bin/sh") - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - - return cmd.Run() -} - -func (dn *DockerNode) GetAttr(name string) (string, error) { - switch name { - case "ifname": - return dn.getInterfaceName() - default: - return dn.LocalNode.GetAttr(name) - } -} - -func (dn *DockerNode) SetAttr(name, val string) error { - switch name { - case "latency": - return dn.setLatency(val) - case "bandwidth": - return dn.setBandwidth(val) - case "jitter": - return dn.setJitter(val) - case "loss": - return dn.setPacketLoss(val) - default: - return fmt.Errorf("no attribute named: %s", name) - } -} - -func (dn *DockerNode) setLatency(val string) error { - dur, err := time.ParseDuration(val) - if err != nil { - return err - } - - ifn, err := dn.getInterfaceName() - if err != nil { - return err - } - - settings := &cnet.LinkSettings{ - Latency: uint(dur.Nanoseconds() / 1000000), - } - - return cnet.SetLink(ifn, settings) -} - -func (dn *DockerNode) setJitter(val string) error { - dur, err := time.ParseDuration(val) - if err != nil { - return err - } - - ifn, err := dn.getInterfaceName() - if err != nil { - return err - } - - settings := &cnet.LinkSettings{ - Jitter: uint(dur.Nanoseconds() / 1000000), - } - - return cnet.SetLink(ifn, settings) -} - -// set bandwidth (expects Mbps) -func (dn *DockerNode) setBandwidth(val string) error { - bw, err := strconv.ParseFloat(val, 32) - if err != nil { - return err - } - - ifn, err := dn.getInterfaceName() - if err != nil { - return err - } - - settings := &cnet.LinkSettings{ - Bandwidth: uint(bw * 1000000), - } - - return cnet.SetLink(ifn, settings) -} - -// set packet loss percentage (dropped / total) -func (dn *DockerNode) setPacketLoss(val string) error { - ratio, err := strconv.ParseUint(val, 10, 8) - if err != nil { - return err - } - - ifn, err := dn.getInterfaceName() - if err != nil { - return err - } - - settings := &cnet.LinkSettings{ - PacketLoss: uint8(ratio), - } - - return cnet.SetLink(ifn, settings) -} - -func (dn *DockerNode) getInterfaceName() (string, error) { - out, err := dn.RunCmd("ip", "link") - if err != nil { - return "", err - } - - var cside string - for _, l := range strings.Split(out, "\n") { - if strings.Contains(l, "@if") { - ifnum := strings.Split(strings.Split(l, " ")[1], "@")[1] - cside = ifnum[2 : len(ifnum)-1] - break - } - } - - if cside == "" { - return "", fmt.Errorf("container-side interface not found") - } - - localout, err := exec.Command("ip", "link").CombinedOutput() - if err != nil { - return "", fmt.Errorf("%s: %s", err, localout) - } - - for _, l := range strings.Split(string(localout), "\n") { - if strings.HasPrefix(l, cside+": ") { - return strings.Split(strings.Fields(l)[1], "@")[0], nil - } - } - - return "", fmt.Errorf("could not determine interface") -} diff --git a/util/localnode.go b/util/localnode.go deleted file mode 100644 index 34f8486..0000000 --- a/util/localnode.go +++ /dev/null @@ -1,354 +0,0 @@ -package iptbutil - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - - config "github.com/ipfs/go-ipfs/repo/config" - serial "github.com/ipfs/go-ipfs/repo/fsrepo/serialize" - - ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr-net" -) - -var ErrTimeout = errors.New("timeout") - -// LocalNode is a machine-local IPFS node controlled by IPTB -type LocalNode struct { - Dir string - PeerID string -} - -// assert LocalNode satisfies the IpfsNode interface -var _ IpfsNode = (*LocalNode)(nil) - -func (n *LocalNode) Init() error { - err := os.MkdirAll(n.Dir, 0777) - if err != nil { - return err - } - - cmd := exec.Command("ipfs", "init", "-b=1024") - cmd.Env, err = n.envForDaemon() - if err != nil { - return err - } - - out, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("%s: %s", err, string(out)) - } - - return nil -} - -func (n *LocalNode) GetPeerID() string { - return n.PeerID -} - -func (n *LocalNode) String() string { - return n.PeerID -} - -// Shell sets up environment variables for a new shell to more easily -// control the given daemon -func (n *LocalNode) Shell() error { - shell := os.Getenv("SHELL") - if shell == "" { - return fmt.Errorf("couldnt find shell!") - } - - nenvs := []string{"IPFS_PATH=" + n.Dir} - - nodes, err := LoadNodes() - if err != nil { - return err - } - - for i, n := range nodes { - peerid := n.GetPeerID() - if peerid == "" { - return fmt.Errorf("failed to check peerID") - } - - nenvs = append(nenvs, fmt.Sprintf("NODE%d=%s", i, peerid)) - } - nenvs = append(os.Environ(), nenvs...) - - return syscall.Exec(shell, []string{shell}, nenvs) -} - -func (n *LocalNode) RunCmd(args ...string) (string, error) { - cmd := exec.Command(args[0], args[1:]...) - - var err error - cmd.Env, err = n.envForDaemon() - if err != nil { - return "", err - } - - outbuf := new(bytes.Buffer) - errbuf := new(bytes.Buffer) - cmd.Stdout = outbuf - cmd.Stderr = errbuf - - err = cmd.Run() - if err != nil { - return "", fmt.Errorf("%s: %s %s", err, outbuf.String(), errbuf.String()) - } - - return outbuf.String(), nil -} - -func (n *LocalNode) APIAddr() (string, error) { - dir := n.Dir - - addrb, err := ioutil.ReadFile(filepath.Join(dir, "api")) - if err != nil { - return "", err - } - - maddr, err := ma.NewMultiaddr(string(addrb)) - if err != nil { - fmt.Println("error parsing multiaddr: ", err) - return "", err - } - - _, addr, err := manet.DialArgs(maddr) - if err != nil { - fmt.Println("error on multiaddr dialargs: ", err) - return "", err - } - return addr, nil -} - -func (n *LocalNode) envForDaemon() ([]string, error) { - envs := os.Environ() - dir := n.Dir - npath := "IPFS_PATH=" + dir - for i, e := range envs { - p := strings.Split(e, "=") - if p[0] == "IPFS_PATH" { - envs[i] = npath - return envs, nil - } - } - - return append(envs, npath), nil -} - -func (n *LocalNode) Start(args []string) error { - alive, err := n.isAlive() - if err != nil { - return err - } - - if alive { - return fmt.Errorf("node is already running") - } - - dir := n.Dir - dargs := append([]string{"daemon"}, args...) - cmd := exec.Command("ipfs", dargs...) - cmd.Dir = dir - - cmd.Env, err = n.envForDaemon() - if err != nil { - return err - } - - setupOpt(cmd) - - stdout, err := os.Create(filepath.Join(dir, "daemon.stdout")) - if err != nil { - return err - } - - stderr, err := os.Create(filepath.Join(dir, "daemon.stderr")) - if err != nil { - return err - } - - cmd.Stdout = stdout - cmd.Stderr = stderr - - err = cmd.Start() - if err != nil { - return err - } - pid := cmd.Process.Pid - - fmt.Printf("Started daemon %s, pid = %d\n", dir, pid) - err = ioutil.WriteFile(filepath.Join(dir, "daemon.pid"), []byte(fmt.Sprint(pid)), 0666) - if err != nil { - return err - } - - // Make sure node 0 is up before starting the rest so - // bootstrapping works properly - cfg, err := serial.Load(filepath.Join(dir, "config")) - if err != nil { - return err - } - - n.PeerID = cfg.Identity.PeerID - - err = waitOnAPI(n) - if err != nil { - return err - } - - return nil -} - -func (n *LocalNode) getPID() (int, error) { - b, err := ioutil.ReadFile(filepath.Join(n.Dir, "daemon.pid")) - if err != nil { - return -1, err - } - - return strconv.Atoi(string(b)) -} - -func (n *LocalNode) isAlive() (bool, error) { - pid, err := n.getPID() - if os.IsNotExist(err) { - return false, nil - } else if err != nil { - return false, err - } - - proc, err := os.FindProcess(pid) - if err != nil { - return false, nil - } - - err = proc.Signal(syscall.Signal(0)) - if err == nil { - return true, nil - } - return false, nil -} - -func (n *LocalNode) Kill() error { - pid, err := n.getPID() - if err != nil { - return fmt.Errorf("error killing daemon %s: %s", n.Dir, err) - } - - p, err := os.FindProcess(pid) - if err != nil { - return fmt.Errorf("error killing daemon %s: %s", n.Dir, err) - } - - waitch := make(chan struct{}, 1) - go func() { - p.Wait() //TODO: pass return state - waitch <- struct{}{} - }() - - defer func() { - err := os.Remove(filepath.Join(n.Dir, "daemon.pid")) - if err != nil && !os.IsNotExist(err) { - panic(fmt.Errorf("error removing pid file for daemon at %s: %s\n", n.Dir, err)) - } - }() - - if err := n.signalAndWait(p, waitch, syscall.SIGTERM, 1*time.Second); err != ErrTimeout { - return err - } - - if err := n.signalAndWait(p, waitch, syscall.SIGTERM, 2*time.Second); err != ErrTimeout { - return err - } - - if err := n.signalAndWait(p, waitch, syscall.SIGQUIT, 5*time.Second); err != ErrTimeout { - return err - } - - if err := n.signalAndWait(p, waitch, syscall.SIGKILL, 5*time.Second); err != ErrTimeout { - return err - } - - for { - err := p.Signal(syscall.Signal(0)) - if err != nil { - break - } - time.Sleep(time.Millisecond * 10) - } - - return nil -} - -func (n *LocalNode) signalAndWait(p *os.Process, waitch <-chan struct{}, signal os.Signal, t time.Duration) error { - err := p.Signal(signal) - if err != nil { - return fmt.Errorf("error killing daemon %s: %s\n", n.Dir, err) - } - - select { - case <-waitch: - return nil - case <-time.After(t): - return ErrTimeout - } -} - -func (n *LocalNode) GetAttr(attr string) (string, error) { - switch attr { - case attrId: - return n.GetPeerID(), nil - case attrPath: - return n.Dir, nil - case attrBwIn: - bw, err := GetBW(n) - if err != nil { - return "", err - } - return fmt.Sprint(bw.TotalIn), nil - case attrBwOut: - bw, err := GetBW(n) - if err != nil { - return "", err - } - return fmt.Sprint(bw.TotalOut), nil - default: - return "", errors.New("unrecognized attribute: " + attr) - } -} - -func (n *LocalNode) GetConfig() (*config.Config, error) { - return serial.Load(filepath.Join(n.Dir, "config")) -} - -func (n *LocalNode) WriteConfig(c *config.Config) error { - return serial.WriteConfigFile(filepath.Join(n.Dir, "config"), c) -} - -func (n *LocalNode) SetAttr(name, val string) error { - return fmt.Errorf("no atttributes to set") -} - -func (n *LocalNode) StdoutReader() (io.ReadCloser, error) { - return n.readerFor("daemon.stdout") -} - -func (n *LocalNode) StderrReader() (io.ReadCloser, error) { - return n.readerFor("daemon.stderr") -} - -func (n *LocalNode) readerFor(file string) (io.ReadCloser, error) { - f, err := os.OpenFile(filepath.Join(n.Dir, file), os.O_RDONLY, 0) - return f, err -} diff --git a/util/node.go b/util/node.go deleted file mode 100644 index 856ed4b..0000000 --- a/util/node.go +++ /dev/null @@ -1,24 +0,0 @@ -package iptbutil - -import ( - "github.com/ipfs/go-ipfs/repo/config" -) - -// IpfsNode defines the interface iptb requires to work -// with an IPFS node -type IpfsNode interface { - Init() error - Kill() error - Start(args []string) error - APIAddr() (string, error) - GetPeerID() string - RunCmd(args ...string) (string, error) - Shell() error - String() string - - GetAttr(string) (string, error) - SetAttr(string, string) error - - GetConfig() (*config.Config, error) - WriteConfig(*config.Config) error -} diff --git a/util/output.go b/util/output.go new file mode 100755 index 0000000..e68575f --- /dev/null +++ b/util/output.go @@ -0,0 +1,48 @@ +package iptbutil + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/ipfs/iptb/testbed/interfaces" +) + +type Output struct { + args []string + + exitcode int + + err error + stdout []byte + stderr []byte +} + +func NewOutput(args []string, stdout, stderr []byte, exitcode int, cmderr error) testbedi.Output { + return &Output{ + args: args, + stdout: stdout, + stderr: stderr, + exitcode: exitcode, + err: cmderr, + } +} + +func (o *Output) Args() []string { + return o.args +} + +func (o *Output) Error() error { + return o.err +} +func (o *Output) ExitCode() int { + return o.exitcode +} + +func (o *Output) Stdout() io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader(o.stdout)) +} + +func (o *Output) Stderr() io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader(o.stderr)) +} diff --git a/util/proc_unix.go b/util/proc_unix.go old mode 100644 new mode 100755 index b550381..dae68de --- a/util/proc_unix.go +++ b/util/proc_unix.go @@ -7,6 +7,6 @@ import ( "syscall" ) -func setupOpt(cmd *exec.Cmd) { +func SetupOpt(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} } diff --git a/util/proc_windows.go b/util/proc_windows.go old mode 100644 new mode 100755 index c3210c9..e7e594e --- a/util/proc_windows.go +++ b/util/proc_windows.go @@ -6,6 +6,6 @@ import ( "os/exec" ) -func setupOpt(cmd *exec.Cmd) { +func SetupOpt(cmd *exec.Cmd) { // Do nothing } diff --git a/util/util.go b/util/util.go old mode 100644 new mode 100755 index 75c4683..d74e514 --- a/util/util.go +++ b/util/util.go @@ -1,100 +1,9 @@ package iptbutil import ( - "encoding/json" - "errors" "fmt" - "io/ioutil" - "net/http" - "os" - "path" - "path/filepath" - "strings" - "sync" - "time" - - serial "github.com/ipfs/go-ipfs/repo/fsrepo/serialize" - "github.com/whyrusleeping/stump" ) -// GetNumNodes returns the number of testbed nodes configured in the testbed directory -func GetNumNodes() int { - for i := 0; i < 2000; i++ { - dir, err := IpfsDirN(i) - if err != nil { - return i - } - _, err = os.Stat(dir) - if os.IsNotExist(err) { - return i - } - } - panic("i dont know whats going on") -} - -func TestBedDir() (string, error) { - tbd := os.Getenv("IPTB_ROOT") - if len(tbd) != 0 { - return tbd, nil - } - - home := os.Getenv("HOME") - if len(home) == 0 { - return "", fmt.Errorf("environment variable HOME is not set") - } - - return path.Join(home, "testbed"), nil -} - -func IpfsDirN(n int) (string, error) { - tbd, err := TestBedDir() - if err != nil { - return "", err - } - return path.Join(tbd, fmt.Sprint(n)), nil -} - -type InitCfg struct { - Count int - Force bool - Bootstrap string - PortStart int - Mdns bool - Utp bool - Websocket bool - Override string - NodeType string -} - -func (c *InitCfg) swarmAddrForPeer(i int) string { - str := "/ip4/0.0.0.0/tcp/%d" - if c.Utp { - str = "/ip4/0.0.0.0/udp/%d/utp" - } - if c.Websocket { - str = "/ip4/0.0.0.0/tcp/%d/ws" - } - - if c.PortStart == 0 { - return fmt.Sprintf(str, 0) - } - return fmt.Sprintf(str, c.PortStart+i) -} - -func (c *InitCfg) apiAddrForPeer(i int) string { - ip := "127.0.0.1" - if c.NodeType == "docker" { - ip = "0.0.0.0" - } - - var port int - if c.PortStart != 0 { - port = c.PortStart + 1000 + i - } - - return fmt.Sprintf("/ip4/%s/tcp/%d", ip, port) -} - func YesNoPrompt(prompt string) bool { var s string for { @@ -109,593 +18,3 @@ func YesNoPrompt(prompt string) bool { fmt.Println("Please press either 'y' or 'n'") } } - -func LoadNodeN(n int) (IpfsNode, error) { - specs, err := ReadNodeSpecs() - if err != nil { - return nil, err - } - - return specs[n].Load() -} - -func LoadNodes() ([]IpfsNode, error) { - specs, err := ReadNodeSpecs() - if err != nil { - return nil, err - } - - return NodesFromSpecs(specs) -} - -func NodesFromSpecs(specs []*NodeSpec) ([]IpfsNode, error) { - var out []IpfsNode - for _, s := range specs { - nd, err := s.Load() - if err != nil { - return nil, err - } - out = append(out, nd) - } - return out, nil -} - -type NodeSpec struct { - Type string - Dir string - Extra map[string]interface{} -} - -func ReadNodeSpecs() ([]*NodeSpec, error) { - tbd, err := TestBedDir() - if err != nil { - return nil, err - } - - data, err := ioutil.ReadFile(filepath.Join(tbd, "nodespec")) - if err != nil { - return nil, err - } - - var specs []*NodeSpec - err = json.Unmarshal(data, &specs) - if err != nil { - return nil, err - } - - return specs, nil -} - -func WriteNodeSpecs(specs []*NodeSpec) error { - tbd, err := TestBedDir() - if err != nil { - return err - } - - err = os.MkdirAll(tbd, 0775) - if err != nil { - return err - } - - fi, err := os.Create(filepath.Join(tbd, "nodespec")) - if err != nil { - return err - } - - defer fi.Close() - err = json.NewEncoder(fi).Encode(specs) - if err != nil { - return err - } - - return nil -} - -func (ns *NodeSpec) Load() (IpfsNode, error) { - switch ns.Type { - case "local": - ln := &LocalNode{ - Dir: ns.Dir, - } - - if _, err := os.Stat(filepath.Join(ln.Dir, "config")); err == nil { - pid, err := GetPeerID(ln.Dir) - if err != nil { - return nil, err - } - - ln.PeerID = pid - } - - return ln, nil - case "docker": - imgi, ok := ns.Extra["image"] - if !ok { - return nil, fmt.Errorf("no 'image' field on docker node spec") - } - - img := imgi.(string) - - dn := &DockerNode{ - ImageName: img, - LocalNode: LocalNode{ - Dir: ns.Dir, - }, - } - - if _, err := os.Stat(filepath.Join(dn.Dir, "config")); err == nil { - pid, err := GetPeerID(dn.Dir) - if err != nil { - return nil, err - } - - dn.PeerID = pid - } - - didfi := filepath.Join(ns.Dir, "dockerID") - if _, err := os.Stat(didfi); err == nil { - data, err := ioutil.ReadFile(didfi) - if err != nil { - return nil, err - } - - dn.ID = string(data) - } - - return dn, nil - default: - return nil, fmt.Errorf("unrecognized iptb node type") - } -} - -func initSpecs(cfg *InitCfg) ([]*NodeSpec, error) { - var specs []*NodeSpec - // generate node spec - - for i := 0; i < cfg.Count; i++ { - dir, err := IpfsDirN(i) - if err != nil { - return nil, err - } - var ns *NodeSpec - - switch cfg.NodeType { - case "docker": - img := "ipfs/go-ipfs" - if altimg := os.Getenv("IPFS_DOCKER_IMAGE"); altimg != "" { - img = altimg - } - ns = &NodeSpec{ - Type: "docker", - Dir: dir, - Extra: map[string]interface{}{ - "image": img, - }, - } - default: - ns = &NodeSpec{ - Type: "local", - Dir: dir, - } - } - specs = append(specs, ns) - } - - return specs, nil -} - -func IpfsInit(cfg *InitCfg) error { - tbd, err := TestBedDir() - if err != nil { - return err - } - - if _, err := os.Stat(filepath.Join(tbd, "nodespec")); !os.IsNotExist(err) { - if !cfg.Force && !YesNoPrompt("testbed nodes already exist, overwrite? [y/n]") { - return nil - } - tbd, err := TestBedDir() - err = os.RemoveAll(tbd) - if err != nil { - return err - } - } - - specs, err := initSpecs(cfg) - if err != nil { - return err - } - - nodes, err := NodesFromSpecs(specs) - if err != nil { - return err - } - - err = WriteNodeSpecs(specs) - if err != nil { - return err - } - - wait := sync.WaitGroup{} - for _, n := range nodes { - wait.Add(1) - go func(nd IpfsNode) { - defer wait.Done() - err := nd.Init() - if err != nil { - stump.Error(err) - return - } - }(n) - } - wait.Wait() - - // Now setup bootstrapping - switch cfg.Bootstrap { - case "star": - err := starBootstrap(nodes, cfg) - if err != nil { - return err - } - case "none": - err := clearBootstrapping(nodes, cfg) - if err != nil { - return err - } - default: - return fmt.Errorf("unrecognized bootstrapping option: %s", cfg.Bootstrap) - } - - /* - if cfg.Override != "" { - err := ApplyConfigOverride(cfg) - if err != nil { - return err - } - } - */ - - return nil -} - -func ApplyConfigOverride(cfg *InitCfg) error { - fir, err := os.Open(cfg.Override) - if err != nil { - return err - } - defer fir.Close() - - var configs map[string]interface{} - err = json.NewDecoder(fir).Decode(&configs) - if err != nil { - return err - } - - for i := 0; i < cfg.Count; i++ { - err := applyOverrideToNode(configs, i) - if err != nil { - return err - } - } - - return nil -} - -func applyOverrideToNode(ovr map[string]interface{}, node int) error { - for k, v := range ovr { - _ = k - switch v.(type) { - case map[string]interface{}: - default: - } - - } - - panic("not implemented") -} - -func starBootstrap(nodes []IpfsNode, icfg *InitCfg) error { - // '0' node is the bootstrap node - king := nodes[0] - - bcfg, err := king.GetConfig() - if err != nil { - return err - } - - bcfg.Bootstrap = nil - bcfg.Addresses.Swarm = []string{icfg.swarmAddrForPeer(0)} - bcfg.Addresses.API = icfg.apiAddrForPeer(0) - bcfg.Addresses.Gateway = "" - bcfg.Discovery.MDNS.Enabled = icfg.Mdns - - err = king.WriteConfig(bcfg) - if err != nil { - return err - } - - for i, nd := range nodes[1:] { - cfg, err := nd.GetConfig() - if err != nil { - return err - } - - ba := fmt.Sprintf("%s/ipfs/%s", bcfg.Addresses.Swarm[0], bcfg.Identity.PeerID) - ba = strings.Replace(ba, "0.0.0.0", "127.0.0.1", -1) - cfg.Bootstrap = []string{ba} - cfg.Addresses.Gateway = "" - cfg.Discovery.MDNS.Enabled = icfg.Mdns - cfg.Addresses.Swarm = []string{ - icfg.swarmAddrForPeer(i + 1), - } - cfg.Addresses.API = icfg.apiAddrForPeer(i + 1) - - err = nd.WriteConfig(cfg) - if err != nil { - return err - } - } - return nil -} - -func clearBootstrapping(nodes []IpfsNode, icfg *InitCfg) error { - for i, nd := range nodes { - cfg, err := nd.GetConfig() - if err != nil { - return err - } - - cfg.Bootstrap = nil - cfg.Addresses.Gateway = "" - cfg.Addresses.Swarm = []string{icfg.swarmAddrForPeer(i)} - cfg.Addresses.API = icfg.apiAddrForPeer(i) - cfg.Discovery.MDNS.Enabled = icfg.Mdns - err = nd.WriteConfig(cfg) - if err != nil { - return err - } - } - return nil -} - -func IpfsKillAll(nds []IpfsNode) error { - var errs []error - for _, n := range nds { - err := n.Kill() - if err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - var errstr string - for _, e := range errs { - errstr += "\n" + e.Error() - } - return fmt.Errorf(strings.TrimSpace(errstr)) - } - return nil -} - -func IpfsStart(nodes []IpfsNode, waitall bool, args []string) error { - for _, n := range nodes { - if err := n.Start(args); err != nil { - return err - } - } - if waitall { - for _, n := range nodes { - err := waitOnSwarmPeers(n) - if err != nil { - return err - } - } - - } - return nil -} - -func waitOnAPI(n IpfsNode) error { - for i := 0; i < 50; i++ { - err := tryAPICheck(n) - if err == nil { - return nil - } - stump.VLog("temp error waiting on API: ", err) - time.Sleep(time.Millisecond * 400) - } - return fmt.Errorf("node %s failed to come online in given time period", n.GetPeerID()) -} - -func tryAPICheck(n IpfsNode) error { - addr, err := n.APIAddr() - if err != nil { - return err - } - - stump.VLog("checking api addresss at: ", addr) - resp, err := http.Get("http://" + addr + "/api/v0/id") - if err != nil { - return err - } - - out := make(map[string]interface{}) - err = json.NewDecoder(resp.Body).Decode(&out) - if err != nil { - return fmt.Errorf("liveness check failed: %s", err) - } - - id, ok := out["ID"] - if !ok { - return fmt.Errorf("liveness check failed: ID field not present in output") - } - - idstr := id.(string) - if idstr != n.GetPeerID() { - return fmt.Errorf("liveness check failed: unexpected peer at endpoint") - } - - return nil -} - -func waitOnSwarmPeers(n IpfsNode) error { - addr, err := n.APIAddr() - if err != nil { - return err - } - - for i := 0; i < 50; i++ { - resp, err := http.Get("http://" + addr + "/api/v0/swarm/peers") - if err == nil { - out := make(map[string]interface{}) - err := json.NewDecoder(resp.Body).Decode(&out) - if err != nil { - return fmt.Errorf("liveness check failed: %s", err) - } - - pstrings, ok := out["Strings"] - if ok { - if len(pstrings.([]interface{})) == 0 { - continue - } - return nil - } - - peers, ok := out["Peers"] - if !ok { - return fmt.Errorf("object from swarm peers doesnt look right (api mismatch?)") - } - - if peers == nil { - time.Sleep(time.Millisecond * 200) - continue - } - - if plist, ok := peers.([]interface{}); ok && len(plist) == 0 { - continue - } - - return nil - } - time.Sleep(time.Millisecond * 200) - } - return fmt.Errorf("node at %s failed to bootstrap in given time period", addr) -} - -// GetFile downloads a single file from node nd, it is asynchronous and works with channels -func GetFile(hash string, nd IpfsNode, c chan float64, e chan error) { - start := time.Now() - _, err := nd.RunCmd("ipfs", "cat", hash) - elapsed := time.Since(start) - if err != nil { - c <- 0. - e <- err - } else { - c <- elapsed.Seconds() - e <- nil - } -} - -// GetPeerID reads the config of node 'n' and returns its peer ID -func GetPeerID(ipfsdir string) (string, error) { - cfg, err := serial.Load(path.Join(ipfsdir, "config")) - if err != nil { - return "", err - } - return cfg.Identity.PeerID, nil -} - -func ConnectNodes(from, to IpfsNode, timeout string) error { - if from == to { - // skip connecting to self.. - return nil - } - - out, err := to.RunCmd("ipfs", "id", "-f", "") - if err != nil { - return fmt.Errorf("error checking node address: %s", err) - } - - stump.Log("connecting %s -> %s\n", from, to) - - addrs := strings.Fields(string(out)) - orderishAddresses(addrs) - for i := 0; i < len(addrs); i++ { - addr := addrs[i] - args := []string{"ipfs", "swarm", "connect", addr} - if timeout != "" { - args = append(args, "--timeout="+timeout) - } - - _, err = from.RunCmd(args...) - - if err == nil { - stump.Log("connection success!") - return nil - } - stump.Log("dial attempt to %s failed: %s", addr, err) - time.Sleep(time.Second) - } - - return errors.New("no dialable addresses") -} - -func orderishAddresses(addrs []string) { - for i, a := range addrs { - if strings.Contains(a, "127.0.0.1") { - addrs[i], addrs[0] = addrs[0], addrs[i] - return - } - } -} - -type BW struct { - TotalIn int - TotalOut int -} - -func GetBW(n IpfsNode) (*BW, error) { - addr, err := n.APIAddr() - if err != nil { - return nil, err - } - - resp, err := http.Get("http://" + addr + "/api/v0/stats/bw") - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var bw BW - err = json.NewDecoder(resp.Body).Decode(&bw) - if err != nil { - return nil, err - } - - return &bw, nil -} - -const ( - attrId = "id" - attrPath = "path" - attrBwIn = "bw_in" - attrBwOut = "bw_out" -) - -func GetListOfAttr() []string { - return []string{attrId, attrPath, attrBwIn, attrBwOut} -} - -func GetAttrDescr(attr string) (string, error) { - switch attr { - case attrId: - return "node ID", nil - case attrPath: - return "node IPFS_PATH", nil - case attrBwIn: - return "node input bandwidth", nil - case attrBwOut: - return "node output bandwidth", nil - default: - return "", errors.New("unrecognized attribute") - } -}