package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "os" "path" "strconv" "strings" "github.com/tendermint/netmon/handlers" "github.com/tendermint/netmon/types" "github.com/codegangsta/cli" . "github.com/tendermint/go-common" "github.com/tendermint/go-logger" pcm "github.com/tendermint/go-process" "github.com/tendermint/go-rpc/server" "github.com/tendermint/go-wire" ) func init() { logger.SetLogLevel("debug") } func main() { app := cli.NewApp() app.Name = "netmon" app.Usage = "netmon [command] [args...]" app.Commands = []cli.Command{ { Name: "config", Usage: "Create a config from a mintnet testnet", ArgsUsage: "[chainID] [prefix] [N]", Action: func(c *cli.Context) { cmdConfig(c) }, }, { Name: "chains-and-vals", Usage: "Add a chain or validator set to the main config file", ArgsUsage: "", Action: func(c *cli.Context) { cmdChainsAndVals(c) }, Subcommands: []cli.Command{ { Name: "chain", Usage: "Add a chain to the main config file", ArgsUsage: "[configFile] [chainBaseDir]", Action: func(c *cli.Context) { cmdAddChain(c) }, }, { Name: "val", Usage: "Add a validator set to the main config file", ArgsUsage: "[configFile] [valsetBaseDir]", Action: func(c *cli.Context) { cmdAddValSet(c) }, }, }, }, { Name: "monitor", Usage: "Monitor a chain", ArgsUsage: "[config file]", Action: func(c *cli.Context) { cmdMonitor(c) }, }, { Name: "bench", Usage: "Benchmark a chain's tx throughput and latency", ArgsUsage: "[config file] [results dir] [n txs] -- [command to fire n txs]", Action: func(c *cli.Context) { cmdBench(c) }, Flags: []cli.Flag{ cli.IntFlag{ Name: "n_txs", Value: 0, Usage: "run benchmark until this many txs have been committed", }, cli.IntFlag{ Name: "n_blocks", Value: 0, Usage: "run benchmark until this many blocks have been committed", }, }, }, } app.Run(os.Args) } func cmdChainsAndVals(c *cli.Context) { cli.ShowAppHelp(c) } func cmdAddChain(c *cli.Context) { args := c.Args() if len(args) != 2 { Exit("add chain expectes 2 arg") } cfgFile, chainDir := args[0], args[1] // load major config chainsAndVals := new(ChainsAndValidators) if err := ReadJSONFile(chainsAndVals, cfgFile); err != nil { Exit(err.Error()) } // load new chain chainCfg_ := new(BlockchainConfig) if err := ReadJSONFile(chainCfg_, path.Join(chainDir, "chain_config.json")); err != nil { Exit(err.Error()) } chainCfg := convertMintnetBlockchain(chainCfg_) // append new chain chainsAndVals.Blockchains = append(chainsAndVals.Blockchains, chainCfg) // write major config b := wire.JSONBytes(chainsAndVals) if err := ioutil.WriteFile(cfgFile, b, 0600); err != nil { Exit(err.Error()) } } func ReadJSONFile(o interface{}, filename string) error { b, err := ioutil.ReadFile(filename) if err != nil { return err } wire.ReadJSON(o, b, &err) if err != nil { return err } return nil } func cmdAddValSet(c *cli.Context) { args := c.Args() if len(args) != 2 { Exit("add chain expectes 2 arg") } cfgFile, valSetDir := args[0], args[1] // load major config chainsAndVals := new(ChainsAndValidators) if err := ReadJSONFile(chainsAndVals, cfgFile); err != nil { Exit(err.Error()) } // load new validator set valSet := new(types.ValidatorSet) if err := ReadJSONFile(valSet, path.Join(valSetDir, "validator_set.json")); err != nil { Exit(err.Error()) } // append new validator set chainsAndVals.ValidatorSets = append(chainsAndVals.ValidatorSets, valSet) // write major config to file b := wire.JSONBytes(chainsAndVals) if err := ioutil.WriteFile(cfgFile, b, 0600); err != nil { Exit(err.Error()) } } func cmdMonitor(c *cli.Context) { args := c.Args() if len(args) != 1 { Exit("monitor expectes 1 arg") } chainsAndValsFile := args[0] chainsAndVals, err := LoadChainsAndValsFromFile(chainsAndValsFile) if err != nil { Exit(err.Error()) } network := registerNetwork(chainsAndVals) startRPC(network) TrapSignal(func() { network.Stop() }) } func cmdBench(c *cli.Context) { args := c.Args() if len(args) < 2 { Exit("bench expects at least 2 args") } chainsAndValsFile := args[0] resultsDir := args[1] // extra args are a program to run locally if len(args) > 2 { args = args[2:] } else { args = args[:0] } chainsAndVals, err := LoadChainsAndValsFromFile(chainsAndValsFile) if err != nil { Exit(err.Error()) } network := registerNetwork(chainsAndVals) startRPC(network) // benchmark txs done := make(chan *types.BenchmarkResults) // we should only have one chain for a benchmark run chAndValIDs, _ := network.Status() chain, _ := network.GetChain(chAndValIDs.ChainIDs[0]) // setup benchresults struct and fire txs if nTxs := c.Int("n_txs"); nTxs != 0 { chain.Status.BenchmarkTxs(done, nTxs, args) } else if nBlocks := c.Int("n_blocks"); nBlocks != 0 { chain.Status.BenchmarkBlocks(done, nBlocks, args) } else { Exit("Must specify one of n_txs or n_blocks") } results := <-done b, err := json.Marshal(results) if err != nil { Exit(err.Error()) } fmt.Println(string(b)) if err := ioutil.WriteFile(path.Join(resultsDir, "netmon.log"), b, 0600); err != nil { Exit(err.Error()) } finalResults := fmt.Sprintf("%f,%f\n", results.MeanLatency, results.MeanThroughput) if err := ioutil.WriteFile(path.Join(resultsDir, "final_results"), []byte(finalResults), 0600); err != nil { Exit(err.Error()) } } func registerNetwork(chainsAndVals *ChainsAndValidators) *handlers.TendermintNetwork { // the main object that watches for changes and serves the rpc requests network := handlers.NewTendermintNetwork() for _, valSetCfg := range chainsAndVals.ValidatorSets { // Register validator set _, err := network.RegisterValidatorSet(valSetCfg) if err != nil { Exit("Register validator set error: " + err.Error()) } } for _, chainCfg := range chainsAndVals.Blockchains { // Register blockchain _, err := network.RegisterChain(chainCfg) if err != nil { Exit(Fmt("Register chain error for chain %s: %v", chainCfg.ID, err)) } } return network } func startRPC(network *handlers.TendermintNetwork) { // the routes are functions on the network object routes := handlers.Routes(network) // serve http and ws mux := http.NewServeMux() wm := rpcserver.NewWebsocketManager(routes, nil) // TODO: evsw mux.HandleFunc("/websocket", wm.WebsocketHandler) rpcserver.RegisterRPCFuncs(mux, routes) if _, err := rpcserver.StartHTTPServer("0.0.0.0:46670", mux); err != nil { Exit(err.Error()) } } func cmdConfig(c *cli.Context) { args := c.Args() if len(args) != 3 { Exit("config expects 3 args") } id, prefix := args[0], args[1] n, err := strconv.Atoi(args[2]) if err != nil { Exit(err.Error()) } chain, err := ConfigFromMachines(id, prefix, n) if err != nil { Exit(err.Error()) } b, err := json.Marshal(chain) if err != nil { Exit(err.Error()) } fmt.Println(string(b)) } func ConfigFromMachines(chainID, prefix string, N int) (*types.BlockchainConfig, error) { chain := &types.BlockchainConfig{ ID: chainID, Validators: make([]*types.ValidatorState, N), } for i := 0; i < N; i++ { id := fmt.Sprintf("%s%d", prefix, i+1) ip, success := runProcessGetResult(id+"-ip", "docker-machine", []string{"ip", id}) if !success { return nil, fmt.Errorf(ip) } val := &types.Validator{ ID: id, // TODO: pubkey } chainVal := &types.ValidatorState{ Config: &types.ValidatorConfig{ Validator: val, RPCAddr: fmt.Sprintf("%s:%d", strings.Trim(ip, "\n"), 46657), Index: i, }, } chain.Validators[i] = chainVal } return chain, nil } func runProcessGetResult(label string, command string, args []string) (string, bool) { outFile := NewBufferCloser(nil) fmt.Println(Green(command), Green(args)) proc, err := pcm.StartProcess(label, command, args, nil, outFile) if err != nil { return "", false } <-proc.WaitCh if proc.ExitState.Success() { fmt.Println(Blue(string(outFile.Bytes()))) return string(outFile.Bytes()), true } else { // Error! fmt.Println(Red(string(outFile.Bytes()))) return string(outFile.Bytes()), false } } //---------------------------------------------------------------------- type ChainsAndValidators struct { ValidatorSets []*types.ValidatorSet `json:"validator_sets"` Blockchains []*types.BlockchainConfig `json:"blockchains"` } func LoadChainsAndValsFromFile(configFile string) (*ChainsAndValidators, error) { b, err := ioutil.ReadFile(configFile) if err != nil { return nil, err } chainsAndVals_ := new(ChainsAndValidators) wire.ReadJSON(chainsAndVals_, b, &err) if err != nil { return nil, err } return chainsAndVals_, nil } // because types are duplicated in mintnet type BlockchainConfig struct { ID string `json:"id"` ValSetID string `json:"val_set_id"` Validators []*types.ValidatorConfig `json:"validators"` } func convertMintnetBlockchain(b *BlockchainConfig) *types.BlockchainConfig { vals := make([]*types.ValidatorState, len(b.Validators)) for j, v := range b.Validators { vals[j] = new(types.ValidatorState) vals[j].Config = v } return &types.BlockchainConfig{ ID: b.ID, ValSetID: b.ValSetID, Validators: vals, } }