wizard.go 8.75 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Copyright 2017 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math/big"
24
	"net"
25
	"net/url"
26 27 28 29 30
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
31
	"sync"
32 33

	"github.com/ethereum/go-ethereum/common"
34
	"github.com/ethereum/go-ethereum/console/prompt"
35 36
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/log"
37
	"github.com/peterh/liner"
38 39 40 41 42 43
	"golang.org/x/crypto/ssh/terminal"
)

// config contains all the configurations needed by puppeth that should be saved
// between sessions.
type config struct {
44
	path      string   // File containing the configuration values
45
	bootnodes []string // Bootnodes to always connect to by all nodes
46
	ethstats  string   // Ethstats settings to cache for node deploys
47

48
	Genesis *core.Genesis     `json:"genesis,omitempty"` // Genesis block to cache for node deploys
49 50 51 52 53 54 55 56 57 58 59 60
	Servers map[string][]byte `json:"servers,omitempty"`
}

// servers retrieves an alphabetically sorted list of servers.
func (c config) servers() []string {
	servers := make([]string, 0, len(c.Servers))
	for server := range c.Servers {
		servers = append(servers, server)
	}
	sort.Strings(servers)

	return servers
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
}

// flush dumps the contents of config to disk.
func (c config) flush() {
	os.MkdirAll(filepath.Dir(c.path), 0755)

	out, _ := json.MarshalIndent(c, "", "  ")
	if err := ioutil.WriteFile(c.path, out, 0644); err != nil {
		log.Warn("Failed to save puppeth configs", "file", c.path, "err", err)
	}
}

type wizard struct {
	network string // Network name to manage
	conf    config // Configurations from previous runs

	servers  map[string]*sshClient // SSH connections to servers to administer
	services map[string][]string   // Ethereum services known to be running on servers

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
	lock sync.Mutex // Lock to protect configs during concurrent service discovery
}

// prompts the user for input with the given prompt string.  Returns when a value is entered.
// Causes the wizard to exit if ctrl-d is pressed
func promptInput(p string) string {
	for {
		text, err := prompt.Stdin.PromptInput(p)
		if err != nil {
			if err != liner.ErrPromptAborted {
				log.Crit("Failed to read user input", "err", err)
			}
		} else {
			return text
		}
	}
96 97 98 99
}

// read reads a single line from stdin, trimming if from spaces.
func (w *wizard) read() string {
100
	text := promptInput("> ")
101 102 103 104 105 106 107
	return strings.TrimSpace(text)
}

// readString reads a single line from stdin, trimming if from spaces, enforcing
// non-emptyness.
func (w *wizard) readString() string {
	for {
108
		text := promptInput("> ")
109 110 111 112 113 114
		if text = strings.TrimSpace(text); text != "" {
			return text
		}
	}
}

115 116 117
// readDefaultString reads a single line from stdin, trimming if from spaces. If
// an empty line is entered, the default value is returned.
func (w *wizard) readDefaultString(def string) string {
118
	text := promptInput("> ")
119 120 121 122 123 124 125 126 127 128
	if text = strings.TrimSpace(text); text != "" {
		return text
	}
	return def
}

// readDefaultYesNo reads a single line from stdin, trimming if from spaces and
// interpreting it as a 'yes' or a 'no'. If an empty line is entered, the default
// value is returned.
func (w *wizard) readDefaultYesNo(def bool) bool {
129
	for {
130
		text := promptInput("> ")
131 132 133
		if text = strings.ToLower(strings.TrimSpace(text)); text == "" {
			return def
		}
134 135 136 137 138 139
		if text == "y" || text == "yes" {
			return true
		}
		if text == "n" || text == "no" {
			return false
		}
140
		log.Error("Invalid input, expected 'y', 'yes', 'n', 'no' or empty")
141 142 143
	}
}

144 145 146 147
// readURL reads a single line from stdin, trimming if from spaces and trying to
// interpret it as a URL (http, https or file).
func (w *wizard) readURL() *url.URL {
	for {
148
		text := promptInput("> ")
149 150 151 152 153 154
		uri, err := url.Parse(strings.TrimSpace(text))
		if err != nil {
			log.Error("Invalid input, expected URL", "err", err)
			continue
		}
		return uri
155
	}
156 157 158 159 160 161
}

// readInt reads a single line from stdin, trimming if from spaces, enforcing it
// to parse into an integer.
func (w *wizard) readInt() int {
	for {
162
		text := promptInput("> ")
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
		if text = strings.TrimSpace(text); text == "" {
			continue
		}
		val, err := strconv.Atoi(strings.TrimSpace(text))
		if err != nil {
			log.Error("Invalid input, expected integer", "err", err)
			continue
		}
		return val
	}
}

// readDefaultInt reads a single line from stdin, trimming if from spaces, enforcing
// it to parse into an integer. If an empty line is entered, the default value is
// returned.
func (w *wizard) readDefaultInt(def int) int {
	for {
180
		text := promptInput("> ")
181 182 183 184 185 186 187 188 189 190 191 192
		if text = strings.TrimSpace(text); text == "" {
			return def
		}
		val, err := strconv.Atoi(strings.TrimSpace(text))
		if err != nil {
			log.Error("Invalid input, expected integer", "err", err)
			continue
		}
		return val
	}
}

193 194 195 196 197
// readDefaultBigInt reads a single line from stdin, trimming if from spaces,
// enforcing it to parse into a big integer. If an empty line is entered, the
// default value is returned.
func (w *wizard) readDefaultBigInt(def *big.Int) *big.Int {
	for {
198
		text := promptInput("> ")
199 200 201 202 203 204 205 206 207 208 209 210
		if text = strings.TrimSpace(text); text == "" {
			return def
		}
		val, ok := new(big.Int).SetString(text, 0)
		if !ok {
			log.Error("Invalid input, expected big integer")
			continue
		}
		return val
	}
}

211 212 213 214
// readDefaultFloat reads a single line from stdin, trimming if from spaces, enforcing
// it to parse into a float. If an empty line is entered, the default value is returned.
func (w *wizard) readDefaultFloat(def float64) float64 {
	for {
215
		text := promptInput("> ")
216 217 218 219 220 221 222 223 224 225 226 227
		if text = strings.TrimSpace(text); text == "" {
			return def
		}
		val, err := strconv.ParseFloat(strings.TrimSpace(text), 64)
		if err != nil {
			log.Error("Invalid input, expected float", "err", err)
			continue
		}
		return val
	}
}

228 229 230
// readPassword reads a single line from stdin, trimming it from the trailing new
// line and returns it. The input will not be echoed.
func (w *wizard) readPassword() string {
231
	fmt.Printf("> ")
232
	text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
233 234
	if err != nil {
		log.Crit("Failed to read password", "err", err)
235
	}
236 237
	fmt.Println()
	return string(text)
238 239 240 241 242 243
}

// readAddress reads a single line from stdin, trimming if from spaces and converts
// it to an Ethereum address.
func (w *wizard) readAddress() *common.Address {
	for {
244
		text := promptInput("> 0x")
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
		if text = strings.TrimSpace(text); text == "" {
			return nil
		}
		// Make sure it looks ok and return it if so
		if len(text) != 40 {
			log.Error("Invalid address length, please retry")
			continue
		}
		bigaddr, _ := new(big.Int).SetString(text, 16)
		address := common.BigToAddress(bigaddr)
		return &address
	}
}

// readDefaultAddress reads a single line from stdin, trimming if from spaces and
// converts it to an Ethereum address. If an empty line is entered, the default
// value is returned.
func (w *wizard) readDefaultAddress(def common.Address) common.Address {
	for {
		// Read the address from the user
265
		text := promptInput("> 0x")
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
		if text = strings.TrimSpace(text); text == "" {
			return def
		}
		// Make sure it looks ok and return it if so
		if len(text) != 40 {
			log.Error("Invalid address length, please retry")
			continue
		}
		bigaddr, _ := new(big.Int).SetString(text, 16)
		return common.BigToAddress(bigaddr)
	}
}

// readJSON reads a raw JSON message and returns it.
func (w *wizard) readJSON() string {
	var blob json.RawMessage

	for {
284 285 286
		text := promptInput("> ")
		reader := strings.NewReader(text)
		if err := json.NewDecoder(reader).Decode(&blob); err != nil {
287 288 289 290 291 292
			log.Error("Invalid JSON, please try again", "err", err)
			continue
		}
		return string(blob)
	}
}
293 294

// readIPAddress reads a single line from stdin, trimming if from spaces and
295 296 297 298
// returning it if it's convertible to an IP address. The reason for keeping
// the user input format instead of returning a Go net.IP is to match with
// weird formats used by ethstats, which compares IPs textually, not by value.
func (w *wizard) readIPAddress() string {
299 300 301
	for {
		// Read the IP address from the user
		fmt.Printf("> ")
302
		text := promptInput("> ")
303
		if text = strings.TrimSpace(text); text == "" {
304
			return ""
305 306
		}
		// Make sure it looks ok and return it if so
307
		if ip := net.ParseIP(text); ip == nil {
308 309 310
			log.Error("Invalid IP address, please retry")
			continue
		}
311
		return text
312 313
	}
}