aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/puppeth/wizard.go
blob: 92d7962a04ee33ad35968b877d26d5cd67cf8516 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// 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 (
    "bufio"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "math/big"
    "os"
    "path/filepath"
    "sort"
    "strconv"
    "strings"
    "syscall"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core"
    "github.com/ethereum/go-ethereum/log"
    "golang.org/x/crypto/ssh/terminal"
)

// config contains all the configurations needed by puppeth that should be saved
// between sessions.
type config struct {
    path      string        // File containing the configuration values
    genesis   *core.Genesis // Genesis block to cache for node deploys
    bootFull  []string      // Bootnodes to always connect to by full nodes
    bootLight []string      // Bootnodes to always connect to by light nodes
    ethstats  string        // Ethstats settings to cache for node deploys

    Servers []string `json:"servers,omitempty"`
}

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

    sort.Strings(c.Servers)
    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

    in *bufio.Reader // Wrapper around stdin to allow reading user input
}

// read reads a single line from stdin, trimming if from spaces.
func (w *wizard) read() string {
    fmt.Printf("> ")
    text, err := w.in.ReadString('\n')
    if err != nil {
        log.Crit("Failed to read user input", "err", err)
    }
    return strings.TrimSpace(text)
}

// readString reads a single line from stdin, trimming if from spaces, enforcing
// non-emptyness.
func (w *wizard) readString() string {
    for {
        fmt.Printf("> ")
        text, err := w.in.ReadString('\n')
        if err != nil {
            log.Crit("Failed to read user input", "err", err)
        }
        if text = strings.TrimSpace(text); text != "" {
            return text
        }
    }
}

// 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 {
    for {
        fmt.Printf("> ")
        text, err := w.in.ReadString('\n')
        if err != nil {
            log.Crit("Failed to read user input", "err", err)
        }
        if text = strings.TrimSpace(text); text != "" {
            return text
        }
        return def
    }
}

// readInt reads a single line from stdin, trimming if from spaces, enforcing it
// to parse into an integer.
func (w *wizard) readInt() int {
    for {
        fmt.Printf("> ")
        text, err := w.in.ReadString('\n')
        if err != nil {
            log.Crit("Failed to read user input", "err", err)
        }
        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 {
        fmt.Printf("> ")
        text, err := w.in.ReadString('\n')
        if err != nil {
            log.Crit("Failed to read user input", "err", err)
        }
        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
    }
}

// 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 {
    for {
        fmt.Printf("> ")
        text, err := terminal.ReadPassword(int(syscall.Stdin))
        if err != nil {
            log.Crit("Failed to read password", "err", err)
        }
        fmt.Println()
        return string(text)
    }
}

// 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 {
        // Read the address from the user
        fmt.Printf("> 0x")
        text, err := w.in.ReadString('\n')
        if err != nil {
            log.Crit("Failed to read user input", "err", err)
        }
        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
        fmt.Printf("> 0x")
        text, err := w.in.ReadString('\n')
        if err != nil {
            log.Crit("Failed to read user input", "err", err)
        }
        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 {
        fmt.Printf("> ")
        if err := json.NewDecoder(w.in).Decode(&blob); err != nil {
            log.Error("Invalid JSON, please try again", "err", err)
            continue
        }
        return string(blob)
    }
}