├── .gitignore ├── ci └── Jenkinsfile ├── version.json ├── sharness ├── .gitignore ├── bin │ └── checkflags ├── t0010-version.sh ├── t0020-stop.sh ├── t0000-sharness.sh ├── lib │ ├── test-lib.sh │ └── install-sharness.sh └── Makefile ├── util ├── proc_windows.go ├── proc_unix.go ├── util.go └── output.go ├── Makefile ├── main.go ├── go.mod ├── .github ├── workflows │ ├── stale.yml │ ├── generated-pr.yml │ ├── tagpush.yml │ ├── releaser.yml │ ├── go-check.yml │ ├── release-check.yml │ └── go-test.yml └── ISSUE_TEMPLATE │ ├── config.yml │ └── open_an_issue.md ├── commands ├── commands.go ├── shell.go ├── events.go ├── stop.go ├── init.go ├── start.go ├── restart.go ├── metric.go ├── testbed.go ├── logs.go ├── utils_test.go ├── auto.go ├── connect.go ├── run.go ├── attr.go └── utils.go ├── testbed ├── interfaces │ ├── output.go │ └── node.go ├── spec.go └── testbed.go ├── LICENSE ├── CHANGELOG.md ├── go.sum ├── README.md └── cli └── cli.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | iptb 3 | -------------------------------------------------------------------------------- /ci/Jenkinsfile: -------------------------------------------------------------------------------- 1 | golang() 2 | 3 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1.4.1" 3 | } 4 | -------------------------------------------------------------------------------- /sharness/.gitignore: -------------------------------------------------------------------------------- 1 | lib/sharness/ 2 | test-results/ 3 | trash directory.*.sh/ 4 | bin/iptb 5 | BUILD-OPTIONS -------------------------------------------------------------------------------- /util/proc_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package iptbutil 4 | 5 | import ( 6 | "os/exec" 7 | ) 8 | 9 | func SetupOpt(cmd *exec.Cmd) { 10 | // Do nothing 11 | } 12 | -------------------------------------------------------------------------------- /util/proc_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package iptbutil 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func SetupOpt(cmd *exec.Cmd) { 11 | cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CLEAN = 2 | 3 | all: iptb 4 | 5 | iptb: 6 | go build 7 | CLEAN += iptb 8 | 9 | install: 10 | go install 11 | 12 | test: 13 | make -C sharness all 14 | 15 | clean: 16 | rm $(CLEAN) 17 | 18 | .PHONY: all test iptb install plugins clean 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/ipfs/iptb/cli" 8 | ) 9 | 10 | func main() { 11 | cli := cli.NewCli() 12 | 13 | if err := cli.Run(os.Args); err != nil { 14 | fmt.Fprintf(cli.ErrWriter, "%s\n", err) 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ipfs/iptb 2 | 3 | require ( 4 | github.com/mattn/go-shellwords v1.0.12 5 | github.com/urfave/cli v1.22.16 6 | ) 7 | 8 | require ( 9 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 10 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 11 | ) 12 | 13 | go 1.24 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func NewUsageError(s string) error { 8 | return &UsageError{ 9 | s, 10 | } 11 | } 12 | 13 | type UsageError struct { 14 | s string 15 | } 16 | 17 | func (e *UsageError) Error() string { 18 | return fmt.Sprintf("Usage Error: %s", e.s) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Getting Help on IPFS 4 | url: https://ipfs.io/help 5 | about: All information about how and where to get help on IPFS. 6 | - name: IPFS Official Forum 7 | url: https://discuss.ipfs.io 8 | about: Please post general questions, support requests, and discussions here. 9 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package iptbutil 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func YesNoPrompt(prompt string) bool { 8 | var s string 9 | for { 10 | fmt.Printf("%s [y/n] ", prompt) 11 | fmt.Scanf("%s", &s) 12 | switch s { 13 | case "y", "Y": 14 | return true 15 | case "n", "N": 16 | return false 17 | } 18 | fmt.Println("Please press either 'y' or 'n'") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/tagpush.yml: -------------------------------------------------------------------------------- 1 | name: Tag Push Checker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: read 10 | issues: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | releaser: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/tagpush.yml@v1.0 19 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Releaser 2 | 3 | on: 4 | push: 5 | paths: [ 'version.json' ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.sha }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | releaser: 17 | uses: ipdxco/unified-github-workflows/.github/workflows/releaser.yml@v1.0 18 | -------------------------------------------------------------------------------- /.github/workflows/go-check.yml: -------------------------------------------------------------------------------- 1 | name: Go Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["master"] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | go-check: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v1.0 19 | -------------------------------------------------------------------------------- /testbed/interfaces/output.go: -------------------------------------------------------------------------------- 1 | package testbedi 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Output manages running, inprocess, a process 8 | type Output interface { 9 | // Args is the cleaned up version of the input. 10 | Args() []string 11 | 12 | // Error is the error returned from the command, after it exited. 13 | Error() error 14 | 15 | // Code is the unix style exit code, set after the command exited. 16 | ExitCode() int 17 | 18 | Stdout() io.ReadCloser 19 | Stderr() io.ReadCloser 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release-check.yml: -------------------------------------------------------------------------------- 1 | name: Release Checker 2 | 3 | on: 4 | pull_request_target: 5 | paths: [ 'version.json' ] 6 | types: [ opened, synchronize, reopened, labeled, unlabeled ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | release-check: 19 | uses: ipdxco/unified-github-workflows/.github/workflows/release-check.yml@v1.0 20 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Go Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["master"] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | go-test: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/go-test.yml@v1.0 19 | secrets: 20 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 21 | -------------------------------------------------------------------------------- /sharness/bin/checkflags: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Author: Christian Couder 3 | # MIT LICENSED 4 | 5 | if test "$#" -lt 3 6 | then 7 | echo >&2 "usage $0 FILE VALUES MSG..." 8 | exit 1 9 | fi 10 | 11 | FLAG_FILE="$1" 12 | FLAG_VALS="$2" 13 | shift 14 | shift 15 | FLAG_MSGS="$@" 16 | 17 | test -f $FLAG_FILE || touch $FLAG_FILE 18 | 19 | # Use x in front of tested values as flags could be 20 | # interpreted by "test" to be for itself. 21 | if test x"$FLAG_VALS" != x"$(cat "$FLAG_FILE")" 22 | then 23 | echo "$FLAG_MSGS" 24 | echo "$FLAG_VALS" >"$FLAG_FILE" 25 | fi 26 | -------------------------------------------------------------------------------- /sharness/t0010-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description="iptb --version and --help tests" 4 | 5 | . lib/test-lib.sh 6 | 7 | test_expect_success "iptb binary is here" ' 8 | test -f ../bin/iptb 9 | ' 10 | 11 | test_expect_success "'iptb --version' works" ' 12 | iptb --version >actual 13 | ' 14 | 15 | test_expect_success "'iptb --version' output looks good" ' 16 | egrep "^iptb version [0-9]+.[0-9]+.[0-9]+$" actual 17 | ' 18 | 19 | test_expect_success "'iptb --help' works" ' 20 | iptb --help >actual 21 | ' 22 | 23 | test_expect_success "'iptb --help' output looks good" ' 24 | grep "COMMANDS" actual && 25 | grep "USAGE" actual 26 | ' 27 | 28 | test_done 29 | -------------------------------------------------------------------------------- /sharness/t0020-stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description="iptb stop tests" 4 | 5 | . lib/test-lib.sh 6 | 7 | export IPTB_ROOT=. 8 | ln -s ../plugins $IPTB_ROOT/plugins 9 | 10 | test_expect_success "iptb auto works" ' 11 | ../bin/iptb auto -count 3 -type localipfs 12 | ' 13 | 14 | test_expect_success "iptb start works" ' 15 | ../bin/iptb start --wait -- --debug 16 | ' 17 | 18 | test_expect_success "iptb stop works" ' 19 | ../bin/iptb stop && sleep 1 20 | ' 21 | 22 | for i in {0..2}; do 23 | test_expect_success "daemon '$i' was shut down gracefully" ' 24 | cat testbeds/default/'$i'/daemon.stderr | tail -1 | grep "Gracefully shut down daemon" 25 | ' 26 | done 27 | 28 | test_done 29 | -------------------------------------------------------------------------------- /sharness/t0000-sharness.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description="Show basic features of Sharness" 4 | 5 | . lib/test-lib.sh 6 | 7 | test_expect_success "Success is reported like this" " 8 | echo hello world | grep hello 9 | " 10 | 11 | test_expect_success "Commands are chained this way" " 12 | test x = 'x' && 13 | test 2 -gt 1 && 14 | echo success 15 | " 16 | 17 | return_42() { 18 | echo "Will return soon" 19 | return 42 20 | } 21 | 22 | test_expect_success "You can test for a specific exit code" " 23 | test_expect_code 42 return_42 24 | " 25 | 26 | test_expect_failure "We expect this to fail" " 27 | test 1 = 2 28 | " 29 | 30 | test_done 31 | 32 | # vi: set ft=sh : 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/open_an_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Open an issue 3 | about: Only for actionable issues relevant to this repository. 4 | title: '' 5 | labels: need/triage 6 | assignees: '' 7 | 8 | --- 9 | 20 | -------------------------------------------------------------------------------- /util/output.go: -------------------------------------------------------------------------------- 1 | package iptbutil 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 8 | ) 9 | 10 | type Output struct { 11 | args []string 12 | 13 | exitcode int 14 | 15 | err error 16 | stdout []byte 17 | stderr []byte 18 | } 19 | 20 | func NewOutput(args []string, stdout, stderr []byte, exitcode int, cmderr error) testbedi.Output { 21 | return &Output{ 22 | args: args, 23 | stdout: stdout, 24 | stderr: stderr, 25 | exitcode: exitcode, 26 | err: cmderr, 27 | } 28 | } 29 | 30 | func (o *Output) Args() []string { 31 | return o.args 32 | } 33 | 34 | func (o *Output) Error() error { 35 | return o.err 36 | } 37 | func (o *Output) ExitCode() int { 38 | return o.exitcode 39 | } 40 | 41 | func (o *Output) Stdout() io.ReadCloser { 42 | return io.NopCloser(bytes.NewReader(o.stdout)) 43 | } 44 | 45 | func (o *Output) Stderr() io.ReadCloser { 46 | return io.NopCloser(bytes.NewReader(o.stderr)) 47 | } 48 | -------------------------------------------------------------------------------- /commands/shell.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | "strconv" 8 | 9 | cli "github.com/urfave/cli" 10 | 11 | "github.com/ipfs/iptb/testbed" 12 | ) 13 | 14 | var ShellCmd = cli.Command{ 15 | Category: "CORE", 16 | Name: "shell", 17 | Usage: "starts a shell within the context of node", 18 | ArgsUsage: "", 19 | Action: func(c *cli.Context) error { 20 | flagRoot := c.GlobalString("IPTB_ROOT") 21 | flagTestbed := c.GlobalString("testbed") 22 | 23 | if !c.Args().Present() { 24 | return NewUsageError("shell takes exactly 1 argument") 25 | } 26 | 27 | i, err := strconv.Atoi(c.Args().First()) 28 | if err != nil { 29 | return fmt.Errorf("parse err: %s", err) 30 | } 31 | 32 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 33 | 34 | nodes, err := tb.Nodes() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return nodes[i].Shell(context.Background(), nodes) 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /sharness/lib/test-lib.sh: -------------------------------------------------------------------------------- 1 | # Test framework for ipfs-update 2 | # 3 | # Copyright (c) 2015 Christian Couder 4 | # MIT Licensed; see the LICENSE file in this repository. 5 | # 6 | # We are using Sharness (https://github.com/mlafeldt/sharness) 7 | # which was extracted from the Git test framework. 8 | 9 | SHARNESS_LIB="lib/sharness/sharness.sh" 10 | 11 | # Set sharness verbosity. we set the env var directly as 12 | # it's too late to pass in --verbose, and --verbose is harder 13 | # to pass through in some cases. 14 | test "$TEST_VERBOSE" = 1 && verbose=t && echo '# TEST_VERBOSE='"$TEST_VERBOSE" 15 | 16 | . "$SHARNESS_LIB" || { 17 | echo >&2 "Cannot source: $SHARNESS_LIB" 18 | echo >&2 "Please check Sharness installation." 19 | exit 1 20 | } 21 | 22 | # Please put iptb specific shell functions and variables below 23 | 24 | # Echo the args, run the cmd, and then also fail, 25 | # making sure a test case fails. 26 | test_fsh() { 27 | echo "> $@" 28 | eval "$@" 29 | echo "" 30 | false 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeromy Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /commands/events.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path" 7 | "strconv" 8 | 9 | cli "github.com/urfave/cli" 10 | 11 | "github.com/ipfs/iptb/testbed" 12 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 13 | ) 14 | 15 | var EventsCmd = cli.Command{ 16 | Category: "METRICS", 17 | Name: "events", 18 | Usage: "stream events from specified nodes (or all)", 19 | ArgsUsage: "[node]", 20 | Action: func(c *cli.Context) error { 21 | flagRoot := c.GlobalString("IPTB_ROOT") 22 | flagTestbed := c.GlobalString("testbed") 23 | 24 | if !c.Args().Present() { 25 | return NewUsageError("events takes exactly 1 argument") 26 | } 27 | 28 | i, err := strconv.Atoi(c.Args().First()) 29 | if err != nil { 30 | return fmt.Errorf("parse err: %s", err) 31 | } 32 | 33 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 34 | 35 | node, err := tb.Node(i) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | mn, ok := node.(testbedi.Metric) 41 | if !ok { 42 | return fmt.Errorf("node does not implement metrics") 43 | } 44 | 45 | el, err := mn.Events() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | _, err = io.Copy(c.App.Writer, el) 51 | return err 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /commands/stop.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | ) 13 | 14 | var StopCmd = cli.Command{ 15 | Category: "CORE", 16 | Name: "stop", 17 | Usage: "stop specified nodes (or all)", 18 | ArgsUsage: "[nodes]", 19 | Action: func(c *cli.Context) error { 20 | flagRoot := c.GlobalString("IPTB_ROOT") 21 | flagTestbed := c.GlobalString("testbed") 22 | flagQuiet := c.GlobalBool("quiet") 23 | 24 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 25 | nodes, err := tb.Nodes() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | nodeRange := c.Args().First() 31 | 32 | if nodeRange == "" { 33 | nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) 34 | } 35 | 36 | list, err := parseRange(nodeRange) 37 | if err != nil { 38 | return fmt.Errorf("could not parse node range %s", nodeRange) 39 | } 40 | 41 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 42 | return nil, node.Stop(context.Background()) 43 | } 44 | 45 | results, err := mapWithOutput(list, nodes, runCmd) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return buildReport(results, flagQuiet) 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /sharness/Makefile: -------------------------------------------------------------------------------- 1 | # Run sharness tests 2 | # 3 | # Copyright (c) 2014 Christian Couder 4 | # MIT Licensed; see the LICENSE file in this repository. 5 | # 6 | # NOTE: run with TEST_VERBOSE=1 for verbose sharness tests. 7 | 8 | T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) 9 | LIBDIR = lib 10 | SHARNESSDIR = sharness 11 | AGGREGATE = $(LIBDIR)/$(SHARNESSDIR)/aggregate-results.sh 12 | 13 | # Binaries generated 14 | BINS = bin/iptb 15 | 16 | # Source files location 17 | IPTB_SRC = ../ 18 | 19 | # User might want to override those on the command line 20 | GOFLAGS = 21 | 22 | all: aggregate 23 | 24 | clean: clean-test-results 25 | @echo "*** $@ ***" 26 | -rm -rf $(BINS) 27 | -rm -rf plugins 28 | 29 | clean-test-results: 30 | @echo "*** $@ ***" 31 | -rm -rf test-results 32 | 33 | $(T): clean-test-results deps 34 | @echo "*** $@ ***" 35 | ./$@ 36 | 37 | aggregate: clean-test-results $(T) 38 | @echo "*** $@ ***" 39 | ls test-results/t*-*.sh.*.counts | $(AGGREGATE) 40 | 41 | # Needed dependencies. 42 | deps: sharness $(BINS) plugins 43 | 44 | sharness: 45 | @echo "*** checking $@ ***" 46 | lib/install-sharness.sh 47 | 48 | find_go_files = $(shell find $(1) -name "*.go") 49 | 50 | bin/iptb: $(call find_go_files, $(IPTB_SRC)) BUILD-OPTIONS 51 | @echo "*** installing $@ ***" 52 | go build $(GOFLAGS) -o $@ $(IPTB_SRC) 53 | 54 | plugins: 55 | make -C ../plugins all 56 | make -C ../plugins install IPTB_ROOT=$(shell pwd) 57 | 58 | race: 59 | make GOFLAGS=-race all 60 | 61 | BUILD-OPTIONS: FORCE 62 | @bin/checkflags '$@' '$(GOFLAGS)' '*** new Go flags ***' 63 | 64 | .PHONY: all clean clean-test-results $(T) aggregate deps sharness FORCE 65 | 66 | -------------------------------------------------------------------------------- /commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | ) 13 | 14 | var InitCmd = cli.Command{ 15 | Category: "CORE", 16 | Name: "init", 17 | Usage: "initialize specified nodes (or all)", 18 | ArgsUsage: "[nodes] -- [arguments...]", 19 | Flags: []cli.Flag{ 20 | cli.BoolFlag{ 21 | Name: "terminator", 22 | Hidden: true, 23 | }, 24 | }, 25 | Before: func(c *cli.Context) error { 26 | if present := isTerminatorPresent(c); present { 27 | return c.Set("terminator", "true") 28 | } 29 | 30 | return nil 31 | }, 32 | Action: func(c *cli.Context) error { 33 | flagRoot := c.GlobalString("IPTB_ROOT") 34 | flagTestbed := c.GlobalString("testbed") 35 | flagQuiet := c.GlobalBool("quiet") 36 | 37 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 38 | nodes, err := tb.Nodes() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) 44 | 45 | if nodeRange == "" { 46 | nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) 47 | } 48 | 49 | list, err := parseRange(nodeRange) 50 | if err != nil { 51 | return fmt.Errorf("could not parse node range %s", nodeRange) 52 | } 53 | 54 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 55 | return node.Init(context.Background(), args...) 56 | } 57 | 58 | results, err := mapWithOutput(list, nodes, runCmd) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | return buildReport(results, flagQuiet) 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # IPTB Changelog 2 | 3 | ## 2.0.0 2018-11-02 4 | 5 | The **IPTB v2** release is a complete rewrite for the most part that shifts IPTB to a more general tool that many projects outside IPFS can use independently. 6 | 7 | ### Highlights 8 | The key differences from IPTB v1 are changes to the [Core Interface](#core-interface) to define functionality and make IPTB easier to extend, use of [plugins](#plugins) to load functionality, and updates to the [CLI](#cli) to make it easier to use independently. 9 | 10 | ### Core Interface 11 | IPTB previously had some built in assumptions, largely around go-ipfs. We now have a set of interfaces that define all of IPTBs functionality and make it easier to extend, and support other projects. 12 | 13 | - Core 14 | - Metrics 15 | - Attributes 16 | 17 | See https://github.com/ipfs/iptb/blob/master/testbed/interfaces/node.go 18 | 19 | ### Plugins 20 | IPTB uses plugins to load functionality. Plugins can be loaded from disk by placing them under $IPTB_ROOT/plugins, or by building them into the IPTB binary and registering them. 21 | 22 | Current plugins written for the IPFS project can be found @ https://github.com/ipfs/iptb-plugins 23 | 24 | ### CLI 25 | Due to the large changes under the hood to IPTB, we wanted to also take the time to update the cli. All commands now use the same order, accepting the node ID / range as the first argument. The CLI closely maps to the IPTB interfaces. 26 | 27 | See README https://github.com/ipfs/iptb#usage 28 | 29 | The CLI is also now a package itself, which makes it really easy to roll-your-own-iptb by registering plugins. This makes it easy to build a custom IPTB with built in plugins and not having to worry as much with moving plugins around on disk. 30 | 31 | See https://github.com/ipfs/iptb-plugins/blob/master/iptb/iptb.go 32 | -------------------------------------------------------------------------------- /commands/start.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | ) 13 | 14 | var StartCmd = cli.Command{ 15 | Category: "CORE", 16 | Name: "start", 17 | Usage: "start specified nodes (or all)", 18 | ArgsUsage: "[nodes] -- [arguments...]", 19 | Flags: []cli.Flag{ 20 | cli.BoolFlag{ 21 | Name: "wait", 22 | Usage: "wait for nodes to start before returning", 23 | }, 24 | cli.BoolFlag{ 25 | Name: "terminator", 26 | Hidden: true, 27 | }, 28 | }, 29 | Before: func(c *cli.Context) error { 30 | if present := isTerminatorPresent(c); present { 31 | return c.Set("terminator", "true") 32 | } 33 | 34 | return nil 35 | }, 36 | Action: func(c *cli.Context) error { 37 | flagRoot := c.GlobalString("IPTB_ROOT") 38 | flagTestbed := c.GlobalString("testbed") 39 | flagQuiet := c.GlobalBool("quiet") 40 | flagWait := c.Bool("wait") 41 | 42 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 43 | nodes, err := tb.Nodes() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) 49 | 50 | if nodeRange == "" { 51 | nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) 52 | } 53 | 54 | list, err := parseRange(nodeRange) 55 | if err != nil { 56 | return fmt.Errorf("could not parse node range %s", nodeRange) 57 | } 58 | 59 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 60 | return node.Start(context.Background(), flagWait, args...) 61 | } 62 | 63 | results, err := mapWithOutput(list, nodes, runCmd) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return buildReport(results, flagQuiet) 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /commands/restart.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | ) 13 | 14 | var RestartCmd = cli.Command{ 15 | Category: "CORE", 16 | Name: "restart", 17 | Usage: "restart specified nodes (or all)", 18 | ArgsUsage: "[nodes] -- [arguments...]", 19 | Flags: []cli.Flag{ 20 | cli.BoolFlag{ 21 | Name: "wait", 22 | Usage: "wait for nodes to start before returning", 23 | }, 24 | cli.BoolFlag{ 25 | Name: "terminator", 26 | Hidden: true, 27 | }, 28 | }, 29 | Before: func(c *cli.Context) error { 30 | if present := isTerminatorPresent(c); present { 31 | return c.Set("terminator", "true") 32 | } 33 | 34 | return nil 35 | }, 36 | Action: func(c *cli.Context) error { 37 | flagRoot := c.GlobalString("IPTB_ROOT") 38 | flagTestbed := c.GlobalString("testbed") 39 | flagQuiet := c.GlobalBool("quiet") 40 | flagWait := c.Bool("wait") 41 | 42 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 43 | nodes, err := tb.Nodes() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | nodeRange, args := parseCommand(c.Args(), c.IsSet("terminator")) 49 | 50 | if nodeRange == "" { 51 | nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) 52 | } 53 | 54 | list, err := parseRange(nodeRange) 55 | if err != nil { 56 | return fmt.Errorf("could not parse node range %s", nodeRange) 57 | } 58 | 59 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 60 | if err := node.Stop(context.Background()); err != nil { 61 | return nil, err 62 | } 63 | 64 | return node.Start(context.Background(), flagWait, args...) 65 | } 66 | 67 | results, err := mapWithOutput(list, nodes, runCmd) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | return buildReport(results, flagQuiet) 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /sharness/lib/install-sharness.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # install-sharness.sh 3 | # 4 | # Copyright (c) 2014 Juan Batiz-Benet 5 | # Copyright (c) 2015 Christian Couder 6 | # MIT Licensed; see the LICENSE file in this repository. 7 | # 8 | # This script checks that Sharness is installed in: 9 | # 10 | # $(pwd)/$clonedir/$sharnessdir/ 11 | # 12 | # where $clonedir and $sharnessdir are configured below. 13 | # 14 | # If Sharness is not installed, this script will clone it 15 | # from $urlprefix (defined below). 16 | # 17 | # If Sharness is not uptodate with $version (defined below), 18 | # this script will fetch and will update the installed 19 | # version to $version. 20 | # 21 | 22 | # settings 23 | version=35e1480425c022cb964b614621bdcd21ceaf2e94 24 | urlprefix=https://github.com/mlafeldt/sharness.git 25 | clonedir=lib 26 | sharnessdir=sharness 27 | 28 | if test -f "$clonedir/$sharnessdir/SHARNESS_VERSION_$version" 29 | then 30 | # There is the right version file. Great, we are done! 31 | exit 0 32 | fi 33 | 34 | die() { 35 | echo >&2 "$@" 36 | exit 1 37 | } 38 | 39 | checkout_version() { 40 | git checkout "$version" || die "Could not checkout '$version'" 41 | rm -f SHARNESS_VERSION_* || die "Could not remove 'SHARNESS_VERSION_*'" 42 | touch "SHARNESS_VERSION_$version" || die "Could not create 'SHARNESS_VERSION_$version'" 43 | echo "Sharness version $version is checked out!" 44 | } 45 | 46 | if test -d "$clonedir/$sharnessdir/.git" 47 | then 48 | # We need to update sharness! 49 | cd "$clonedir/$sharnessdir" || die "Could not cd into '$clonedir/$sharnessdir' directory" 50 | git fetch || die "Could not fetch to update sharness" 51 | else 52 | # We need to clone sharness! 53 | mkdir -p "$clonedir" || die "Could not create '$clonedir' directory" 54 | cd "$clonedir" || die "Could not cd into '$clonedir' directory" 55 | 56 | git clone "$urlprefix" || die "Could not clone '$urlprefix'" 57 | cd "$sharnessdir" || die "Could not cd into '$sharnessdir' directory" 58 | fi 59 | 60 | checkout_version 61 | -------------------------------------------------------------------------------- /commands/metric.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strconv" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | ) 13 | 14 | var MetricCmd = cli.Command{ 15 | Category: "METRICS", 16 | Name: "metric", 17 | Usage: "get metric from node", 18 | ArgsUsage: " [metric]", 19 | Action: func(c *cli.Context) error { 20 | if c.NArg() == 1 { 21 | return metricList(c) 22 | } 23 | 24 | if c.NArg() == 2 { 25 | return metricGet(c) 26 | } 27 | 28 | return NewUsageError("metric takes 1 or 2 arguments only") 29 | }, 30 | } 31 | 32 | func metricList(c *cli.Context) error { 33 | flagRoot := c.GlobalString("IPTB_ROOT") 34 | flagTestbed := c.GlobalString("testbed") 35 | 36 | i, err := strconv.Atoi(c.Args().First()) 37 | if err != nil { 38 | return fmt.Errorf("parse err: %s", err) 39 | } 40 | 41 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 42 | 43 | node, err := tb.Node(i) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | metricNode, ok := node.(testbedi.Metric) 49 | if !ok { 50 | return fmt.Errorf("node does not implement metrics") 51 | } 52 | 53 | metricList := metricNode.GetMetricList() 54 | for _, m := range metricList { 55 | desc, err := metricNode.GetMetricDesc(m) 56 | if err != nil { 57 | return fmt.Errorf("error getting metric description: %s", err) 58 | } 59 | 60 | fmt.Fprintf(c.App.Writer, "\t%s: %s\n", m, desc) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func metricGet(c *cli.Context) error { 67 | flagRoot := c.GlobalString("IPTB_ROOT") 68 | flagTestbed := c.GlobalString("testbed") 69 | 70 | argNode := c.Args()[0] 71 | argMetric := c.Args()[1] 72 | 73 | i, err := strconv.Atoi(argNode) 74 | if err != nil { 75 | return fmt.Errorf("parse err: %s", err) 76 | } 77 | 78 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 79 | 80 | node, err := tb.Node(i) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | metricNode, ok := node.(testbedi.Metric) 86 | if !ok { 87 | return fmt.Errorf("node does not implement metrics") 88 | } 89 | 90 | value, err := metricNode.Metric(argMetric) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | _, err = fmt.Fprintf(c.App.Writer, "%s\n", value) 96 | 97 | return err 98 | } 99 | -------------------------------------------------------------------------------- /commands/testbed.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | ) 12 | 13 | var TestbedCmd = cli.Command{ 14 | Name: "testbed", 15 | Usage: "manage testbeds", 16 | Subcommands: []cli.Command{ 17 | TestbedCreateCmd, 18 | }, 19 | } 20 | 21 | var TestbedCreateCmd = cli.Command{ 22 | Name: "create", 23 | Usage: "create testbed", 24 | ArgsUsage: "--type ", 25 | Flags: []cli.Flag{ 26 | cli.IntFlag{ 27 | Name: "count", 28 | Usage: "number of nodes to initialize", 29 | Value: 0, 30 | }, 31 | cli.BoolFlag{ 32 | Name: "force", 33 | Usage: "force overwrite of existing testbed", 34 | }, 35 | cli.StringFlag{ 36 | Name: "type", 37 | Usage: "kind of nodes to initialize", 38 | EnvVar: "IPTB_PLUGIN", 39 | }, 40 | cli.StringSliceFlag{ 41 | Name: "attr", 42 | Usage: "specify addition attributes for nodes", 43 | }, 44 | cli.BoolFlag{ 45 | Name: "init", 46 | Usage: "initialize after creation (like calling `init` after create)", 47 | }, 48 | }, 49 | Action: func(c *cli.Context) error { 50 | flagRoot := c.GlobalString("IPTB_ROOT") 51 | flagTestbed := c.GlobalString("testbed") 52 | flagType := c.String("type") 53 | flagInit := c.Bool("init") 54 | flagCount := c.Int("count") 55 | flagForce := c.Bool("force") 56 | flagAttrs := c.StringSlice("attr") 57 | 58 | attrs := parseAttrSlice(flagAttrs) 59 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 60 | 61 | if err := testbed.AlreadyInitCheck(tb.Dir(), flagForce); err != nil { 62 | return err 63 | } 64 | 65 | var specs []*testbed.NodeSpec 66 | if flagCount > 0 { 67 | if flagType == "" { 68 | return fmt.Errorf("must specify a type to create testbed nodes") 69 | } 70 | 71 | var err error 72 | specs, err = testbed.BuildSpecs(tb.Dir(), flagCount, flagType, attrs) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | 78 | if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil { 79 | return err 80 | } 81 | 82 | if flagInit { 83 | nodes, err := tb.Nodes() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | for _, n := range nodes { 89 | if _, err := n.Init(context.Background()); err != nil { 90 | return err 91 | } 92 | } 93 | } 94 | 95 | return nil 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 8 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 15 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 16 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 17 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 19 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 20 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 21 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 22 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 23 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPTB 2 | 3 | `iptb` is a program used to create and manage a cluster of sandboxed nodes 4 | locally on your computer. Spin up 1000s of nodes! Using `iptb` makes testing 5 | libp2p networks easy! 6 | 7 | For working with IPFS please see [ipfs/iptb-plugins](https://github.com/ipfs/iptb-plugins). 8 | 9 | ### Example (ipfs) 10 | 11 | ``` 12 | $ iptb auto -type -count 5 13 | 14 | 15 | $ iptb start 16 | 17 | $ iptb shell 0 18 | $ echo $IPFS_PATH 19 | /home/iptb/testbed/testbeds/default/0 20 | 21 | $ echo 'hey!' | ipfs add -q 22 | QmNqugRcYjwh9pEQUK7MLuxvLjxDNZL1DH8PJJgWtQXxuF 23 | 24 | $ exit 25 | 26 | $ iptb connect 0 4 27 | 28 | $ iptb shell 4 29 | $ ipfs cat QmNqugRcYjwh9pEQUK7MLuxvLjxDNZL1DH8PJJgWtQXxuF 30 | hey! 31 | ``` 32 | 33 | ### Usage 34 | ``` 35 | NAME: 36 | iptb - iptb is a tool for managing test clusters of libp2p nodes 37 | 38 | USAGE: 39 | iptb [global options] command [command options] [arguments...] 40 | 41 | VERSION: 42 | 2.0.0 43 | 44 | COMMANDS: 45 | auto create default testbed and initialize 46 | testbed manage testbeds 47 | help, h Shows a list of commands or help for one command 48 | ATTRIBUTES: 49 | attr get, set, list attributes 50 | CORE: 51 | init initialize specified nodes (or all) 52 | start start specified nodes (or all) 53 | stop stop specified nodes (or all) 54 | restart restart specified nodes (or all) 55 | run run command on specified nodes (or all) 56 | connect connect sets of nodes together (or all) 57 | shell starts a shell within the context of node 58 | METRICS: 59 | logs show logs from specified nodes (or all) 60 | events stream events from specified nodes (or all) 61 | metric get metric from node 62 | 63 | GLOBAL OPTIONS: 64 | --testbed value Name of testbed to use under IPTB_ROOT (default: "default") [$IPTB_TESTBED] 65 | --quiet Suppresses extra output from iptb 66 | --help, -h show help 67 | --version, -v print the version 68 | ``` 69 | 70 | ### Install 71 | 72 | _Note: For MacOS golang v1.11 is needed to support plugin loading 73 | (see [golang/go#24653](https://github.com/golang/go/issues/24653) for more information)_ 74 | 75 | ``` 76 | $ go get github.com/ipfs/iptb 77 | ``` 78 | 79 | ### Plugins 80 | 81 | Plugins are now used to implement support for managing nodes. Plugins are 82 | stored under `$IPTB_ROOT/plugins` (see [configuration](#configuration)) 83 | 84 | Plugins for the IPFS project can be found in [ipfs/iptb-plugins](https://github.com/ipfs/iptb-plugins). 85 | 86 | ### Configuration 87 | 88 | By default, `iptb` uses `$HOME/testbed` to store created nodes. This path is configurable via the environment variables `IPTB_ROOT`. 89 | 90 | ### License 91 | 92 | MIT 93 | -------------------------------------------------------------------------------- /commands/logs.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path" 7 | "strings" 8 | 9 | cli "github.com/urfave/cli" 10 | 11 | "github.com/ipfs/iptb/testbed" 12 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 13 | ) 14 | 15 | var LogsCmd = cli.Command{ 16 | Category: "METRICS", 17 | Name: "logs", 18 | Usage: "show logs from specified nodes (or all)", 19 | ArgsUsage: "[nodes]", 20 | Flags: []cli.Flag{ 21 | cli.BoolTFlag{ 22 | Name: "err, e", 23 | Usage: "show stderr stream", 24 | }, 25 | cli.BoolTFlag{ 26 | Name: "out, o", 27 | Usage: "show stdout stream", 28 | }, 29 | }, 30 | Action: func(c *cli.Context) error { 31 | flagRoot := c.GlobalString("IPTB_ROOT") 32 | flagTestbed := c.GlobalString("testbed") 33 | flagQuiet := c.GlobalBool("quiet") 34 | flagErr := c.BoolT("err") 35 | flagOut := c.BoolT("out") 36 | 37 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 38 | nodes, err := tb.Nodes() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | nodeRange := c.Args().First() 44 | 45 | if nodeRange == "" { 46 | nodeRange = fmt.Sprintf("[0-%d]", len(nodes)-1) 47 | } 48 | 49 | list, err := parseRange(nodeRange) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 55 | metricNode, ok := node.(testbedi.Metric) 56 | if !ok { 57 | return nil, fmt.Errorf("node does not implement metrics") 58 | } 59 | 60 | stdout := io.NopCloser(strings.NewReader("")) 61 | stderr := io.NopCloser(strings.NewReader("")) 62 | 63 | if flagOut { 64 | var err error 65 | stdout, err = metricNode.StdoutReader() 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | 71 | if flagErr { 72 | var err error 73 | stderr, err = metricNode.StderrReader() 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | return NewOutput(stdout, stderr), nil 80 | } 81 | 82 | results, err := mapWithOutput(list, nodes, runCmd) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return buildReport(results, flagQuiet) 88 | }, 89 | } 90 | 91 | func NewOutput(stdout, stderr io.ReadCloser) testbedi.Output { 92 | return &Output{ 93 | stdout: stdout, 94 | stderr: stderr, 95 | } 96 | } 97 | 98 | type Output struct { 99 | stdout io.ReadCloser 100 | stderr io.ReadCloser 101 | } 102 | 103 | func (o *Output) Args() []string { 104 | return []string{} 105 | } 106 | 107 | func (o *Output) Error() error { 108 | return nil 109 | } 110 | func (o *Output) ExitCode() int { 111 | return 0 112 | } 113 | 114 | func (o *Output) Stdout() io.ReadCloser { 115 | return o.stdout 116 | } 117 | 118 | func (o *Output) Stderr() io.ReadCloser { 119 | return o.stderr 120 | } 121 | -------------------------------------------------------------------------------- /commands/utils_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "runtime" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var ( 13 | wd, _ = os.Getwd() 14 | ) 15 | 16 | func expect(t *testing.T, a interface{}, b interface{}) { 17 | _, fn, line, _ := runtime.Caller(1) 18 | fn = strings.Replace(fn, wd+"/", "", -1) 19 | 20 | if !reflect.DeepEqual(a, b) { 21 | t.Errorf("(%s:%d) Expected %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 22 | } 23 | } 24 | 25 | func TestParseRange(t *testing.T) { 26 | cases := []struct { 27 | input string 28 | expectedList []int 29 | expectedErr error 30 | }{ 31 | {"0", []int{0}, nil}, 32 | {"[0-1]", []int{0, 1}, nil}, 33 | {"[0-5]", []int{0, 1, 2, 3, 4, 5}, nil}, 34 | {"[4-7]", []int{4, 5, 6, 7}, nil}, 35 | {"[0,1]", []int{0, 1}, nil}, 36 | {"[1,4]", []int{1, 4}, nil}, 37 | {"[1,3,5-8]", []int{1, 3, 5, 6, 7, 8}, nil}, 38 | } 39 | 40 | for _, c := range cases { 41 | list, err := parseRange(c.input) 42 | 43 | expect(t, err, c.expectedErr) 44 | expect(t, list, c.expectedList) 45 | } 46 | } 47 | 48 | func TestValidRange(t *testing.T) { 49 | buildError := func(max, total int) error { 50 | return fmt.Errorf("node range contains value (%d) outside of valid range [0-%d]", max, total-1) 51 | } 52 | 53 | cases := []struct { 54 | inputList []int 55 | inputTotal int 56 | expectedErr error 57 | }{ 58 | {[]int{0, 1}, 2, nil}, 59 | {[]int{0, 3}, 2, buildError(3, 2)}, 60 | } 61 | 62 | for _, c := range cases { 63 | err := validRange(c.inputList, c.inputTotal) 64 | 65 | expect(t, err, c.expectedErr) 66 | } 67 | } 68 | 69 | func TestParseCommand(t *testing.T) { 70 | cases := []struct { 71 | inputArgs []string 72 | inputTerm bool 73 | expectedRange string 74 | expectedArgs []string 75 | }{ 76 | {[]string{"0", "--", "--foo", "--bar"}, false, "0", []string{"--foo", "--bar"}}, 77 | {[]string{"--foo", "--bar"}, true, "", []string{"--foo", "--bar"}}, 78 | } 79 | 80 | for _, c := range cases { 81 | nodeRange, args := parseCommand(c.inputArgs, c.inputTerm) 82 | 83 | expect(t, nodeRange, c.expectedRange) 84 | expect(t, args, c.expectedArgs) 85 | } 86 | } 87 | 88 | func TestParseAttrSlice(t *testing.T) { 89 | cases := []struct { 90 | inputArgs []string 91 | expectedAttrs map[string]string 92 | }{ 93 | {[]string{}, map[string]string{}}, 94 | {[]string{"foo"}, map[string]string{"foo": "true"}}, 95 | {[]string{"foo,bar"}, map[string]string{"foo": "bar"}}, 96 | {[]string{"foo,bar,thing"}, map[string]string{"foo": "bar,thing"}}, 97 | {[]string{"foo,bar", "one,two"}, map[string]string{"foo": "bar", "one": "two"}}, 98 | } 99 | 100 | for _, c := range cases { 101 | attrs := parseAttrSlice(c.inputArgs) 102 | 103 | expect(t, attrs, c.expectedAttrs) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | 9 | cli "github.com/urfave/cli" 10 | 11 | "github.com/ipfs/iptb/commands" 12 | "github.com/ipfs/iptb/testbed" 13 | ) 14 | 15 | func loadPlugins(dir string) error { 16 | if _, err := os.Stat(dir); os.IsNotExist(err) { 17 | return nil 18 | } 19 | 20 | plugs, err := os.ReadDir(dir) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | for _, f := range plugs { 26 | plg, err := testbed.LoadPlugin(path.Join(dir, f.Name())) 27 | 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "%s\n", err) 30 | continue 31 | } 32 | 33 | overloaded, err := testbed.RegisterPlugin(*plg, false) 34 | if overloaded { 35 | fmt.Fprintf(os.Stderr, "overriding built in plugin %s with %s\n", plg.PluginName, path.Join(dir, f.Name())) 36 | } 37 | 38 | if err != nil { 39 | return err 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func NewCli() *cli.App { 47 | app := cli.NewApp() 48 | app.Usage = "iptb is a tool for managing test clusters of libp2p nodes" 49 | app.Version = "2.0.0" 50 | app.Flags = []cli.Flag{ 51 | cli.StringFlag{ 52 | Name: "testbed", 53 | Value: "default", 54 | EnvVar: "IPTB_TESTBED", 55 | Usage: "Name of testbed to use under IPTB_ROOT", 56 | }, 57 | cli.BoolFlag{ 58 | Name: "quiet", 59 | Usage: "Suppresses extra output from iptb", 60 | }, 61 | cli.StringFlag{ 62 | Name: "IPTB_ROOT", 63 | EnvVar: "IPTB_ROOT", 64 | Hidden: true, 65 | }, 66 | } 67 | app.Before = func(c *cli.Context) error { 68 | flagRoot := c.GlobalString("IPTB_ROOT") 69 | 70 | if len(flagRoot) == 0 { 71 | home := os.Getenv("HOME") 72 | if len(home) == 0 { 73 | return fmt.Errorf("environment variable HOME not set") 74 | } 75 | 76 | flagRoot = path.Join(home, "testbed") 77 | } else { 78 | var err error 79 | 80 | flagRoot, err = filepath.Abs(flagRoot) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | 86 | c.Set("IPTB_ROOT", flagRoot) 87 | 88 | return loadPlugins(path.Join(flagRoot, "plugins")) 89 | } 90 | app.Commands = []cli.Command{ 91 | commands.AutoCmd, 92 | commands.TestbedCmd, 93 | 94 | commands.InitCmd, 95 | commands.StartCmd, 96 | commands.StopCmd, 97 | commands.RestartCmd, 98 | commands.RunCmd, 99 | commands.ConnectCmd, 100 | commands.ShellCmd, 101 | 102 | commands.AttrCmd, 103 | 104 | commands.LogsCmd, 105 | commands.EventsCmd, 106 | commands.MetricCmd, 107 | } 108 | 109 | // https://github.com/urfave/cli/issues/736 110 | // Currently unreleased 111 | /* 112 | app.ExitErrHandler = func(c *cli.Context, err error) { 113 | switch err.(type) { 114 | case *commands.UsageError: 115 | fmt.Fprintf(c.App.ErrWriter, "%s\n\n", err) 116 | cli.ShowCommandHelpAndExit(c, c.Command.Name, 1) 117 | default: 118 | cli.HandleExitCoder(err) 119 | } 120 | } 121 | */ 122 | 123 | app.ErrWriter = os.Stderr 124 | app.Writer = os.Stdout 125 | 126 | return app 127 | } 128 | -------------------------------------------------------------------------------- /commands/auto.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "path" 6 | 7 | cli "github.com/urfave/cli" 8 | 9 | "github.com/ipfs/iptb/testbed" 10 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 11 | ) 12 | 13 | var AutoCmd = cli.Command{ 14 | Name: "auto", 15 | Usage: "create default testbed and initialize", 16 | Description: ` 17 | The auto command is a quick way to use iptb for simple configurations. 18 | 19 | The auto command is similar to 'testbed create' except in a few ways 20 | 21 | - No attr options can be passed in 22 | - All nodes are initialize by default and ready to be started 23 | - An optional --start flag can be passed to start all nodes 24 | 25 | The following two examples are equivalent 26 | 27 | $ iptb testbed create -count 5 -type -init 28 | $ iptb auto -count 5 -type 29 | `, 30 | ArgsUsage: "--type ", 31 | Flags: []cli.Flag{ 32 | cli.IntFlag{ 33 | Name: "count", 34 | Usage: "number of nodes to initialize", 35 | Value: 1, 36 | }, 37 | cli.BoolFlag{ 38 | Name: "force", 39 | Usage: "force overwrite of existing nodespecs", 40 | }, 41 | cli.StringFlag{ 42 | Name: "type", 43 | Usage: "kind of nodes to initialize", 44 | EnvVar: "IPTB_PLUGIN", 45 | }, 46 | cli.BoolFlag{ 47 | Name: "start", 48 | Usage: "starts nodes immediately", 49 | }, 50 | }, 51 | Action: func(c *cli.Context) error { 52 | flagRoot := c.GlobalString("IPTB_ROOT") 53 | flagTestbed := c.GlobalString("testbed") 54 | flagQuiet := c.GlobalBool("quiet") 55 | flagType := c.String("type") 56 | flagStart := c.Bool("start") 57 | flagCount := c.Int("count") 58 | flagForce := c.Bool("force") 59 | 60 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 61 | 62 | if err := testbed.AlreadyInitCheck(tb.Dir(), flagForce); err != nil { 63 | return err 64 | } 65 | 66 | specs, err := testbed.BuildSpecs(tb.Dir(), flagCount, flagType, nil) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil { 72 | return err 73 | } 74 | 75 | nodes, err := tb.Nodes() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | var list []int 81 | for i := range nodes { 82 | list = append(list, i) 83 | } 84 | 85 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 86 | return node.Init(context.Background()) 87 | } 88 | 89 | results, err := mapWithOutput(list, nodes, runCmd) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if err := buildReport(results, flagQuiet); err != nil { 95 | return err 96 | } 97 | 98 | if flagStart { 99 | runCmd := func(node testbedi.Core) (testbedi.Output, error) { 100 | return node.Start(context.Background(), true) 101 | } 102 | 103 | results, err := mapWithOutput(list, nodes, runCmd) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | if err := buildReport(results, flagQuiet); err != nil { 109 | return err 110 | } 111 | } 112 | 113 | return nil 114 | }, 115 | } 116 | -------------------------------------------------------------------------------- /commands/connect.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | "time" 8 | 9 | "github.com/ipfs/iptb/testbed" 10 | cli "github.com/urfave/cli" 11 | ) 12 | 13 | var ConnectCmd = cli.Command{ 14 | Category: "CORE", 15 | Name: "connect", 16 | Usage: "connect sets of nodes together (or all)", 17 | ArgsUsage: "[nodes] [nodes]", 18 | Description: ` 19 | The connect command allows for connecting sets of nodes together. 20 | 21 | Every node listed in the first set, will try to connect to every node 22 | listed in the second set. 23 | 24 | There are three variants of the command. It can accept no arugments, 25 | a single argument, or two arguments. The no argument and single argument 26 | expands out to the two argument usage. 27 | 28 | $ iptb connect => iptb connect [0-C] [0-C] 29 | $ iptb connect [n-m] => iptb connect [n-m] [n-m] 30 | $ iptb connect [n-m] [i-k] 31 | 32 | Sets of nodes can be expressed in the following ways 33 | 34 | INPUT EXPANDED 35 | 0 0 36 | [0] 0 37 | [0-4] 0,1,2,3,4 38 | [0,2-4] 0,2,3,4 39 | [2-4,0] 2,3,4,0 40 | [0,2,4] 0,2,4 41 | `, 42 | Flags: []cli.Flag{ 43 | cli.StringFlag{ 44 | Name: "timeout", 45 | Usage: "timeout on the command", 46 | Value: "30s", 47 | }, 48 | }, 49 | Action: func(c *cli.Context) error { 50 | flagRoot := c.GlobalString("IPTB_ROOT") 51 | flagTestbed := c.GlobalString("testbed") 52 | flagQuiet := c.GlobalBool("quiet") 53 | flagTimeout := c.String("timeout") 54 | 55 | timeout, err := time.ParseDuration(flagTimeout) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 61 | args := c.Args() 62 | 63 | var results []Result 64 | switch c.NArg() { 65 | case 0: 66 | nodes, err := tb.Nodes() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | fromto, err := parseRange(fmt.Sprintf("[0-%d]", len(nodes)-1)) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | results, err = connectNodes(tb, fromto, fromto, timeout) 77 | if err != nil { 78 | return err 79 | } 80 | case 1: 81 | fromto, err := parseRange(args[0]) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | results, err = connectNodes(tb, fromto, fromto, timeout) 87 | if err != nil { 88 | return err 89 | } 90 | case 2: 91 | from, err := parseRange(args[0]) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | to, err := parseRange(args[1]) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | results, err = connectNodes(tb, from, to, timeout) 102 | if err != nil { 103 | return err 104 | } 105 | default: 106 | return NewUsageError("connet accepts between 0 and 2 arguments") 107 | } 108 | 109 | return buildReport(results, flagQuiet) 110 | }, 111 | } 112 | 113 | func connectNodes(tb testbed.BasicTestbed, from, to []int, timeout time.Duration) ([]Result, error) { 114 | var results []Result 115 | 116 | nodes, err := tb.Nodes() 117 | if err != nil { 118 | return results, err 119 | } 120 | 121 | for _, f := range from { 122 | for _, t := range to { 123 | if f == t { 124 | continue 125 | } 126 | 127 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 128 | defer cancel() 129 | 130 | err = nodes[f].Connect(ctx, nodes[t]) 131 | if err != nil { 132 | err = fmt.Errorf("node[%d] => node[%d]: %w", f, t, err) 133 | } 134 | 135 | results = append(results, Result{ 136 | Node: f, 137 | Output: nil, 138 | Error: err, 139 | }) 140 | } 141 | } 142 | 143 | return results, nil 144 | } 145 | -------------------------------------------------------------------------------- /commands/run.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | 13 | cli "github.com/urfave/cli" 14 | 15 | "github.com/ipfs/iptb/testbed" 16 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 17 | "github.com/mattn/go-shellwords" 18 | ) 19 | 20 | var RunCmd = cli.Command{ 21 | Category: "CORE", 22 | Name: "run", 23 | Usage: "concurrently run command(s) on specified nodes (or all)", 24 | ArgsUsage: "[nodes] -- ", 25 | Description: ` 26 | Commands may also be passed in via stdin or a pipe, e.g. the command 27 | 28 | $ iptb run 0 -- echo "Running on node 0" 29 | 30 | can be equivalently written as 31 | 32 | $ iptb run <= len(specs) { 100 | return nil, fmt.Errorf("Spec index out of range") 101 | } 102 | 103 | return specs[n], err 104 | } 105 | 106 | func (tb *BasicTestbed) Specs() ([]*NodeSpec, error) { 107 | if tb.specs != nil { 108 | return tb.specs, nil 109 | } 110 | 111 | return tb.loadSpecs() 112 | } 113 | 114 | func (tb *BasicTestbed) Node(n int) (testbedi.Core, error) { 115 | nodes, err := tb.Nodes() 116 | 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | if n >= len(nodes) { 122 | return nil, fmt.Errorf("Node index out of range") 123 | } 124 | 125 | return nodes[n], err 126 | } 127 | 128 | func (tb *BasicTestbed) Nodes() ([]testbedi.Core, error) { 129 | if tb.nodes != nil { 130 | return tb.nodes, nil 131 | } 132 | 133 | return tb.loadNodes() 134 | } 135 | 136 | func (tb *BasicTestbed) loadSpecs() ([]*NodeSpec, error) { 137 | specs, err := ReadNodeSpecs(tb.dir) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return specs, nil 143 | } 144 | 145 | func (tb *BasicTestbed) loadNodes() ([]testbedi.Core, error) { 146 | specs, err := tb.Specs() 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return NodesFromSpecs(specs) 152 | } 153 | 154 | func NodesFromSpecs(specs []*NodeSpec) ([]testbedi.Core, error) { 155 | var out []testbedi.Core 156 | for _, s := range specs { 157 | nd, err := s.Load() 158 | if err != nil { 159 | return nil, err 160 | } 161 | out = append(out, nd) 162 | } 163 | return out, nil 164 | } 165 | 166 | func ReadNodeSpecs(dir string) ([]*NodeSpec, error) { 167 | data, err := os.ReadFile(filepath.Join(dir, "nodespec.json")) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | var specs []*NodeSpec 173 | err = json.Unmarshal(data, &specs) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return specs, nil 179 | } 180 | 181 | func WriteNodeSpecs(dir string, specs []*NodeSpec) error { 182 | err := os.MkdirAll(dir, 0775) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | fi, err := os.Create(filepath.Join(dir, "nodespec.json")) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | defer fi.Close() 193 | return json.NewEncoder(fi).Encode(specs) 194 | } 195 | -------------------------------------------------------------------------------- /commands/attr.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strconv" 7 | 8 | cli "github.com/urfave/cli" 9 | 10 | "github.com/ipfs/iptb/testbed" 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | ) 13 | 14 | var AttrCmd = cli.Command{ 15 | Category: "ATTRIBUTES", 16 | Name: "attr", 17 | Usage: "get, set, list attributes", 18 | Subcommands: []cli.Command{ 19 | AttrSetCmd, 20 | AttrGetCmd, 21 | AttrListCmd, 22 | }, 23 | } 24 | 25 | var AttrSetCmd = cli.Command{ 26 | Name: "set", 27 | Usage: "set an attribute for a node", 28 | ArgsUsage: " ", 29 | Flags: []cli.Flag{ 30 | cli.BoolFlag{ 31 | Name: "save", 32 | Usage: "saves attribute value to nodespec", 33 | }, 34 | }, 35 | Action: func(c *cli.Context) error { 36 | flagRoot := c.GlobalString("IPTB_ROOT") 37 | flagTestbed := c.GlobalString("testbed") 38 | flagSave := c.Bool("save") 39 | 40 | if c.NArg() != 3 { 41 | return NewUsageError("set takes exactly 3 argument") 42 | } 43 | 44 | argNode := c.Args()[0] 45 | argAttr := c.Args()[1] 46 | argValue := c.Args()[2] 47 | 48 | i, err := strconv.Atoi(argNode) 49 | if err != nil { 50 | return fmt.Errorf("parse err: %s", err) 51 | } 52 | 53 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 54 | 55 | node, err := tb.Node(i) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | attrNode, ok := node.(testbedi.Attribute) 61 | if !ok { 62 | return fmt.Errorf("node does not implement attributes") 63 | } 64 | 65 | if err := attrNode.SetAttr(argAttr, argValue); err != nil { 66 | return err 67 | } 68 | 69 | if flagSave { 70 | specs, err := tb.Specs() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | specs[i].SetAttr(argAttr, argValue) 76 | 77 | if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil { 78 | return err 79 | } 80 | } 81 | 82 | return nil 83 | }, 84 | } 85 | 86 | var AttrGetCmd = cli.Command{ 87 | Name: "get", 88 | Usage: "get an attribute for a node", 89 | ArgsUsage: " ", 90 | Action: func(c *cli.Context) error { 91 | flagRoot := c.GlobalString("IPTB_ROOT") 92 | flagTestbed := c.GlobalString("testbed") 93 | 94 | if c.NArg() != 2 { 95 | return NewUsageError("get takes exactly 2 argument") 96 | } 97 | 98 | argNode := c.Args()[0] 99 | argAttr := c.Args()[1] 100 | 101 | i, err := strconv.Atoi(argNode) 102 | if err != nil { 103 | return fmt.Errorf("parse err: %s", err) 104 | } 105 | 106 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 107 | 108 | node, err := tb.Node(i) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | attrNode, ok := node.(testbedi.Attribute) 114 | if !ok { 115 | return fmt.Errorf("node does not implement attributes") 116 | } 117 | 118 | value, err := attrNode.Attr(argAttr) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | _, err = fmt.Fprintf(c.App.Writer, "%s\n", value) 124 | 125 | return err 126 | }, 127 | } 128 | 129 | var AttrListCmd = cli.Command{ 130 | Name: "list", 131 | Usage: "list attributes available for a node", 132 | ArgsUsage: "", 133 | Flags: []cli.Flag{ 134 | cli.StringFlag{ 135 | Name: "type", 136 | Usage: "look up attributes for node type", 137 | EnvVar: "IPTB_PLUGIN", 138 | }, 139 | }, 140 | Action: func(c *cli.Context) error { 141 | flagRoot := c.GlobalString("IPTB_ROOT") 142 | flagTestbed := c.GlobalString("testbed") 143 | flagType := c.String("type") 144 | 145 | if !c.Args().Present() && len(flagType) == 0 { 146 | return NewUsageError("specify a node, or a type") 147 | } 148 | 149 | if c.Args().Present() { 150 | i, err := strconv.Atoi(c.Args().First()) 151 | if err != nil { 152 | return fmt.Errorf("parse err: %s", err) 153 | } 154 | 155 | tb := testbed.NewTestbed(path.Join(flagRoot, "testbeds", flagTestbed)) 156 | 157 | spec, err := tb.Spec(i) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | flagType = spec.Type 163 | } 164 | 165 | plg, ok := testbed.GetPlugin(flagType) 166 | if !ok { 167 | return fmt.Errorf("unknown plugin %s", flagType) 168 | } 169 | 170 | attrList := plg.GetAttrList() 171 | for _, a := range attrList { 172 | desc, err := plg.GetAttrDesc(a) 173 | if err != nil { 174 | return fmt.Errorf("error getting attribute description: %s", err) 175 | } 176 | 177 | fmt.Fprintf(c.App.Writer, "\t%s: %s\n", a, desc) 178 | } 179 | 180 | return nil 181 | }, 182 | } 183 | -------------------------------------------------------------------------------- /testbed/interfaces/node.go: -------------------------------------------------------------------------------- 1 | package testbedi 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // NewNodeFunc constructs a node implementing the Core interface. It is provided 9 | // a path to an already created directory `dir`, as well as a map of attributes 10 | // which can be supplied to shape process execution. 11 | // Examples of attributes include: which binary to use, docker image, cpu/ram 12 | // limits, or any other information that may be required to property setup or 13 | // manage the node. 14 | type NewNodeFunc func(dir string, attrs map[string]string) (Core, error) 15 | 16 | // GetAttrListFunc returns a list of attribute names that can be queried from 17 | // the node. These attributes may include those can be set from the NewNodeFunc, 18 | // or additional attributes at may only be available after initialization. 19 | // Attributes returned should be queriable through the Attribute interface. 20 | // Examples include: api address, peerid, cpu/ram limits, jitter. 21 | type GetAttrListFunc func() []string 22 | 23 | // GetAttrDescFunc returns the description of the attribute `attr` 24 | type GetAttrDescFunc func(attr string) (string, error) 25 | 26 | type Libp2p interface { 27 | // PeerID returns the peer id 28 | PeerID() (string, error) 29 | // APIAddr returns the multiaddr for the api 30 | APIAddr() (string, error) 31 | // SwarmAddrs returns the swarm addrs for the node 32 | SwarmAddrs() ([]string, error) 33 | } 34 | 35 | type Config interface { 36 | // Config returns the configuration of the node 37 | Config() (interface{}, error) 38 | // WriteConfig writes the configuration of the node 39 | WriteConfig(interface{}) error 40 | } 41 | 42 | // Attributes are ways to shape process execution and additional information that alters the 43 | // environment the process executes in 44 | type Attribute interface { 45 | // Attr returns the value of attr 46 | Attr(attr string) (string, error) 47 | // SetAttr sets the attr to val 48 | SetAttr(attr string, val string) error 49 | // GetAttrList returns a list of attrs that can be retrieved 50 | GetAttrList() []string 51 | // GetAttrDesc returns the description of attr 52 | GetAttrDesc(attr string) (string, error) 53 | /* Example: 54 | 55 | * Network: 56 | - Bandwidth 57 | - Jitter 58 | - Latency 59 | - Packet_Loss 60 | 61 | * CPU 62 | - Limit 63 | 64 | * RAM 65 | - Limit 66 | 67 | */ 68 | } 69 | 70 | // Metrics are ways to gather information during process execution 71 | type Metric interface { 72 | // Events returns reader for events 73 | Events() (io.ReadCloser, error) 74 | // StderrReader returns reader of stderr for the node 75 | StderrReader() (io.ReadCloser, error) 76 | // StdoutReader returns reader of stdout for the node 77 | StdoutReader() (io.ReadCloser, error) 78 | 79 | // Heartbeat returns key values pairs of a defined set of metrics 80 | Heartbeat() (map[string]string, error) 81 | // Metric returns metric value at key 82 | Metric(key string) (string, error) 83 | // GetMetricList returns list of metrics 84 | GetMetricList() []string 85 | // GetMetricDesc returns description of metrics 86 | GetMetricDesc(key string) (string, error) 87 | /* Examples: 88 | 89 | * Filesystem: 90 | - device_name 91 | - swap 92 | - mount_point 93 | - total 94 | - pct_used 95 | 96 | * CPU 97 | - cores 98 | - iowait 99 | - pct_used 100 | 101 | * RAM 102 | - total 103 | - pct_used 104 | 105 | * Network 106 | - bwout 107 | - bwin 108 | - ping 109 | 110 | */ 111 | } 112 | 113 | // Core specifies the interface to a process controlled by iptb 114 | type Core interface { 115 | Libp2p 116 | // Allows a node to run any initialization it may require 117 | // Ex: Installing additional dependencies / setting up configuration 118 | Init(ctx context.Context, args ...string) (Output, error) 119 | // Starts the node, wait can be used to delay the return till the node is ready 120 | // to accept commands 121 | Start(ctx context.Context, wait bool, args ...string) (Output, error) 122 | // Stops the node 123 | Stop(ctx context.Context) error 124 | // Runs a command in the context of the node 125 | RunCmd(ctx context.Context, stdin io.Reader, args ...string) (Output, error) 126 | // Connect the node to another 127 | Connect(ctx context.Context, n Core) error 128 | // Starts a shell in the context of the node 129 | Shell(ctx context.Context, ns []Core) error 130 | 131 | // Dir returns the iptb directory assigned to the node 132 | Dir() string 133 | // Type returns a string that identifies the implementation 134 | // Examples localipfs, dockeripfs, etc. 135 | Type() string 136 | 137 | String() string 138 | } 139 | -------------------------------------------------------------------------------- /commands/utils.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | 11 | testbedi "github.com/ipfs/iptb/testbed/interfaces" 12 | cli "github.com/urfave/cli" 13 | ) 14 | 15 | // the flag terminator stops flag parsing, but it also swallowed if its the 16 | // first argument into a command / subcommand. To find it, we have to look 17 | // up to the parent command. 18 | // iptb run 0 -- ipfs id => c.Args: 0 -- ipfs id 19 | // iptb run -- ipfs id => c.Args: ipfs id 20 | func isTerminatorPresent(c *cli.Context) bool { 21 | argsParent := c.Parent().Args().Tail() 22 | argsSelf := c.Args() 23 | 24 | ls := len(argsSelf) 25 | lp := len(argsParent) 26 | 27 | term := lp - ls - 1 28 | 29 | if lp > ls && argsParent[term] == "--" { 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | func parseAttrSlice(attrsraw []string) map[string]string { 37 | attrs := make(map[string]string) 38 | for _, attr := range attrsraw { 39 | parts := strings.Split(attr, ",") 40 | 41 | if len(parts) == 1 { 42 | attrs[parts[0]] = "true" 43 | } else { 44 | attrs[parts[0]] = strings.Join(parts[1:], ",") 45 | } 46 | } 47 | 48 | return attrs 49 | } 50 | 51 | func parseCommand(args []string, terminator bool) (string, []string) { 52 | if terminator { 53 | return "", args 54 | } 55 | 56 | if len(args) == 0 { 57 | return "", []string{} 58 | } 59 | 60 | if len(args) == 1 { 61 | return args[0], []string{} 62 | } 63 | 64 | if args[0] == "--" { 65 | return "", args[1:] 66 | } 67 | 68 | arguments := args[1:] 69 | 70 | if arguments[0] == "--" { 71 | arguments = arguments[1:] 72 | } 73 | 74 | return args[0], arguments 75 | } 76 | 77 | func parseRange(s string) ([]int, error) { 78 | if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { 79 | ranges := strings.Split(s[1:len(s)-1], ",") 80 | var out []int 81 | for _, r := range ranges { 82 | rng, err := expandDashRange(r) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | out = append(out, rng...) 88 | } 89 | return out, nil 90 | } 91 | i, err := strconv.Atoi(s) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return []int{i}, nil 97 | } 98 | 99 | func expandDashRange(s string) ([]int, error) { 100 | parts := strings.Split(s, "-") 101 | if len(parts) == 1 { 102 | i, err := strconv.Atoi(s) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return []int{i}, nil 107 | } 108 | low, err := strconv.Atoi(parts[0]) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | hi, err := strconv.Atoi(parts[1]) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | var out []int 119 | for i := low; i <= hi; i++ { 120 | out = append(out, i) 121 | } 122 | return out, nil 123 | } 124 | 125 | type Result struct { 126 | Node int 127 | Output testbedi.Output 128 | Error error 129 | } 130 | 131 | type outputFunc func(testbedi.Core) (testbedi.Output, error) 132 | 133 | func mapListWithOutput(ranges [][]int, nodes []testbedi.Core, fns []outputFunc) ([]Result, error) { 134 | var wg sync.WaitGroup 135 | var lk sync.Mutex 136 | var errs []error 137 | 138 | total := 0 139 | offsets := make([]int, len(ranges)) 140 | for i, list := range ranges { 141 | offsets[i] = total 142 | total += len(list) 143 | } 144 | results := make([]Result, total) 145 | 146 | for i, list := range ranges { 147 | wg.Add(1) 148 | go func(i int, list []int) { 149 | defer wg.Done() 150 | results_i, err := mapWithOutput(list, nodes, fns[i]) 151 | 152 | lk.Lock() 153 | defer lk.Unlock() 154 | 155 | if err != nil { 156 | errs = append(errs, err) 157 | } 158 | for j, result := range results_i { 159 | results[offsets[i]+j] = result 160 | } 161 | }(i, list) 162 | wg.Wait() 163 | } 164 | 165 | if len(errs) != 0 { 166 | return results, cli.NewMultiError(errs...) 167 | } 168 | 169 | return results, nil 170 | } 171 | 172 | func mapWithOutput(list []int, nodes []testbedi.Core, fn outputFunc) ([]Result, error) { 173 | var wg sync.WaitGroup 174 | var lk sync.Mutex 175 | results := make([]Result, len(list)) 176 | 177 | if err := validRange(list, len(nodes)); err != nil { 178 | return results, err 179 | } 180 | 181 | for i, n := range list { 182 | wg.Add(1) 183 | go func(i, n int, node testbedi.Core) { 184 | defer wg.Done() 185 | out, err := fn(node) 186 | if err != nil { 187 | err = fmt.Errorf("node[%d]: %w", n, err) 188 | } 189 | 190 | lk.Lock() 191 | defer lk.Unlock() 192 | 193 | results[i] = Result{ 194 | Node: n, 195 | Output: out, 196 | Error: err, 197 | } 198 | }(i, n, nodes[n]) 199 | } 200 | 201 | wg.Wait() 202 | 203 | return results, nil 204 | 205 | } 206 | 207 | func validRange(list []int, total int) error { 208 | max := 0 209 | for _, n := range list { 210 | if max < n { 211 | max = n 212 | } 213 | } 214 | 215 | if max >= total { 216 | return fmt.Errorf("node range contains value (%d) outside of valid range [0-%d]", max, total-1) 217 | } 218 | 219 | return nil 220 | } 221 | 222 | func buildReport(results []Result, quiet bool) error { 223 | var errs []error 224 | 225 | for _, rs := range results { 226 | if rs.Error != nil { 227 | errs = append(errs, rs.Error) 228 | } 229 | 230 | if quiet { 231 | io.Copy(os.Stdout, rs.Output.Stdout()) 232 | io.Copy(os.Stdout, rs.Output.Stderr()) 233 | continue 234 | } 235 | 236 | if rs.Output != nil { 237 | fmt.Printf("node[%d] exit %d\n", rs.Node, rs.Output.ExitCode()) 238 | if rs.Output.Error() != nil { 239 | fmt.Printf("%s", rs.Output.Error()) 240 | } 241 | 242 | fmt.Println() 243 | 244 | io.Copy(os.Stdout, rs.Output.Stdout()) 245 | io.Copy(os.Stdout, rs.Output.Stderr()) 246 | 247 | fmt.Println() 248 | } 249 | 250 | } 251 | 252 | if len(errs) != 0 { 253 | return cli.NewMultiError(errs...) 254 | } 255 | 256 | return nil 257 | } 258 | --------------------------------------------------------------------------------