cmd/puppeth: your Ethereum private network manager (#13854)
This commit is contained in:
committed by
Felix Lange
parent
18bbe12425
commit
706a1e552c
152
cmd/puppeth/module.go
Normal file
152
cmd/puppeth/module.go
Normal file
@ -0,0 +1,152 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrServiceUnknown is returned when a service container doesn't exist.
|
||||
ErrServiceUnknown = errors.New("service unknown")
|
||||
|
||||
// ErrServiceOffline is returned when a service container exists, but it is not
|
||||
// running.
|
||||
ErrServiceOffline = errors.New("service offline")
|
||||
|
||||
// ErrServiceUnreachable is returned when a service container is running, but
|
||||
// seems to not respond to communication attempts.
|
||||
ErrServiceUnreachable = errors.New("service unreachable")
|
||||
|
||||
// ErrNotExposed is returned if a web-service doesn't have an exposed port, nor
|
||||
// a reverse-proxy in front of it to forward requests.
|
||||
ErrNotExposed = errors.New("service not exposed, nor proxied")
|
||||
)
|
||||
|
||||
// containerInfos is a heavily reduced version of the huge inspection dataset
|
||||
// returned from docker inspect, parsed into a form easily usable by puppeth.
|
||||
type containerInfos struct {
|
||||
running bool // Flag whether the container is running currently
|
||||
envvars map[string]string // Collection of environmental variables set on the container
|
||||
portmap map[string]int // Port mapping from internal port/proto combos to host binds
|
||||
volumes map[string]string // Volume mount points from container to host directories
|
||||
}
|
||||
|
||||
// inspectContainer runs docker inspect against a running container
|
||||
func inspectContainer(client *sshClient, container string) (*containerInfos, error) {
|
||||
// Check whether there's a container running for the service
|
||||
out, err := client.Run(fmt.Sprintf("docker inspect %s", container))
|
||||
if err != nil {
|
||||
return nil, ErrServiceUnknown
|
||||
}
|
||||
// If yes, extract various configuration options
|
||||
type inspection struct {
|
||||
State struct {
|
||||
Running bool
|
||||
}
|
||||
Mounts []struct {
|
||||
Source string
|
||||
Destination string
|
||||
}
|
||||
Config struct {
|
||||
Env []string
|
||||
}
|
||||
HostConfig struct {
|
||||
PortBindings map[string][]map[string]string
|
||||
}
|
||||
}
|
||||
var inspects []inspection
|
||||
if err = json.Unmarshal(out, &inspects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inspect := inspects[0]
|
||||
|
||||
// Infos retrieved, parse the above into something meaningful
|
||||
infos := &containerInfos{
|
||||
running: inspect.State.Running,
|
||||
envvars: make(map[string]string),
|
||||
portmap: make(map[string]int),
|
||||
volumes: make(map[string]string),
|
||||
}
|
||||
for _, envvar := range inspect.Config.Env {
|
||||
if parts := strings.Split(envvar, "="); len(parts) == 2 {
|
||||
infos.envvars[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
for portname, details := range inspect.HostConfig.PortBindings {
|
||||
if len(details) > 0 {
|
||||
port, _ := strconv.Atoi(details[0]["HostPort"])
|
||||
infos.portmap[portname] = port
|
||||
}
|
||||
}
|
||||
for _, mount := range inspect.Mounts {
|
||||
infos.volumes[mount.Destination] = mount.Source
|
||||
}
|
||||
return infos, err
|
||||
}
|
||||
|
||||
// tearDown connects to a remote machine via SSH and terminates docker containers
|
||||
// running with the specified name in the specified network.
|
||||
func tearDown(client *sshClient, network string, service string, purge bool) ([]byte, error) {
|
||||
// Tear down the running (or paused) container
|
||||
out, err := client.Run(fmt.Sprintf("docker rm -f %s_%s_1", network, service))
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
// If requested, purge the associated docker image too
|
||||
if purge {
|
||||
return client.Run(fmt.Sprintf("docker rmi %s/%s", network, service))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// resolve retrieves the hostname a service is running on either by returning the
|
||||
// actual server name and port, or preferably an nginx virtual host if available.
|
||||
func resolve(client *sshClient, network string, service string, port int) (string, error) {
|
||||
// Inspect the service to get various configurations from it
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, service))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !infos.running {
|
||||
return "", ErrServiceOffline
|
||||
}
|
||||
// Container online, extract any environmental variables
|
||||
if vhost := infos.envvars["VIRTUAL_HOST"]; vhost != "" {
|
||||
return vhost, nil
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", client.server, port), nil
|
||||
}
|
||||
|
||||
// checkPort tries to connect to a remote host on a given
|
||||
func checkPort(host string, port int) error {
|
||||
log.Trace("Verifying remote TCP connectivity", "server", host, "port", port)
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
537
cmd/puppeth/module_dashboard.go
Normal file
537
cmd/puppeth/module_dashboard.go
Normal file
File diff suppressed because one or more lines are too long
159
cmd/puppeth/module_ethstats.go
Normal file
159
cmd/puppeth/module_ethstats.go
Normal file
@ -0,0 +1,159 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend
|
||||
// and associated monitoring site.
|
||||
var ethstatsDockerfile = `
|
||||
FROM mhart/alpine-node:latest
|
||||
|
||||
RUN \
|
||||
apk add --update git && \
|
||||
git clone --depth=1 https://github.com/karalabe/eth-netstats && \
|
||||
apk del git && rm -rf /var/cache/apk/* && \
|
||||
\
|
||||
cd /eth-netstats && npm install && npm install -g grunt-cli && grunt
|
||||
|
||||
WORKDIR /eth-netstats
|
||||
EXPOSE 3000
|
||||
|
||||
RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: []};' > lib/utils/config.js
|
||||
|
||||
CMD ["npm", "start"]
|
||||
`
|
||||
|
||||
// ethstatsComposefile is the docker-compose.yml file required to deploy and
|
||||
// maintain an ethstats monitoring site.
|
||||
var ethstatsComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
ethstats:
|
||||
build: .
|
||||
image: {{.Network}}/ethstats{{if not .VHost}}
|
||||
ports:
|
||||
- "{{.Port}}:3000"{{end}}
|
||||
environment:
|
||||
- WS_SECRET={{.Secret}}{{if .VHost}}
|
||||
- VIRTUAL_HOST={{.VHost}}{{end}}
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployEthstats deploys a new ethstats container to a remote machine via SSH,
|
||||
// docker and docker-compose. If an instance with the specified network name
|
||||
// already exists there, it will be overwritten!
|
||||
func deployEthstats(client *sshClient, network string, port int, secret string, vhost string, trusted []string) ([]byte, error) {
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
for i, address := range trusted {
|
||||
trusted[i] = fmt.Sprintf("\"%s\"", address)
|
||||
}
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(ethstatsDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"Trusted": strings.Join(trusted, ", "),
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(ethstatsComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"Port": port,
|
||||
"Secret": secret,
|
||||
"VHost": vhost,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
// Upload the deployment files to the remote server (and clean up afterwards)
|
||||
if out, err := client.Upload(files); err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer client.Run("rm -rf " + workdir)
|
||||
|
||||
// Build and deploy the ethstats service
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
|
||||
}
|
||||
|
||||
// ethstatsInfos is returned from an ethstats status check to allow reporting
|
||||
// various configuration parameters.
|
||||
type ethstatsInfos struct {
|
||||
host string
|
||||
port int
|
||||
secret string
|
||||
config string
|
||||
}
|
||||
|
||||
// String implements the stringer interface.
|
||||
func (info *ethstatsInfos) String() string {
|
||||
return fmt.Sprintf("host=%s, port=%d, secret=%s", info.host, info.port, info.secret)
|
||||
}
|
||||
|
||||
// checkEthstats does a health-check against an ethstats server to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkEthstats(client *sshClient, network string) (*ethstatsInfos, error) {
|
||||
// Inspect a possible ethstats container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_ethstats_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve the port from the host, or the reverse proxy
|
||||
port := infos.portmap["3000/tcp"]
|
||||
if port == 0 {
|
||||
if proxy, _ := checkNginx(client, network); proxy != nil {
|
||||
port = proxy.port
|
||||
}
|
||||
}
|
||||
if port == 0 {
|
||||
return nil, ErrNotExposed
|
||||
}
|
||||
// Resolve the host from the reverse-proxy and configure the connection string
|
||||
host := infos.envvars["VIRTUAL_HOST"]
|
||||
if host == "" {
|
||||
host = client.server
|
||||
}
|
||||
secret := infos.envvars["WS_SECRET"]
|
||||
config := fmt.Sprintf("%s@%s", secret, host)
|
||||
if port != 80 && port != 443 {
|
||||
config += fmt.Sprintf(":%d", port)
|
||||
}
|
||||
// Run a sanity check to see if the port is reachable
|
||||
if err = checkPort(host, port); err != nil {
|
||||
log.Warn("Ethstats service seems unreachable", "server", host, "port", port, "err", err)
|
||||
}
|
||||
// Container available, assemble and return the useful infos
|
||||
return ðstatsInfos{
|
||||
host: host,
|
||||
port: port,
|
||||
secret: secret,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
210
cmd/puppeth/module_faucet.go
Normal file
210
cmd/puppeth/module_faucet.go
Normal file
@ -0,0 +1,210 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// faucetDockerfile is the Dockerfile required to build an faucet container to
|
||||
// grant crypto tokens based on GitHub authentications.
|
||||
var faucetDockerfile = `
|
||||
FROM alpine:latest
|
||||
|
||||
RUN mkdir /go
|
||||
ENV GOPATH /go
|
||||
|
||||
RUN \
|
||||
apk add --update git go make gcc musl-dev ca-certificates linux-headers && \
|
||||
mkdir -p $GOPATH/src/github.com/ethereum && \
|
||||
(cd $GOPATH/src/github.com/ethereum && git clone --depth=1 https://github.com/ethereum/go-ethereum) && \
|
||||
go build -v github.com/ethereum/go-ethereum/cmd/faucet && \
|
||||
apk del git go make gcc musl-dev linux-headers && \
|
||||
rm -rf $GOPATH && rm -rf /var/cache/apk/*
|
||||
|
||||
ADD genesis.json /genesis.json
|
||||
ADD account.json /account.json
|
||||
ADD account.pass /account.pass
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ \
|
||||
"/faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", \
|
||||
"--ethport", "{{.EthPort}}", "--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", \
|
||||
"--github.user", "{{.GitHubUser}}", "--github.token", "{{.GitHubToken}}", "--account.json", "/account.json", "--account.pass", "/account.pass" \
|
||||
]`
|
||||
|
||||
// faucetComposefile is the docker-compose.yml file required to deploy and maintain
|
||||
// a crypto faucet.
|
||||
var faucetComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
faucet:
|
||||
build: .
|
||||
image: {{.Network}}/faucet
|
||||
ports:
|
||||
- "{{.EthPort}}:{{.EthPort}}"{{if not .VHost}}
|
||||
- "{{.ApiPort}}:8080"{{end}}
|
||||
volumes:
|
||||
- {{.Datadir}}:/root/.faucet
|
||||
environment:
|
||||
- ETH_PORT={{.EthPort}}
|
||||
- ETH_NAME={{.EthName}}
|
||||
- FAUCET_AMOUNT={{.FaucetAmount}}
|
||||
- FAUCET_MINUTES={{.FaucetMinutes}}
|
||||
- GITHUB_USER={{.GitHubUser}}
|
||||
- GITHUB_TOKEN={{.GitHubToken}}{{if .VHost}}
|
||||
- VIRTUAL_HOST={{.VHost}}
|
||||
- VIRTUAL_PORT=8080{{end}}
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployFaucet deploys a new faucet container to a remote machine via SSH,
|
||||
// docker and docker-compose. If an instance with the specified network name
|
||||
// already exists there, it will be overwritten!
|
||||
func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos) ([]byte, error) {
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(faucetDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"NetworkID": config.node.network,
|
||||
"Bootnodes": strings.Join(bootnodes, ","),
|
||||
"Ethstats": config.node.ethstats,
|
||||
"EthPort": config.node.portFull,
|
||||
"GitHubUser": config.githubUser,
|
||||
"GitHubToken": config.githubToken,
|
||||
"FaucetName": strings.Title(network),
|
||||
"FaucetAmount": config.amount,
|
||||
"FaucetMinutes": config.minutes,
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(faucetComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"Datadir": config.node.datadir,
|
||||
"VHost": config.host,
|
||||
"ApiPort": config.port,
|
||||
"EthPort": config.node.portFull,
|
||||
"EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
|
||||
"GitHubUser": config.githubUser,
|
||||
"GitHubToken": config.githubToken,
|
||||
"FaucetAmount": config.amount,
|
||||
"FaucetMinutes": config.minutes,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
files[filepath.Join(workdir, "genesis.json")] = []byte(config.node.genesis)
|
||||
files[filepath.Join(workdir, "account.json")] = []byte(config.node.keyJSON)
|
||||
files[filepath.Join(workdir, "account.pass")] = []byte(config.node.keyPass)
|
||||
|
||||
// Upload the deployment files to the remote server (and clean up afterwards)
|
||||
if out, err := client.Upload(files); err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer client.Run("rm -rf " + workdir)
|
||||
|
||||
// Build and deploy the faucet service
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
|
||||
}
|
||||
|
||||
// faucetInfos is returned from an faucet status check to allow reporting various
|
||||
// configuration parameters.
|
||||
type faucetInfos struct {
|
||||
node *nodeInfos
|
||||
host string
|
||||
port int
|
||||
amount int
|
||||
minutes int
|
||||
githubUser string
|
||||
githubToken string
|
||||
}
|
||||
|
||||
// String implements the stringer interface.
|
||||
func (info *faucetInfos) String() string {
|
||||
return fmt.Sprintf("host=%s, api=%d, eth=%d, amount=%d, minutes=%d, github=%s, ethstats=%s", info.host, info.port, info.node.portFull, info.amount, info.minutes, info.githubUser, info.node.ethstats)
|
||||
}
|
||||
|
||||
// checkFaucet does a health-check against an faucet server to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkFaucet(client *sshClient, network string) (*faucetInfos, error) {
|
||||
// Inspect a possible faucet container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_faucet_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve the port from the host, or the reverse proxy
|
||||
port := infos.portmap["8080/tcp"]
|
||||
if port == 0 {
|
||||
if proxy, _ := checkNginx(client, network); proxy != nil {
|
||||
port = proxy.port
|
||||
}
|
||||
}
|
||||
if port == 0 {
|
||||
return nil, ErrNotExposed
|
||||
}
|
||||
// Resolve the host from the reverse-proxy and the config values
|
||||
host := infos.envvars["VIRTUAL_HOST"]
|
||||
if host == "" {
|
||||
host = client.server
|
||||
}
|
||||
amount, _ := strconv.Atoi(infos.envvars["FAUCET_AMOUNT"])
|
||||
minutes, _ := strconv.Atoi(infos.envvars["FAUCET_MINUTES"])
|
||||
|
||||
// Retrieve the funding account informations
|
||||
var out []byte
|
||||
keyJSON, keyPass := "", ""
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.json", network)); err == nil {
|
||||
keyJSON = string(bytes.TrimSpace(out))
|
||||
}
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.pass", network)); err == nil {
|
||||
keyPass = string(bytes.TrimSpace(out))
|
||||
}
|
||||
// Run a sanity check to see if the port is reachable
|
||||
if err = checkPort(host, port); err != nil {
|
||||
log.Warn("Faucet service seems unreachable", "server", host, "port", port, "err", err)
|
||||
}
|
||||
// Container available, assemble and return the useful infos
|
||||
return &faucetInfos{
|
||||
node: &nodeInfos{
|
||||
datadir: infos.volumes["/root/.faucet"],
|
||||
portFull: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"],
|
||||
ethstats: infos.envvars["ETH_NAME"],
|
||||
keyJSON: keyJSON,
|
||||
keyPass: keyPass,
|
||||
},
|
||||
host: host,
|
||||
port: port,
|
||||
amount: amount,
|
||||
minutes: minutes,
|
||||
githubUser: infos.envvars["GITHUB_USER"],
|
||||
githubToken: infos.envvars["GITHUB_TOKEN"],
|
||||
}, nil
|
||||
}
|
106
cmd/puppeth/module_nginx.go
Normal file
106
cmd/puppeth/module_nginx.go
Normal file
@ -0,0 +1,106 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// nginxDockerfile is theis the Dockerfile required to build an nginx reverse-
|
||||
// proxy.
|
||||
var nginxDockerfile = `FROM jwilder/nginx-proxy`
|
||||
|
||||
// nginxComposefile is the docker-compose.yml file required to deploy and maintain
|
||||
// an nginx reverse-proxy. The proxy is responsible for exposing one or more HTTP
|
||||
// services running on a single host.
|
||||
var nginxComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
nginx:
|
||||
build: .
|
||||
image: {{.Network}}/nginx
|
||||
ports:
|
||||
- "{{.Port}}:80"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployNginx deploys a new nginx reverse-proxy container to expose one or more
|
||||
// HTTP services running on a single host. If an instance with the specified
|
||||
// network name already exists there, it will be overwritten!
|
||||
func deployNginx(client *sshClient, network string, port int) ([]byte, error) {
|
||||
log.Info("Deploying nginx reverse-proxy", "server", client.server, "port", port)
|
||||
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(nginxDockerfile)).Execute(dockerfile, nil)
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(nginxComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"Port": port,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
// Upload the deployment files to the remote server (and clean up afterwards)
|
||||
if out, err := client.Upload(files); err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer client.Run("rm -rf " + workdir)
|
||||
|
||||
// Build and deploy the ethstats service
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
|
||||
}
|
||||
|
||||
// nginxInfos is returned from an nginx reverse-proxy status check to allow
|
||||
// reporting various configuration parameters.
|
||||
type nginxInfos struct {
|
||||
port int
|
||||
}
|
||||
|
||||
// String implements the stringer interface.
|
||||
func (info *nginxInfos) String() string {
|
||||
return fmt.Sprintf("port=%d", info.port)
|
||||
}
|
||||
|
||||
// checkNginx does a health-check against an nginx reverse-proxy to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkNginx(client *sshClient, network string) (*nginxInfos, error) {
|
||||
// Inspect a possible nginx container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_nginx_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Container available, assemble and return the useful infos
|
||||
return &nginxInfos{
|
||||
port: infos.portmap["80/tcp"],
|
||||
}, nil
|
||||
}
|
222
cmd/puppeth/module_node.go
Normal file
222
cmd/puppeth/module_node.go
Normal file
@ -0,0 +1,222 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// nodeDockerfile is the Dockerfile required to run an Ethereum node.
|
||||
var nodeDockerfile = `
|
||||
FROM ethereum/client-go:alpine-develop
|
||||
|
||||
ADD genesis.json /genesis.json
|
||||
{{if .Unlock}}
|
||||
ADD signer.json /signer.json
|
||||
ADD signer.pass /signer.pass
|
||||
{{end}}
|
||||
RUN \
|
||||
echo '/geth init /genesis.json' > geth.sh && \{{if .Unlock}}
|
||||
echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
|
||||
echo $'/geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine{{end}}{{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}}' >> geth.sh
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "geth.sh"]
|
||||
`
|
||||
|
||||
// nodeComposefile is the docker-compose.yml file required to deploy and maintain
|
||||
// an Ethereum node (bootnode or miner for now).
|
||||
var nodeComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
{{.Type}}:
|
||||
build: .
|
||||
image: {{.Network}}/{{.Type}}
|
||||
ports:
|
||||
- "{{.FullPort}}:{{.FullPort}}"
|
||||
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
|
||||
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
|
||||
volumes:
|
||||
- {{.Datadir}}:/root/.ethereum
|
||||
environment:
|
||||
- FULL_PORT={{.FullPort}}/tcp
|
||||
- LIGHT_PORT={{.LightPort}}/udp
|
||||
- TOTAL_PEERS={{.TotalPeers}}
|
||||
- LIGHT_PEERS={{.LightPeers}}
|
||||
- STATS_NAME={{.Ethstats}}
|
||||
- MINER_NAME={{.Etherbase}}
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployNode deploys a new Ethereum node container to a remote machine via SSH,
|
||||
// docker and docker-compose. If an instance with the specified network name
|
||||
// already exists there, it will be overwritten!
|
||||
func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos) ([]byte, error) {
|
||||
kind := "sealnode"
|
||||
if config.keyJSON == "" && config.etherbase == "" {
|
||||
kind = "bootnode"
|
||||
bootnodes = make([]string, 0)
|
||||
}
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
lightFlag := ""
|
||||
if config.peersLight > 0 {
|
||||
lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
|
||||
}
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"NetworkID": config.network,
|
||||
"Port": config.portFull,
|
||||
"Peers": config.peersTotal,
|
||||
"LightFlag": lightFlag,
|
||||
"Bootnodes": strings.Join(bootnodes, ","),
|
||||
"Ethstats": config.ethstats,
|
||||
"Etherbase": config.etherbase,
|
||||
"Unlock": config.keyJSON != "",
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Type": kind,
|
||||
"Datadir": config.datadir,
|
||||
"Network": network,
|
||||
"FullPort": config.portFull,
|
||||
"TotalPeers": config.peersTotal,
|
||||
"Light": config.peersLight > 0,
|
||||
"LightPort": config.portFull + 1,
|
||||
"LightPeers": config.peersLight,
|
||||
"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
|
||||
"Etherbase": config.etherbase,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
//genesisfile, _ := json.MarshalIndent(config.genesis, "", " ")
|
||||
files[filepath.Join(workdir, "genesis.json")] = []byte(config.genesis)
|
||||
|
||||
if config.keyJSON != "" {
|
||||
files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
|
||||
files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
|
||||
}
|
||||
// Upload the deployment files to the remote server (and clean up afterwards)
|
||||
if out, err := client.Upload(files); err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer client.Run("rm -rf " + workdir)
|
||||
|
||||
// Build and deploy the bootnode service
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
|
||||
}
|
||||
|
||||
// nodeInfos is returned from a boot or seal node status check to allow reporting
|
||||
// various configuration parameters.
|
||||
type nodeInfos struct {
|
||||
genesis []byte
|
||||
network int64
|
||||
datadir string
|
||||
ethstats string
|
||||
portFull int
|
||||
portLight int
|
||||
enodeFull string
|
||||
enodeLight string
|
||||
peersTotal int
|
||||
peersLight int
|
||||
etherbase string
|
||||
keyJSON string
|
||||
keyPass string
|
||||
}
|
||||
|
||||
// String implements the stringer interface.
|
||||
func (info *nodeInfos) String() string {
|
||||
discv5 := ""
|
||||
if info.peersLight > 0 {
|
||||
discv5 = fmt.Sprintf(", portv5=%d", info.portLight)
|
||||
}
|
||||
return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s", info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats)
|
||||
}
|
||||
|
||||
// checkNode does a health-check against an boot or seal node server to verify
|
||||
// whether it's running, and if yes, whether it's responsive.
|
||||
func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
|
||||
kind := "bootnode"
|
||||
if !boot {
|
||||
kind = "sealnode"
|
||||
}
|
||||
// Inspect a possible bootnode container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve a few types from the environmental variables
|
||||
totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
|
||||
lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
|
||||
|
||||
// Container available, retrieve its node ID and its genesis json
|
||||
var out []byte
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 /geth --exec admin.nodeInfo.id attach", network, kind)); err != nil {
|
||||
return nil, ErrServiceUnreachable
|
||||
}
|
||||
id := bytes.Trim(bytes.TrimSpace(out), "\"")
|
||||
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
|
||||
return nil, ErrServiceUnreachable
|
||||
}
|
||||
genesis := bytes.TrimSpace(out)
|
||||
|
||||
keyJSON, keyPass := "", ""
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
|
||||
keyJSON = string(bytes.TrimSpace(out))
|
||||
}
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
|
||||
keyPass = string(bytes.TrimSpace(out))
|
||||
}
|
||||
// Run a sanity check to see if the devp2p is reachable
|
||||
port := infos.portmap[infos.envvars["FULL_PORT"]]
|
||||
if err = checkPort(client.server, port); err != nil {
|
||||
log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
|
||||
}
|
||||
// Assemble and return the useful infos
|
||||
stats := &nodeInfos{
|
||||
genesis: genesis,
|
||||
datadir: infos.volumes["/root/.ethereum"],
|
||||
portFull: infos.portmap[infos.envvars["FULL_PORT"]],
|
||||
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
|
||||
peersTotal: totalPeers,
|
||||
peersLight: lightPeers,
|
||||
ethstats: infos.envvars["STATS_NAME"],
|
||||
etherbase: infos.envvars["MINER_NAME"],
|
||||
keyJSON: keyJSON,
|
||||
keyPass: keyPass,
|
||||
}
|
||||
stats.enodeFull = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.portFull)
|
||||
if stats.portLight != 0 {
|
||||
stats.enodeLight = fmt.Sprintf("enode://%s@%s:%d?discport=%d", id, client.address, stats.portFull, stats.portLight)
|
||||
}
|
||||
return stats, nil
|
||||
}
|
55
cmd/puppeth/puppeth.go
Normal file
55
cmd/puppeth/puppeth.go
Normal file
@ -0,0 +1,55 @@
|
||||
// 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/>.
|
||||
|
||||
// puppeth is a command to assemble and maintain private networks.
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// main is just a boring entry point to set up the CLI app.
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "puppeth"
|
||||
app.Usage = "assemble and maintain private Ethereum networks"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "network",
|
||||
Usage: "name of the network to administer",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "loglevel",
|
||||
Value: 4,
|
||||
Usage: "log level to emit to the screen",
|
||||
},
|
||||
}
|
||||
app.Action = func(c *cli.Context) error {
|
||||
// Set up the logger to print everything and the random generator
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Start the wizard and relinquish control
|
||||
makeWizard(c.String("network")).run()
|
||||
return nil
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
195
cmd/puppeth/ssh.go
Normal file
195
cmd/puppeth/ssh.go
Normal file
@ -0,0 +1,195 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// sshClient is a small wrapper around Go's SSH client with a few utility methods
|
||||
// implemented on top.
|
||||
type sshClient struct {
|
||||
server string // Server name or IP without port number
|
||||
address string // IP address of the remote server
|
||||
client *ssh.Client
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// dial establishes an SSH connection to a remote node using the current user and
|
||||
// the user's configured private RSA key.
|
||||
func dial(server string) (*sshClient, error) {
|
||||
// Figure out a label for the server and a logger
|
||||
label := server
|
||||
if strings.Contains(label, ":") {
|
||||
label = label[:strings.Index(label, ":")]
|
||||
}
|
||||
logger := log.New("server", label)
|
||||
logger.Debug("Attempting to establish SSH connection")
|
||||
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Configure the supported authentication methods (private key and password)
|
||||
var auths []ssh.AuthMethod
|
||||
|
||||
path := filepath.Join(user.HomeDir, ".ssh", "id_rsa")
|
||||
if buf, err := ioutil.ReadFile(path); err != nil {
|
||||
log.Warn("No SSH key, falling back to passwords", "path", path, "err", err)
|
||||
} else {
|
||||
key, err := ssh.ParsePrivateKey(buf)
|
||||
if err != nil {
|
||||
log.Warn("Bad SSH key, falling back to passwords", "path", path, "err", err)
|
||||
} else {
|
||||
auths = append(auths, ssh.PublicKeys(key))
|
||||
}
|
||||
}
|
||||
auths = append(auths, ssh.PasswordCallback(func() (string, error) {
|
||||
fmt.Printf("What's the login password for %s at %s? (won't be echoed)\n> ", user.Username, server)
|
||||
blob, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
|
||||
fmt.Println()
|
||||
return string(blob), err
|
||||
}))
|
||||
// Resolve the IP address of the remote server
|
||||
addr, err := net.LookupHost(label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addr) == 0 {
|
||||
return nil, errors.New("no IPs associated with domain")
|
||||
}
|
||||
// Try to dial in to the remote server
|
||||
logger.Trace("Dialing remote SSH server", "user", user.Username, "key", path)
|
||||
if !strings.Contains(server, ":") {
|
||||
server += ":22"
|
||||
}
|
||||
client, err := ssh.Dial("tcp", server, &ssh.ClientConfig{User: user.Username, Auth: auths})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Connection established, return our utility wrapper
|
||||
c := &sshClient{
|
||||
server: label,
|
||||
address: addr[0],
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
if err := c.init(); err != nil {
|
||||
client.Close()
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// init runs some initialization commands on the remote server to ensure it's
|
||||
// capable of acting as puppeth target.
|
||||
func (client *sshClient) init() error {
|
||||
client.logger.Debug("Verifying if docker is available")
|
||||
if out, err := client.Run("docker version"); err != nil {
|
||||
if len(out) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("docker configured incorrectly: %s", out)
|
||||
}
|
||||
client.logger.Debug("Verifying if docker-compose is available")
|
||||
if out, err := client.Run("docker-compose version"); err != nil {
|
||||
if len(out) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("docker-compose configured incorrectly: %s", out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close terminates the connection to an SSH server.
|
||||
func (client *sshClient) Close() error {
|
||||
return client.client.Close()
|
||||
}
|
||||
|
||||
// Run executes a command on the remote server and returns the combined output
|
||||
// along with any error status.
|
||||
func (client *sshClient) Run(cmd string) ([]byte, error) {
|
||||
// Establish a single command session
|
||||
session, err := client.client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Execute the command and return any output
|
||||
client.logger.Trace("Running command on remote server", "cmd", cmd)
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
|
||||
// Stream executes a command on the remote server and streams all outputs into
|
||||
// the local stdout and stderr streams.
|
||||
func (client *sshClient) Stream(cmd string) error {
|
||||
// Establish a single command session
|
||||
session, err := client.client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
session.Stdout = os.Stdout
|
||||
session.Stderr = os.Stderr
|
||||
|
||||
// Execute the command and return any output
|
||||
client.logger.Trace("Streaming command on remote server", "cmd", cmd)
|
||||
return session.Run(cmd)
|
||||
}
|
||||
|
||||
// Upload copied the set of files to a remote server via SCP, creating any non-
|
||||
// existing folder in te mean time.
|
||||
func (client *sshClient) Upload(files map[string][]byte) ([]byte, error) {
|
||||
// Establish a single command session
|
||||
session, err := client.client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Create a goroutine that streams the SCP content
|
||||
go func() {
|
||||
out, _ := session.StdinPipe()
|
||||
defer out.Close()
|
||||
|
||||
for file, content := range files {
|
||||
client.logger.Trace("Uploading file to server", "file", file, "bytes", len(content))
|
||||
|
||||
fmt.Fprintln(out, "D0755", 0, filepath.Dir(file)) // Ensure the folder exists
|
||||
fmt.Fprintln(out, "C0644", len(content), filepath.Base(file)) // Create the actual file
|
||||
out.Write(content) // Stream the data content
|
||||
fmt.Fprint(out, "\x00") // Transfer end with \x00
|
||||
fmt.Fprintln(out, "E") // Leave directory (simpler)
|
||||
}
|
||||
}()
|
||||
return session.CombinedOutput("/usr/bin/scp -v -tr ./")
|
||||
}
|
229
cmd/puppeth/wizard.go
Normal file
229
cmd/puppeth/wizard.go
Normal file
@ -0,0 +1,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)
|
||||
}
|
||||
}
|
132
cmd/puppeth/wizard_dashboard.go
Normal file
132
cmd/puppeth/wizard_dashboard.go
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployDashboard queries the user for various input on deploying a web-service
|
||||
// dashboard, after which is pushes the container.
|
||||
func (w *wizard) deployDashboard() {
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active dashboard configurations from the server
|
||||
infos, err := checkDashboard(client, w.network)
|
||||
if err != nil {
|
||||
infos = &dashboardInfos{
|
||||
port: 80,
|
||||
host: client.server,
|
||||
}
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port)
|
||||
infos.port = w.readDefaultInt(infos.port)
|
||||
|
||||
// Figure which virtual-host to deploy the dashboard on
|
||||
infos.host, err = w.ensureVirtualHost(client, infos.port, infos.host)
|
||||
if err != nil {
|
||||
log.Error("Failed to decide on dashboard host", "err", err)
|
||||
return
|
||||
}
|
||||
// Port and proxy settings retrieved, figure out which services are available
|
||||
available := make(map[string][]string)
|
||||
for server, services := range w.services {
|
||||
for _, service := range services {
|
||||
available[service] = append(available[service], server)
|
||||
}
|
||||
}
|
||||
listing := make(map[string]string)
|
||||
for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} {
|
||||
// Gather all the locally hosted pages of this type
|
||||
var pages []string
|
||||
for _, server := range available[service] {
|
||||
client := w.servers[server]
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
// If there's a service running on the machine, retrieve it's port number
|
||||
var port int
|
||||
switch service {
|
||||
case "ethstats":
|
||||
if infos, err := checkEthstats(client, w.network); err == nil {
|
||||
port = infos.port
|
||||
}
|
||||
case "faucet":
|
||||
if infos, err := checkFaucet(client, w.network); err == nil {
|
||||
port = infos.port
|
||||
}
|
||||
}
|
||||
if page, err := resolve(client, w.network, service, port); err == nil && page != "" {
|
||||
pages = append(pages, page)
|
||||
}
|
||||
}
|
||||
// Promt the user to chose one, enter manually or simply not list this service
|
||||
defLabel, defChoice := "don't list", len(pages)+2
|
||||
if len(pages) > 0 {
|
||||
defLabel, defChoice = pages[0], 1
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("Which %s service to list? (default = %s)\n", service, defLabel)
|
||||
for i, page := range pages {
|
||||
fmt.Printf(" %d. %s\n", i+1, page)
|
||||
}
|
||||
fmt.Printf(" %d. List external %s service\n", len(pages)+1, service)
|
||||
fmt.Printf(" %d. Don't list any %s service\n", len(pages)+2, service)
|
||||
|
||||
choice := w.readDefaultInt(defChoice)
|
||||
if choice < 0 || choice > len(pages)+2 {
|
||||
log.Error("Invalid listing choice, aborting")
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case choice <= len(pages):
|
||||
listing[service] = pages[choice-1]
|
||||
case choice == len(pages)+1:
|
||||
fmt.Println()
|
||||
fmt.Printf("Which address is the external %s service at?\n", service)
|
||||
listing[service] = w.readString()
|
||||
default:
|
||||
// No service hosting for this
|
||||
}
|
||||
}
|
||||
// If we have ethstats running, ask whether to make the secret public or not
|
||||
var ethstats bool
|
||||
if w.conf.ethstats != "" {
|
||||
fmt.Println()
|
||||
fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)")
|
||||
ethstats = w.readDefaultString("y") == "y"
|
||||
}
|
||||
// Try to deploy the dashboard container on the host
|
||||
if out, err := deployDashboard(client, w.network, infos.port, infos.host, listing, &w.conf, ethstats); err != nil {
|
||||
log.Error("Failed to deploy dashboard container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
w.networkStats(false)
|
||||
}
|
79
cmd/puppeth/wizard_ethstats.go
Normal file
79
cmd/puppeth/wizard_ethstats.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployEthstats queries the user for various input on deploying an ethstats
|
||||
// monitoring server, after which it executes it.
|
||||
func (w *wizard) deployEthstats() {
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active ethstats configurations from the server
|
||||
infos, err := checkEthstats(client, w.network)
|
||||
if err != nil {
|
||||
infos = ðstatsInfos{
|
||||
port: 80,
|
||||
host: client.server,
|
||||
secret: "",
|
||||
}
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port)
|
||||
infos.port = w.readDefaultInt(infos.port)
|
||||
|
||||
// Figure which virtual-host to deploy ethstats on
|
||||
if infos.host, err = w.ensureVirtualHost(client, infos.port, infos.host); err != nil {
|
||||
log.Error("Failed to decide on ethstats host", "err", err)
|
||||
return
|
||||
}
|
||||
// Port and proxy settings retrieved, figure out the secret and boot ethstats
|
||||
fmt.Println()
|
||||
if infos.secret == "" {
|
||||
fmt.Printf("What should be the secret password for the API? (must not be empty)\n")
|
||||
infos.secret = w.readString()
|
||||
} else {
|
||||
fmt.Printf("What should be the secret password for the API? (default = %s)\n", infos.secret)
|
||||
infos.secret = w.readDefaultString(infos.secret)
|
||||
}
|
||||
// Try to deploy the ethstats server on the host
|
||||
trusted := make([]string, 0, len(w.servers))
|
||||
for _, client := range w.servers {
|
||||
if client != nil {
|
||||
trusted = append(trusted, client.address)
|
||||
}
|
||||
}
|
||||
if out, err := deployEthstats(client, w.network, infos.port, infos.secret, infos.host, trusted); err != nil {
|
||||
log.Error("Failed to deploy ethstats container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
w.networkStats(false)
|
||||
}
|
172
cmd/puppeth/wizard_faucet.go
Normal file
172
cmd/puppeth/wizard_faucet.go
Normal file
@ -0,0 +1,172 @@
|
||||
// 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"
|
||||
"net/http"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployFaucet queries the user for various input on deploying a faucet, after
|
||||
// which it executes it.
|
||||
func (w *wizard) deployFaucet() {
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active faucet configurations from the server
|
||||
infos, err := checkFaucet(client, w.network)
|
||||
if err != nil {
|
||||
infos = &faucetInfos{
|
||||
node: &nodeInfos{portFull: 30303, peersTotal: 25},
|
||||
port: 80,
|
||||
host: client.server,
|
||||
amount: 1,
|
||||
minutes: 1440,
|
||||
}
|
||||
}
|
||||
infos.node.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ")
|
||||
infos.node.network = w.conf.genesis.Config.ChainId.Int64()
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the faucet listen on? (default = %d)\n", infos.port)
|
||||
infos.port = w.readDefaultInt(infos.port)
|
||||
|
||||
// Figure which virtual-host to deploy ethstats on
|
||||
if infos.host, err = w.ensureVirtualHost(client, infos.port, infos.host); err != nil {
|
||||
log.Error("Failed to decide on faucet host", "err", err)
|
||||
return
|
||||
}
|
||||
// Port and proxy settings retrieved, figure out the funcing amount per perdion configurations
|
||||
fmt.Println()
|
||||
fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
|
||||
infos.amount = w.readDefaultInt(infos.amount)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("How many minutes to enforce between requests? (default = %d)\n", infos.minutes)
|
||||
infos.minutes = w.readDefaultInt(infos.minutes)
|
||||
|
||||
// Accessing GitHub gists requires API authorization, retrieve it
|
||||
if infos.githubUser != "" {
|
||||
fmt.Println()
|
||||
fmt.Printf("Reused previous (%s) GitHub API authorization (y/n)? (default = yes)\n", infos.githubUser)
|
||||
if w.readDefaultString("y") != "y" {
|
||||
infos.githubUser, infos.githubToken = "", ""
|
||||
}
|
||||
}
|
||||
if infos.githubUser == "" {
|
||||
// No previous authorization (or new one requested)
|
||||
fmt.Println()
|
||||
fmt.Println("Which GitHub user to verify Gists through?")
|
||||
infos.githubUser = w.readString()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("What is the GitHub personal access token of the user? (won't be echoed)")
|
||||
infos.githubToken = w.readPassword()
|
||||
|
||||
// Do a sanity check query against github to ensure it's valid
|
||||
req, _ := http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||
req.SetBasicAuth(infos.githubUser, infos.githubToken)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Error("Failed to verify GitHub authentication", "err", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var msg struct {
|
||||
Login string `json:"login"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&msg); err != nil {
|
||||
log.Error("Failed to decode authorization response", "err", err)
|
||||
return
|
||||
}
|
||||
if msg.Login != infos.githubUser {
|
||||
log.Error("GitHub authorization failed", "user", infos.githubUser, "message", msg.Message)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
if infos.node.datadir == "" {
|
||||
fmt.Printf("Where should data be stored on the remote machine?\n")
|
||||
infos.node.datadir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.node.datadir)
|
||||
infos.node.datadir = w.readDefaultString(infos.node.datadir)
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which TCP/UDP port should the light client listen on? (default = %d)\n", infos.node.portFull)
|
||||
infos.node.portFull = w.readDefaultInt(infos.node.portFull)
|
||||
|
||||
// Set a proper name to report on the stats page
|
||||
fmt.Println()
|
||||
if infos.node.ethstats == "" {
|
||||
fmt.Printf("What should the node be called on the stats page?\n")
|
||||
infos.node.ethstats = w.readString() + ":" + w.conf.ethstats
|
||||
} else {
|
||||
fmt.Printf("What should the node be called on the stats page? (default = %s)\n", infos.node.ethstats)
|
||||
infos.node.ethstats = w.readDefaultString(infos.node.ethstats) + ":" + w.conf.ethstats
|
||||
}
|
||||
// Load up the credential needed to release funds
|
||||
if infos.node.keyJSON != "" {
|
||||
var key keystore.Key
|
||||
if err := json.Unmarshal([]byte(infos.node.keyJSON), &key); err != nil {
|
||||
infos.node.keyJSON, infos.node.keyPass = "", ""
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Printf("Reuse previous (%s) funding account (y/n)? (default = yes)\n", key.Address.Hex())
|
||||
if w.readDefaultString("y") != "y" {
|
||||
infos.node.keyJSON, infos.node.keyPass = "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if infos.node.keyJSON == "" {
|
||||
fmt.Println()
|
||||
fmt.Println("Please paste the faucet's funding account key JSON:")
|
||||
infos.node.keyJSON = w.readJSON()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("What's the unlock password for the account? (won't be echoed)")
|
||||
infos.node.keyPass = w.readPassword()
|
||||
|
||||
if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil {
|
||||
log.Error("Failed to decrypt key with given passphrase")
|
||||
return
|
||||
}
|
||||
}
|
||||
// Try to deploy the faucet server on the host
|
||||
if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos); err != nil {
|
||||
log.Error("Failed to deploy faucet container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
w.networkStats(false)
|
||||
}
|
136
cmd/puppeth/wizard_genesis.go
Normal file
136
cmd/puppeth/wizard_genesis.go
Normal file
@ -0,0 +1,136 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// makeGenesis creates a new genesis struct based on some user input.
|
||||
func (w *wizard) makeGenesis() {
|
||||
// Construct a default genesis block
|
||||
genesis := &core.Genesis{
|
||||
Timestamp: uint64(time.Now().Unix()),
|
||||
GasLimit: 4700000,
|
||||
Difficulty: big.NewInt(1048576),
|
||||
Alloc: make(core.GenesisAlloc),
|
||||
Config: ¶ms.ChainConfig{
|
||||
HomesteadBlock: big.NewInt(1),
|
||||
EIP150Block: big.NewInt(2),
|
||||
EIP155Block: big.NewInt(3),
|
||||
EIP158Block: big.NewInt(3),
|
||||
},
|
||||
}
|
||||
// Figure out which consensus engine to choose
|
||||
fmt.Println()
|
||||
fmt.Println("Which consensus engine to use? (default = clique)")
|
||||
fmt.Println(" 1. Ethash - proof-of-work")
|
||||
fmt.Println(" 2. Clique - proof-of-authority")
|
||||
|
||||
choice := w.read()
|
||||
switch {
|
||||
case choice == "1":
|
||||
// In case of ethash, we're pretty much done
|
||||
genesis.Config.Ethash = new(params.EthashConfig)
|
||||
genesis.ExtraData = make([]byte, 32)
|
||||
|
||||
case choice == "" || choice == "2":
|
||||
// In the case of clique, configure the consensus parameters
|
||||
genesis.Difficulty = big.NewInt(1)
|
||||
genesis.Config.Clique = ¶ms.CliqueConfig{
|
||||
Period: 15,
|
||||
Epoch: 30000,
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("How many seconds should blocks take? (default = 15)")
|
||||
genesis.Config.Clique.Period = uint64(w.readDefaultInt(15))
|
||||
|
||||
// We also need the initial list of signers
|
||||
fmt.Println()
|
||||
fmt.Println("Which accounts are allowed to seal? (mandatory at least one)")
|
||||
|
||||
var signers []common.Address
|
||||
for {
|
||||
if address := w.readAddress(); address != nil {
|
||||
signers = append(signers, *address)
|
||||
continue
|
||||
}
|
||||
if len(signers) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Sort the signers and embed into the extra-data section
|
||||
for i := 0; i < len(signers); i++ {
|
||||
for j := i + 1; j < len(signers); j++ {
|
||||
if bytes.Compare(signers[i][:], signers[j][:]) > 0 {
|
||||
signers[i], signers[j] = signers[j], signers[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
genesis.ExtraData = make([]byte, 32+len(signers)*common.AddressLength+65)
|
||||
for i, signer := range signers {
|
||||
copy(genesis.ExtraData[32+i*common.AddressLength:], signer[:])
|
||||
}
|
||||
|
||||
default:
|
||||
log.Crit("Invalid consensus engine choice", "choice", choice)
|
||||
}
|
||||
// Consensus all set, just ask for initial funds and go
|
||||
fmt.Println()
|
||||
fmt.Println("Which accounts should be pre-funded? (advisable at least one)")
|
||||
for {
|
||||
// Read the address of the account to fund
|
||||
if address := w.readAddress(); address != nil {
|
||||
genesis.Alloc[*address] = core.GenesisAccount{
|
||||
Balance: new(big.Int).Lsh(big.NewInt(1), 256-7), // 2^256 / 128 (allow many pre-funds without balance overflows)
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add a batch of precompile balances to avoid them getting deleted
|
||||
for i := int64(0); i < 256; i++ {
|
||||
genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Query the user for some custom extras
|
||||
fmt.Println()
|
||||
fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)")
|
||||
genesis.Config.ChainId = big.NewInt(int64(w.readDefaultInt(rand.Intn(65536))))
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Anything fun to embed into the genesis block? (max 32 bytes)")
|
||||
|
||||
extra := w.read()
|
||||
if len(extra) > 32 {
|
||||
extra = extra[:32]
|
||||
}
|
||||
genesis.ExtraData = append([]byte(extra), genesis.ExtraData[len(extra):]...)
|
||||
|
||||
// All done, store the genesis and flush to disk
|
||||
w.conf.genesis = genesis
|
||||
}
|
153
cmd/puppeth/wizard_intro.go
Normal file
153
cmd/puppeth/wizard_intro.go
Normal file
@ -0,0 +1,153 @@
|
||||
// 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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// makeWizard creates and returns a new puppeth wizard.
|
||||
func makeWizard(network string) *wizard {
|
||||
return &wizard{
|
||||
network: network,
|
||||
servers: make(map[string]*sshClient),
|
||||
services: make(map[string][]string),
|
||||
in: bufio.NewReader(os.Stdin),
|
||||
}
|
||||
}
|
||||
|
||||
// run displays some useful infos to the user, starting on the journey of
|
||||
// setting up a new or managing an existing Ethereum private network.
|
||||
func (w *wizard) run() {
|
||||
fmt.Println("+-----------------------------------------------------------+")
|
||||
fmt.Println("| Welcome to puppeth, your Ethereum private network manager |")
|
||||
fmt.Println("| |")
|
||||
fmt.Println("| This tool lets you create a new Ethereum network down to |")
|
||||
fmt.Println("| the genesis block, bootnodes, miners and ethstats servers |")
|
||||
fmt.Println("| without the hassle that it would normally entail. |")
|
||||
fmt.Println("| |")
|
||||
fmt.Println("| Puppeth uses SSH to dial in to remote servers, and builds |")
|
||||
fmt.Println("| its network components out of Docker containers using the |")
|
||||
fmt.Println("| docker-compose toolset. |")
|
||||
fmt.Println("+-----------------------------------------------------------+")
|
||||
fmt.Println()
|
||||
|
||||
// Make sure we have a good network name to work with fmt.Println()
|
||||
if w.network == "" {
|
||||
fmt.Println("Please specify a network name to administer (no spaces, please)")
|
||||
for {
|
||||
w.network = w.readString()
|
||||
if !strings.Contains(w.network, " ") {
|
||||
fmt.Printf("Sweet, you can set this via --network=%s next time!\n\n", w.network)
|
||||
break
|
||||
}
|
||||
log.Error("I also like to live dangerously, still no spaces")
|
||||
}
|
||||
}
|
||||
log.Info("Administering Ethereum network", "name", w.network)
|
||||
|
||||
// Load initial configurations and connect to all live servers
|
||||
w.conf.path = filepath.Join(os.Getenv("HOME"), ".puppeth", w.network)
|
||||
|
||||
blob, err := ioutil.ReadFile(w.conf.path)
|
||||
if err != nil {
|
||||
log.Warn("No previous configurations found", "path", w.conf.path)
|
||||
} else if err := json.Unmarshal(blob, &w.conf); err != nil {
|
||||
log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err)
|
||||
} else {
|
||||
for _, server := range w.conf.Servers {
|
||||
log.Info("Dialing previously configured server", "server", server)
|
||||
client, err := dial(server)
|
||||
if err != nil {
|
||||
log.Error("Previous server unreachable", "server", server, "err", err)
|
||||
}
|
||||
w.servers[server] = client
|
||||
}
|
||||
w.networkStats(false)
|
||||
}
|
||||
// Basics done, loop ad infinitum about what to do
|
||||
for {
|
||||
fmt.Println()
|
||||
fmt.Println("What would you like to do? (default = stats)")
|
||||
fmt.Println(" 1. Show network stats")
|
||||
if w.conf.genesis == nil {
|
||||
fmt.Println(" 2. Configure new genesis")
|
||||
} else {
|
||||
fmt.Println(" 2. Save existing genesis")
|
||||
}
|
||||
if len(w.servers) == 0 {
|
||||
fmt.Println(" 3. Track new remote server")
|
||||
} else {
|
||||
fmt.Println(" 3. Manage tracked machines")
|
||||
}
|
||||
if len(w.services) == 0 {
|
||||
fmt.Println(" 4. Deploy network components")
|
||||
} else {
|
||||
fmt.Println(" 4. Manage network components")
|
||||
}
|
||||
//fmt.Println(" 5. ProTips for common usecases")
|
||||
|
||||
choice := w.read()
|
||||
switch {
|
||||
case choice == "" || choice == "1":
|
||||
w.networkStats(false)
|
||||
|
||||
case choice == "2":
|
||||
// If we don't have a genesis, make one
|
||||
if w.conf.genesis == nil {
|
||||
w.makeGenesis()
|
||||
} else {
|
||||
// Otherwise just save whatever we currently have
|
||||
fmt.Println()
|
||||
fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network)
|
||||
out, _ := json.MarshalIndent(w.conf.genesis, "", " ")
|
||||
if err := ioutil.WriteFile(w.readDefaultString(fmt.Sprintf("%s.json", w.network)), out, 0644); err != nil {
|
||||
log.Error("Failed to save genesis file", "err", err)
|
||||
}
|
||||
log.Info("Exported existing genesis block")
|
||||
}
|
||||
case choice == "3":
|
||||
if len(w.servers) == 0 {
|
||||
if w.makeServer() != "" {
|
||||
w.networkStats(false)
|
||||
}
|
||||
} else {
|
||||
w.manageServers()
|
||||
}
|
||||
case choice == "4":
|
||||
if len(w.services) == 0 {
|
||||
w.deployComponent()
|
||||
} else {
|
||||
w.manageComponents()
|
||||
}
|
||||
|
||||
case choice == "5":
|
||||
w.networkStats(true)
|
||||
|
||||
default:
|
||||
log.Error("That's not something I can do")
|
||||
}
|
||||
}
|
||||
}
|
235
cmd/puppeth/wizard_netstats.go
Normal file
235
cmd/puppeth/wizard_netstats.go
Normal file
@ -0,0 +1,235 @@
|
||||
// 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"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
// networkStats verifies the status of network components and generates a protip
|
||||
// configuration set to give users hints on how to do various tasks.
|
||||
func (w *wizard) networkStats(tips bool) {
|
||||
if len(w.servers) == 0 {
|
||||
log.Error("No remote machines to gather stats from")
|
||||
return
|
||||
}
|
||||
protips := new(protips)
|
||||
|
||||
// Iterate over all the specified hosts and check their status
|
||||
stats := tablewriter.NewWriter(os.Stdout)
|
||||
stats.SetHeader([]string{"Server", "IP", "Status", "Service", "Details"})
|
||||
stats.SetColWidth(128)
|
||||
|
||||
for _, server := range w.conf.Servers {
|
||||
client := w.servers[server]
|
||||
logger := log.New("server", server)
|
||||
logger.Info("Starting remote server health-check")
|
||||
|
||||
// If the server is not connected, try to connect again
|
||||
if client == nil {
|
||||
conn, err := dial(server)
|
||||
if err != nil {
|
||||
logger.Error("Failed to establish remote connection", "err", err)
|
||||
stats.Append([]string{server, "", err.Error(), "", ""})
|
||||
continue
|
||||
}
|
||||
client = conn
|
||||
}
|
||||
// Client connected one way or another, run health-checks
|
||||
services := make(map[string]string)
|
||||
logger.Debug("Checking for nginx availability")
|
||||
if infos, err := checkNginx(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
services["nginx"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
services["nginx"] = infos.String()
|
||||
}
|
||||
logger.Debug("Checking for ethstats availability")
|
||||
if infos, err := checkEthstats(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
services["ethstats"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
services["ethstats"] = infos.String()
|
||||
protips.ethstats = infos.config
|
||||
}
|
||||
logger.Debug("Checking for bootnode availability")
|
||||
if infos, err := checkNode(client, w.network, true); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
services["bootnode"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
services["bootnode"] = infos.String()
|
||||
|
||||
protips.genesis = string(infos.genesis)
|
||||
protips.bootFull = append(protips.bootFull, infos.enodeFull)
|
||||
if infos.enodeLight != "" {
|
||||
protips.bootLight = append(protips.bootLight, infos.enodeLight)
|
||||
}
|
||||
}
|
||||
logger.Debug("Checking for sealnode availability")
|
||||
if infos, err := checkNode(client, w.network, false); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
services["sealnode"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
services["sealnode"] = infos.String()
|
||||
protips.genesis = string(infos.genesis)
|
||||
}
|
||||
logger.Debug("Checking for faucet availability")
|
||||
if infos, err := checkFaucet(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
services["faucet"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
services["faucet"] = infos.String()
|
||||
}
|
||||
logger.Debug("Checking for dashboard availability")
|
||||
if infos, err := checkDashboard(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
services["dashboard"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
services["dashboard"] = infos.String()
|
||||
}
|
||||
// All status checks complete, report and check next server
|
||||
delete(w.services, server)
|
||||
for service := range services {
|
||||
w.services[server] = append(w.services[server], service)
|
||||
}
|
||||
server, address := client.server, client.address
|
||||
for service, status := range services {
|
||||
stats.Append([]string{server, address, "online", service, status})
|
||||
server, address = "", ""
|
||||
}
|
||||
if len(services) == 0 {
|
||||
stats.Append([]string{server, address, "online", "", ""})
|
||||
}
|
||||
}
|
||||
// If a genesis block was found, load it into our configs
|
||||
if protips.genesis != "" {
|
||||
genesis := new(core.Genesis)
|
||||
if err := json.Unmarshal([]byte(protips.genesis), genesis); err != nil {
|
||||
log.Error("Failed to parse remote genesis", "err", err)
|
||||
} else {
|
||||
w.conf.genesis = genesis
|
||||
protips.network = genesis.Config.ChainId.Int64()
|
||||
}
|
||||
}
|
||||
if protips.ethstats != "" {
|
||||
w.conf.ethstats = protips.ethstats
|
||||
}
|
||||
w.conf.bootFull = protips.bootFull
|
||||
w.conf.bootLight = protips.bootLight
|
||||
|
||||
// Print any collected stats and return
|
||||
if !tips {
|
||||
stats.Render()
|
||||
} else {
|
||||
protips.print(w.network)
|
||||
}
|
||||
}
|
||||
|
||||
// protips contains a collection of network infos to report pro-tips
|
||||
// based on.
|
||||
type protips struct {
|
||||
genesis string
|
||||
network int64
|
||||
bootFull []string
|
||||
bootLight []string
|
||||
ethstats string
|
||||
}
|
||||
|
||||
// print analyzes the network information available and prints a collection of
|
||||
// pro tips for the user's consideration.
|
||||
func (p *protips) print(network string) {
|
||||
// If a known genesis block is available, display it and prepend an init command
|
||||
fullinit, lightinit := "", ""
|
||||
if p.genesis != "" {
|
||||
fullinit = fmt.Sprintf("geth --datadir=$HOME/.%s init %s.json && ", network, network)
|
||||
lightinit = fmt.Sprintf("geth --datadir=$HOME/.%s --light init %s.json && ", network, network)
|
||||
}
|
||||
// If an ethstats server is available, add the ethstats flag
|
||||
statsflag := ""
|
||||
if p.ethstats != "" {
|
||||
if strings.Contains(p.ethstats, " ") {
|
||||
statsflag = fmt.Sprintf(` --ethstats="yournode:%s"`, p.ethstats)
|
||||
} else {
|
||||
statsflag = fmt.Sprintf(` --ethstats=yournode:%s`, p.ethstats)
|
||||
}
|
||||
}
|
||||
// If bootnodes have been specified, add the bootnode flag
|
||||
bootflagFull := ""
|
||||
if len(p.bootFull) > 0 {
|
||||
bootflagFull = fmt.Sprintf(` --bootnodes %s`, strings.Join(p.bootFull, ","))
|
||||
}
|
||||
bootflagLight := ""
|
||||
if len(p.bootLight) > 0 {
|
||||
bootflagLight = fmt.Sprintf(` --bootnodes %s`, strings.Join(p.bootLight, ","))
|
||||
}
|
||||
// Assemble all the known pro-tips
|
||||
var tasks, tips []string
|
||||
|
||||
tasks = append(tasks, "Run an archive node with historical data")
|
||||
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=1024%s%s", fullinit, p.network, network, statsflag, bootflagFull))
|
||||
|
||||
tasks = append(tasks, "Run a full node with recent data only")
|
||||
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=512 --fast%s%s", fullinit, p.network, network, statsflag, bootflagFull))
|
||||
|
||||
tasks = append(tasks, "Run a light node with on demand retrievals")
|
||||
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --light%s%s", lightinit, p.network, network, statsflag, bootflagLight))
|
||||
|
||||
tasks = append(tasks, "Run an embedded node with constrained memory")
|
||||
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=32 --light%s%s", lightinit, p.network, network, statsflag, bootflagLight))
|
||||
|
||||
// If the tips are short, display in a table
|
||||
short := true
|
||||
for _, tip := range tips {
|
||||
if len(tip) > 100 {
|
||||
short = false
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
if short {
|
||||
howto := tablewriter.NewWriter(os.Stdout)
|
||||
howto.SetHeader([]string{"Fun tasks for you", "Tips on how to"})
|
||||
howto.SetColWidth(100)
|
||||
|
||||
for i := 0; i < len(tasks); i++ {
|
||||
howto.Append([]string{tasks[i], tips[i]})
|
||||
}
|
||||
howto.Render()
|
||||
return
|
||||
}
|
||||
// Meh, tips got ugly, split into many lines
|
||||
for i := 0; i < len(tasks); i++ {
|
||||
fmt.Println(tasks[i])
|
||||
fmt.Println(strings.Repeat("-", len(tasks[i])))
|
||||
fmt.Println(tips[i])
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
194
cmd/puppeth/wizard_network.go
Normal file
194
cmd/puppeth/wizard_network.go
Normal file
@ -0,0 +1,194 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// manageServers displays a list of servers the user can disconnect from, and an
|
||||
// option to connect to new servers.
|
||||
func (w *wizard) manageServers() {
|
||||
// List all the servers we can disconnect, along with an entry to connect a new one
|
||||
fmt.Println()
|
||||
for i, server := range w.conf.Servers {
|
||||
fmt.Printf(" %d. Disconnect %s\n", i+1, server)
|
||||
}
|
||||
fmt.Printf(" %d. Connect another server\n", len(w.conf.Servers)+1)
|
||||
|
||||
choice := w.readInt()
|
||||
if choice < 0 || choice > len(w.conf.Servers)+1 {
|
||||
log.Error("Invalid server choice, aborting")
|
||||
return
|
||||
}
|
||||
// If the user selected an existing server, drop it
|
||||
if choice <= len(w.conf.Servers) {
|
||||
server := w.conf.Servers[choice-1]
|
||||
client := w.servers[server]
|
||||
|
||||
delete(w.servers, server)
|
||||
if client != nil {
|
||||
client.Close()
|
||||
}
|
||||
w.conf.Servers = append(w.conf.Servers[:choice-1], w.conf.Servers[choice:]...)
|
||||
w.conf.flush()
|
||||
|
||||
log.Info("Disconnected existing server", "server", server)
|
||||
w.networkStats(false)
|
||||
return
|
||||
}
|
||||
// If the user requested connecting a new server, do it
|
||||
if w.makeServer() != "" {
|
||||
w.networkStats(false)
|
||||
}
|
||||
}
|
||||
|
||||
// makeServer reads a single line from stdin and interprets it as a hostname to
|
||||
// connect to. It tries to establish a new SSH session and also executing some
|
||||
// baseline validations.
|
||||
//
|
||||
// If connection succeeds, the server is added to the wizards configs!
|
||||
func (w *wizard) makeServer() string {
|
||||
fmt.Println()
|
||||
fmt.Println("Please enter remote server's address:")
|
||||
|
||||
for {
|
||||
// Read and fial the server to ensure docker is present
|
||||
input := w.readString()
|
||||
|
||||
client, err := dial(input)
|
||||
if err != nil {
|
||||
log.Error("Server not ready for puppeth", "err", err)
|
||||
return ""
|
||||
}
|
||||
// All checks passed, start tracking the server
|
||||
w.servers[input] = client
|
||||
w.conf.Servers = append(w.conf.Servers, input)
|
||||
w.conf.flush()
|
||||
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
// selectServer lists the user all the currnetly known servers to choose from,
|
||||
// also granting the option to add a new one.
|
||||
func (w *wizard) selectServer() string {
|
||||
// List the available server to the user and wait for a choice
|
||||
fmt.Println()
|
||||
fmt.Println("Which server do you want to interact with?")
|
||||
for i, server := range w.conf.Servers {
|
||||
fmt.Printf(" %d. %s\n", i+1, server)
|
||||
}
|
||||
fmt.Printf(" %d. Connect another server\n", len(w.conf.Servers)+1)
|
||||
|
||||
choice := w.readInt()
|
||||
if choice < 0 || choice > len(w.conf.Servers)+1 {
|
||||
log.Error("Invalid server choice, aborting")
|
||||
return ""
|
||||
}
|
||||
// If the user requested connecting to a new server, go for it
|
||||
if choice <= len(w.conf.Servers) {
|
||||
return w.conf.Servers[choice-1]
|
||||
}
|
||||
return w.makeServer()
|
||||
}
|
||||
|
||||
// manageComponents displays a list of network components the user can tear down
|
||||
// and an option
|
||||
func (w *wizard) manageComponents() {
|
||||
// List all the componens we can tear down, along with an entry to deploy a new one
|
||||
fmt.Println()
|
||||
|
||||
var serviceHosts, serviceNames []string
|
||||
for server, services := range w.services {
|
||||
for _, service := range services {
|
||||
serviceHosts = append(serviceHosts, server)
|
||||
serviceNames = append(serviceNames, service)
|
||||
|
||||
fmt.Printf(" %d. Tear down %s on %s\n", len(serviceHosts), strings.Title(service), server)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" %d. Deploy new network component\n", len(serviceHosts)+1)
|
||||
|
||||
choice := w.readInt()
|
||||
if choice < 0 || choice > len(serviceHosts)+1 {
|
||||
log.Error("Invalid component choice, aborting")
|
||||
return
|
||||
}
|
||||
// If the user selected an existing service, destroy it
|
||||
if choice <= len(serviceHosts) {
|
||||
// Figure out the service to destroy and execute it
|
||||
service := serviceNames[choice-1]
|
||||
server := serviceHosts[choice-1]
|
||||
client := w.servers[server]
|
||||
|
||||
if out, err := tearDown(client, w.network, service, true); err != nil {
|
||||
log.Error("Failed to tear down component", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Clean up any references to it from out state
|
||||
services := w.services[server]
|
||||
for i, name := range services {
|
||||
if name == service {
|
||||
w.services[server] = append(services[:i], services[i+1:]...)
|
||||
if len(w.services[server]) == 0 {
|
||||
delete(w.services, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("Torn down existing component", "server", server, "service", service)
|
||||
return
|
||||
}
|
||||
// If the user requested deploying a new component, do it
|
||||
w.deployComponent()
|
||||
}
|
||||
|
||||
// deployComponent displays a list of network components the user can deploy and
|
||||
// guides through the process.
|
||||
func (w *wizard) deployComponent() {
|
||||
// Print all the things we can deploy and wait or user choice
|
||||
fmt.Println()
|
||||
fmt.Println("What would you like to deploy? (recommended order)")
|
||||
fmt.Println(" 1. Ethstats - Network monitoring tool")
|
||||
fmt.Println(" 2. Bootnode - Entry point of the network")
|
||||
fmt.Println(" 3. Sealer - Full node minting new blocks")
|
||||
fmt.Println(" 4. Wallet - Browser wallet for quick sends (todo)")
|
||||
fmt.Println(" 5. Faucet - Crypto faucet to give away funds")
|
||||
fmt.Println(" 6. Dashboard - Website listing above web-services")
|
||||
|
||||
switch w.read() {
|
||||
case "1":
|
||||
w.deployEthstats()
|
||||
case "2":
|
||||
w.deployNode(true)
|
||||
case "3":
|
||||
w.deployNode(false)
|
||||
case "4":
|
||||
case "5":
|
||||
w.deployFaucet()
|
||||
case "6":
|
||||
w.deployDashboard()
|
||||
default:
|
||||
log.Error("That's not something I can do")
|
||||
}
|
||||
}
|
58
cmd/puppeth/wizard_nginx.go
Normal file
58
cmd/puppeth/wizard_nginx.go
Normal file
@ -0,0 +1,58 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ensureVirtualHost checks whether a reverse-proxy is running on the specified
|
||||
// host machine, and if yes requests a virtual host from the user to host a
|
||||
// specific web service on. If no proxy exists, the method will offer to deploy
|
||||
// one.
|
||||
//
|
||||
// If the user elects not to use a reverse proxy, an empty hostname is returned!
|
||||
func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (string, error) {
|
||||
if proxy, _ := checkNginx(client, w.network); proxy != nil {
|
||||
// Reverse proxy is running, if ports match, we need a virtual host
|
||||
if proxy.port == port {
|
||||
fmt.Println()
|
||||
fmt.Printf("Shared port, which domain to assign? (default = %s)\n", def)
|
||||
return w.readDefaultString(def), nil
|
||||
}
|
||||
}
|
||||
// Reverse proxy is not running, offer to deploy a new one
|
||||
fmt.Println()
|
||||
fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)")
|
||||
if w.readDefaultString("y") == "y" {
|
||||
if out, err := deployNginx(client, w.network, port); err != nil {
|
||||
log.Error("Failed to deploy reverse-proxy", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
// Reverse proxy deployed, ask again for the virtual-host
|
||||
fmt.Println()
|
||||
fmt.Printf("Proxy deployed, which domain to assign? (default = %s)\n", def)
|
||||
return w.readDefaultString(def), nil
|
||||
}
|
||||
// Reverse proxy not requested, deploy as a standalone service
|
||||
return "", nil
|
||||
}
|
153
cmd/puppeth/wizard_node.go
Normal file
153
cmd/puppeth/wizard_node.go
Normal file
@ -0,0 +1,153 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployNode creates a new node configuration based on some user input.
|
||||
func (w *wizard) deployNode(boot bool) {
|
||||
// Do some sanity check before the user wastes time on input
|
||||
if w.conf.genesis == nil {
|
||||
log.Error("No genesis block configured")
|
||||
return
|
||||
}
|
||||
if w.conf.ethstats == "" {
|
||||
log.Error("No ethstats server configured")
|
||||
return
|
||||
}
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active ethstats configurations from the server
|
||||
infos, err := checkNode(client, w.network, boot)
|
||||
if err != nil {
|
||||
if boot {
|
||||
infos = &nodeInfos{portFull: 30303, peersTotal: 512, peersLight: 256}
|
||||
} else {
|
||||
infos = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0}
|
||||
}
|
||||
}
|
||||
infos.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ")
|
||||
infos.network = w.conf.genesis.Config.ChainId.Int64()
|
||||
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
if infos.datadir == "" {
|
||||
fmt.Printf("Where should data be stored on the remote machine?\n")
|
||||
infos.datadir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
|
||||
infos.datadir = w.readDefaultString(infos.datadir)
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull)
|
||||
infos.portFull = w.readDefaultInt(infos.portFull)
|
||||
|
||||
// Figure out how many peers to allow (different based on node type)
|
||||
fmt.Println()
|
||||
fmt.Printf("How many peers to allow connecting? (default = %d)\n", infos.peersTotal)
|
||||
infos.peersTotal = w.readDefaultInt(infos.peersTotal)
|
||||
|
||||
// Figure out how many light peers to allow (different based on node type)
|
||||
fmt.Println()
|
||||
fmt.Printf("How many light peers to allow connecting? (default = %d)\n", infos.peersLight)
|
||||
infos.peersLight = w.readDefaultInt(infos.peersLight)
|
||||
|
||||
// Set a proper name to report on the stats page
|
||||
fmt.Println()
|
||||
if infos.ethstats == "" {
|
||||
fmt.Printf("What should the node be called on the stats page?\n")
|
||||
infos.ethstats = w.readString() + ":" + w.conf.ethstats
|
||||
} else {
|
||||
fmt.Printf("What should the node be called on the stats page? (default = %s)\n", infos.ethstats)
|
||||
infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
|
||||
}
|
||||
// If the node is a miner/signer, load up needed credentials
|
||||
if !boot {
|
||||
if w.conf.genesis.Config.Ethash != nil {
|
||||
// Ethash based miners only need an etherbase to mine against
|
||||
fmt.Println()
|
||||
if infos.etherbase == "" {
|
||||
fmt.Printf("What address should the miner user?\n")
|
||||
for {
|
||||
if address := w.readAddress(); address != nil {
|
||||
infos.etherbase = address.Hex()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase)
|
||||
infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex()
|
||||
}
|
||||
} else if w.conf.genesis.Config.Clique != nil {
|
||||
// If a previous signer was already set, offer to reuse it
|
||||
if infos.keyJSON != "" {
|
||||
var key keystore.Key
|
||||
if err := json.Unmarshal([]byte(infos.keyJSON), &key); err != nil {
|
||||
infos.keyJSON, infos.keyPass = "", ""
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Printf("Reuse previous (%s) signing account (y/n)? (default = yes)\n", key.Address.Hex())
|
||||
if w.readDefaultString("y") != "y" {
|
||||
infos.keyJSON, infos.keyPass = "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clique based signers need a keyfile and unlock password, ask if unavailable
|
||||
if infos.keyJSON == "" {
|
||||
fmt.Println()
|
||||
fmt.Println("Please paste the signer's key JSON:")
|
||||
infos.keyJSON = w.readJSON()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("What's the unlock password for the account? (won't be echoed)")
|
||||
infos.keyPass = w.readPassword()
|
||||
|
||||
if _, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil {
|
||||
log.Error("Failed to decrypt key with given passphrase")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to deploy the full node on the host
|
||||
if out, err := deployNode(client, w.network, w.conf.bootFull, infos); err != nil {
|
||||
log.Error("Failed to deploy Ethereum node container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
log.Info("Waiting for node to finish booting")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
w.networkStats(false)
|
||||
}
|
Reference in New Issue
Block a user