cmd/puppeth: remove puppeth
This commit is contained in:
parent
bd6a05e1ee
commit
8ded6a9fcd
|
@ -43,7 +43,6 @@ directory.
|
||||||
| `bootnode` | Stripped down version of our Ethereum client implementation that only takes part in the network node discovery protocol, but does not run any of the higher level application protocols. It can be used as a lightweight bootstrap node to aid in finding peers in private networks. |
|
| `bootnode` | Stripped down version of our Ethereum client implementation that only takes part in the network node discovery protocol, but does not run any of the higher level application protocols. It can be used as a lightweight bootstrap node to aid in finding peers in private networks. |
|
||||||
| `evm` | Developer utility version of the EVM (Ethereum Virtual Machine) that is capable of running bytecode snippets within a configurable environment and execution mode. Its purpose is to allow isolated, fine-grained debugging of EVM opcodes (e.g. `evm --code 60ff60ff --debug run`). |
|
| `evm` | Developer utility version of the EVM (Ethereum Virtual Machine) that is capable of running bytecode snippets within a configurable environment and execution mode. Its purpose is to allow isolated, fine-grained debugging of EVM opcodes (e.g. `evm --code 60ff60ff --debug run`). |
|
||||||
| `rlpdump` | Developer utility tool to convert binary RLP ([Recursive Length Prefix](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp)) dumps (data encoding used by the Ethereum protocol both network as well as consensus wise) to user-friendlier hierarchical representation (e.g. `rlpdump --hex CE0183FFFFFFC4C304050583616263`). |
|
| `rlpdump` | Developer utility tool to convert binary RLP ([Recursive Length Prefix](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp)) dumps (data encoding used by the Ethereum protocol both network as well as consensus wise) to user-friendlier hierarchical representation (e.g. `rlpdump --hex CE0183FFFFFFC4C304050583616263`). |
|
||||||
| `puppeth` | a CLI wizard that aids in creating a new Ethereum network. |
|
|
||||||
|
|
||||||
## Running `geth`
|
## Running `geth`
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,6 @@ var (
|
||||||
executablePath("bootnode"),
|
executablePath("bootnode"),
|
||||||
executablePath("evm"),
|
executablePath("evm"),
|
||||||
executablePath("geth"),
|
executablePath("geth"),
|
||||||
executablePath("puppeth"),
|
|
||||||
executablePath("rlpdump"),
|
executablePath("rlpdump"),
|
||||||
executablePath("clef"),
|
executablePath("clef"),
|
||||||
}
|
}
|
||||||
|
@ -100,10 +99,6 @@ var (
|
||||||
BinaryName: "geth",
|
BinaryName: "geth",
|
||||||
Description: "Ethereum CLI client.",
|
Description: "Ethereum CLI client.",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
BinaryName: "puppeth",
|
|
||||||
Description: "Ethereum private network manager.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
BinaryName: "rlpdump",
|
BinaryName: "rlpdump",
|
||||||
Description: "Developer utility tool that prints RLP structures.",
|
Description: "Developer utility tool that prints RLP structures.",
|
||||||
|
|
|
@ -1,161 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEthName gets the Ethereum Name from ethstats
|
|
||||||
func getEthName(s string) string {
|
|
||||||
n := strings.Index(s, ":")
|
|
||||||
if n >= 0 {
|
|
||||||
return s[:n]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
|
@ -1,176 +0,0 @@
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend
|
|
||||||
// and associated monitoring site.
|
|
||||||
var ethstatsDockerfile = `
|
|
||||||
FROM puppeth/ethstats:latest
|
|
||||||
|
|
||||||
RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js
|
|
||||||
`
|
|
||||||
|
|
||||||
// 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
|
|
||||||
container_name: {{.Network}}_ethstats_1{{if not .VHost}}
|
|
||||||
ports:
|
|
||||||
- "{{.Port}}:3000"{{end}}
|
|
||||||
environment:
|
|
||||||
- WS_SECRET={{.Secret}}{{if .VHost}}
|
|
||||||
- VIRTUAL_HOST={{.VHost}}{{end}}{{if .Banned}}
|
|
||||||
- BANNED={{.Banned}}{{end}}
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "10"
|
|
||||||
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, banned []string, nocache bool) ([]byte, error) {
|
|
||||||
// Generate the content to upload to the server
|
|
||||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
|
||||||
files := make(map[string][]byte)
|
|
||||||
|
|
||||||
trustedLabels := make([]string, len(trusted))
|
|
||||||
for i, address := range trusted {
|
|
||||||
trustedLabels[i] = fmt.Sprintf("\"%s\"", address)
|
|
||||||
}
|
|
||||||
bannedLabels := make([]string, len(banned))
|
|
||||||
for i, address := range banned {
|
|
||||||
bannedLabels[i] = fmt.Sprintf("\"%s\"", address)
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerfile := new(bytes.Buffer)
|
|
||||||
template.Must(template.New("").Parse(ethstatsDockerfile)).Execute(dockerfile, map[string]interface{}{
|
|
||||||
"Trusted": strings.Join(trustedLabels, ", "),
|
|
||||||
"Banned": strings.Join(bannedLabels, ", "),
|
|
||||||
})
|
|
||||||
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,
|
|
||||||
"Banned": strings.Join(banned, ","),
|
|
||||||
})
|
|
||||||
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
|
|
||||||
if nocache {
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
|
||||||
}
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", 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
|
|
||||||
banned []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report converts the typed struct into a plain string->string map, containing
|
|
||||||
// most - but not all - fields for reporting to the user.
|
|
||||||
func (info *ethstatsInfos) Report() map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
"Website address": info.host,
|
|
||||||
"Website listener port": strconv.Itoa(info.port),
|
|
||||||
"Login secret": info.secret,
|
|
||||||
"Banned addresses": strings.Join(info.banned, "\n"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
// Retrieve the IP banned list
|
|
||||||
banned := strings.Split(infos.envvars["BANNED"], ",")
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
banned: banned,
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,194 +0,0 @@
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// explorerDockerfile is the Dockerfile required to run a block explorer.
|
|
||||||
var explorerDockerfile = `
|
|
||||||
FROM puppeth/blockscout:latest
|
|
||||||
|
|
||||||
ADD genesis.json /genesis.json
|
|
||||||
RUN \
|
|
||||||
echo 'geth --cache 512 init /genesis.json' > explorer.sh && \
|
|
||||||
echo $'geth --networkid {{.NetworkID}} --syncmode "full" --gcmode "archive" --port {{.EthPort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --http --http.api "net,web3,eth,debug,txpool" --http.corsdomain "*" --http.vhosts "*" --ws --ws.origins "*" --exitwhensynced' >> explorer.sh && \
|
|
||||||
echo $'exec geth --networkid {{.NetworkID}} --syncmode "full" --gcmode "archive" --port {{.EthPort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --http --http.api "net,web3,eth,debug,txpool" --http.corsdomain "*" --http.vhosts "*" --ws --ws.origins "*" &' >> explorer.sh && \
|
|
||||||
echo '/usr/local/bin/docker-entrypoint.sh postgres &' >> explorer.sh && \
|
|
||||||
echo 'sleep 5' >> explorer.sh && \
|
|
||||||
echo 'mix do ecto.drop --force, ecto.create, ecto.migrate' >> explorer.sh && \
|
|
||||||
echo 'mix phx.server' >> explorer.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "explorer.sh"]
|
|
||||||
`
|
|
||||||
|
|
||||||
// explorerComposefile is the docker-compose.yml file required to deploy and
|
|
||||||
// maintain a block explorer.
|
|
||||||
var explorerComposefile = `
|
|
||||||
version: '2'
|
|
||||||
services:
|
|
||||||
explorer:
|
|
||||||
build: .
|
|
||||||
image: {{.Network}}/explorer
|
|
||||||
container_name: {{.Network}}_explorer_1
|
|
||||||
ports:
|
|
||||||
- "{{.EthPort}}:{{.EthPort}}"
|
|
||||||
- "{{.EthPort}}:{{.EthPort}}/udp"{{if not .VHost}}
|
|
||||||
- "{{.WebPort}}:4000"{{end}}
|
|
||||||
environment:
|
|
||||||
- ETH_PORT={{.EthPort}}
|
|
||||||
- ETH_NAME={{.EthName}}
|
|
||||||
- BLOCK_TRANSFORMER={{.Transformer}}{{if .VHost}}
|
|
||||||
- VIRTUAL_HOST={{.VHost}}
|
|
||||||
- VIRTUAL_PORT=4000{{end}}
|
|
||||||
volumes:
|
|
||||||
- {{.Datadir}}:/opt/app/.ethereum
|
|
||||||
- {{.DBDir}}:/var/lib/postgresql/data
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "10"
|
|
||||||
restart: always
|
|
||||||
`
|
|
||||||
|
|
||||||
// deployExplorer deploys a new block explorer 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 deployExplorer(client *sshClient, network string, bootnodes []string, config *explorerInfos, nocache bool, isClique bool) ([]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(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{
|
|
||||||
"NetworkID": config.node.network,
|
|
||||||
"Bootnodes": strings.Join(bootnodes, ","),
|
|
||||||
"Ethstats": config.node.ethstats,
|
|
||||||
"EthPort": config.node.port,
|
|
||||||
})
|
|
||||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
|
||||||
|
|
||||||
transformer := "base"
|
|
||||||
if isClique {
|
|
||||||
transformer = "clique"
|
|
||||||
}
|
|
||||||
composefile := new(bytes.Buffer)
|
|
||||||
template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{
|
|
||||||
"Network": network,
|
|
||||||
"VHost": config.host,
|
|
||||||
"Ethstats": config.node.ethstats,
|
|
||||||
"Datadir": config.node.datadir,
|
|
||||||
"DBDir": config.dbdir,
|
|
||||||
"EthPort": config.node.port,
|
|
||||||
"EthName": getEthName(config.node.ethstats),
|
|
||||||
"WebPort": config.port,
|
|
||||||
"Transformer": transformer,
|
|
||||||
})
|
|
||||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
|
||||||
files[filepath.Join(workdir, "genesis.json")] = config.node.genesis
|
|
||||||
|
|
||||||
// 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 boot or seal node service
|
|
||||||
if nocache {
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
|
||||||
}
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
|
||||||
}
|
|
||||||
|
|
||||||
// explorerInfos is returned from a block explorer status check to allow reporting
|
|
||||||
// various configuration parameters.
|
|
||||||
type explorerInfos struct {
|
|
||||||
node *nodeInfos
|
|
||||||
dbdir string
|
|
||||||
host string
|
|
||||||
port int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report converts the typed struct into a plain string->string map, containing
|
|
||||||
// most - but not all - fields for reporting to the user.
|
|
||||||
func (info *explorerInfos) Report() map[string]string {
|
|
||||||
report := map[string]string{
|
|
||||||
"Website address ": info.host,
|
|
||||||
"Website listener port ": strconv.Itoa(info.port),
|
|
||||||
"Ethereum listener port ": strconv.Itoa(info.node.port),
|
|
||||||
"Ethstats username": info.node.ethstats,
|
|
||||||
}
|
|
||||||
return report
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkExplorer does a health-check against a block explorer server to verify
|
|
||||||
// whether it's running, and if yes, whether it's responsive.
|
|
||||||
func checkExplorer(client *sshClient, network string) (*explorerInfos, error) {
|
|
||||||
// Inspect a possible explorer container on the host
|
|
||||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_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["4000/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
|
|
||||||
}
|
|
||||||
// Run a sanity check to see if the devp2p is reachable
|
|
||||||
p2pPort := infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"]
|
|
||||||
if err = checkPort(host, p2pPort); err != nil {
|
|
||||||
log.Warn("Explorer node seems unreachable", "server", host, "port", p2pPort, "err", err)
|
|
||||||
}
|
|
||||||
if err = checkPort(host, port); err != nil {
|
|
||||||
log.Warn("Explorer service seems unreachable", "server", host, "port", port, "err", err)
|
|
||||||
}
|
|
||||||
// Assemble and return the useful infos
|
|
||||||
stats := &explorerInfos{
|
|
||||||
node: &nodeInfos{
|
|
||||||
datadir: infos.volumes["/opt/app/.ethereum"],
|
|
||||||
port: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"],
|
|
||||||
ethstats: infos.envvars["ETH_NAME"],
|
|
||||||
},
|
|
||||||
dbdir: infos.volumes["/var/lib/postgresql/data"],
|
|
||||||
host: host,
|
|
||||||
port: port,
|
|
||||||
}
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
|
@ -1,254 +0,0 @@
|
||||||
// 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"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"math/rand"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// faucetDockerfile is the Dockerfile required to build a faucet container to
|
|
||||||
// grant crypto tokens based on GitHub authentications.
|
|
||||||
var faucetDockerfile = `
|
|
||||||
FROM ethereum/client-go:alltools-latest
|
|
||||||
|
|
||||||
ADD genesis.json /genesis.json
|
|
||||||
ADD account.json /account.json
|
|
||||||
ADD account.pass /account.pass
|
|
||||||
|
|
||||||
EXPOSE 8080 30303 30303/udp
|
|
||||||
|
|
||||||
ENTRYPOINT [ \
|
|
||||||
"faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
|
|
||||||
"--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
|
|
||||||
"--account.json", "/account.json", "--account.pass", "/account.pass" \
|
|
||||||
{{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}}{{if .NoAuth}}, "--noauth"{{end}} \
|
|
||||||
{{if .TwitterToken}}, "--twitter.token.v1", "{{.TwitterToken}}"{{end}} \
|
|
||||||
]`
|
|
||||||
|
|
||||||
// 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
|
|
||||||
container_name: {{.Network}}_faucet_1
|
|
||||||
ports:
|
|
||||||
- "{{.EthPort}}:{{.EthPort}}"
|
|
||||||
- "{{.EthPort}}:{{.EthPort}}/udp"{{if not .VHost}}
|
|
||||||
- "{{.ApiPort}}:8080"{{end}}
|
|
||||||
volumes:
|
|
||||||
- {{.Datadir}}:/root/.faucet
|
|
||||||
environment:
|
|
||||||
- ETH_PORT={{.EthPort}}
|
|
||||||
- ETH_NAME={{.EthName}}
|
|
||||||
- FAUCET_AMOUNT={{.FaucetAmount}}
|
|
||||||
- FAUCET_MINUTES={{.FaucetMinutes}}
|
|
||||||
- FAUCET_TIERS={{.FaucetTiers}}
|
|
||||||
- CAPTCHA_TOKEN={{.CaptchaToken}}
|
|
||||||
- CAPTCHA_SECRET={{.CaptchaSecret}}
|
|
||||||
- TWITTER_TOKEN={{.TwitterToken}}
|
|
||||||
- NO_AUTH={{.NoAuth}}{{if .VHost}}
|
|
||||||
- VIRTUAL_HOST={{.VHost}}
|
|
||||||
- VIRTUAL_PORT=8080{{end}}
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "10"
|
|
||||||
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, nocache bool) ([]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.port,
|
|
||||||
"CaptchaToken": config.captchaToken,
|
|
||||||
"CaptchaSecret": config.captchaSecret,
|
|
||||||
"FaucetName": strings.Title(network),
|
|
||||||
"FaucetAmount": config.amount,
|
|
||||||
"FaucetMinutes": config.minutes,
|
|
||||||
"FaucetTiers": config.tiers,
|
|
||||||
"NoAuth": config.noauth,
|
|
||||||
"TwitterToken": config.twitterToken,
|
|
||||||
})
|
|
||||||
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.port,
|
|
||||||
"EthName": getEthName(config.node.ethstats),
|
|
||||||
"CaptchaToken": config.captchaToken,
|
|
||||||
"CaptchaSecret": config.captchaSecret,
|
|
||||||
"FaucetAmount": config.amount,
|
|
||||||
"FaucetMinutes": config.minutes,
|
|
||||||
"FaucetTiers": config.tiers,
|
|
||||||
"NoAuth": config.noauth,
|
|
||||||
"TwitterToken": config.twitterToken,
|
|
||||||
})
|
|
||||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
|
||||||
|
|
||||||
files[filepath.Join(workdir, "genesis.json")] = 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
|
|
||||||
if nocache {
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
|
||||||
}
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
|
||||||
}
|
|
||||||
|
|
||||||
// faucetInfos is returned from a faucet status check to allow reporting various
|
|
||||||
// configuration parameters.
|
|
||||||
type faucetInfos struct {
|
|
||||||
node *nodeInfos
|
|
||||||
host string
|
|
||||||
port int
|
|
||||||
amount int
|
|
||||||
minutes int
|
|
||||||
tiers int
|
|
||||||
noauth bool
|
|
||||||
captchaToken string
|
|
||||||
captchaSecret string
|
|
||||||
twitterToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report converts the typed struct into a plain string->string map, containing
|
|
||||||
// most - but not all - fields for reporting to the user.
|
|
||||||
func (info *faucetInfos) Report() map[string]string {
|
|
||||||
report := map[string]string{
|
|
||||||
"Website address": info.host,
|
|
||||||
"Website listener port": strconv.Itoa(info.port),
|
|
||||||
"Ethereum listener port": strconv.Itoa(info.node.port),
|
|
||||||
"Funding amount (base tier)": fmt.Sprintf("%d Ethers", info.amount),
|
|
||||||
"Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes),
|
|
||||||
"Funding tiers": strconv.Itoa(info.tiers),
|
|
||||||
"Captha protection": fmt.Sprintf("%v", info.captchaToken != ""),
|
|
||||||
"Using Twitter API": fmt.Sprintf("%v", info.twitterToken != ""),
|
|
||||||
"Ethstats username": info.node.ethstats,
|
|
||||||
}
|
|
||||||
if info.noauth {
|
|
||||||
report["Debug mode (no auth)"] = "enabled"
|
|
||||||
}
|
|
||||||
if info.node.keyJSON != "" {
|
|
||||||
var key struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil {
|
|
||||||
report["Funding account"] = common.HexToAddress(key.Address).Hex()
|
|
||||||
} else {
|
|
||||||
log.Error("Failed to retrieve signer address", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return report
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkFaucet does a health-check against a 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"])
|
|
||||||
tiers, _ := strconv.Atoi(infos.envvars["FAUCET_TIERS"])
|
|
||||||
|
|
||||||
// Retrieve the funding account information
|
|
||||||
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"],
|
|
||||||
port: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"],
|
|
||||||
ethstats: infos.envvars["ETH_NAME"],
|
|
||||||
keyJSON: keyJSON,
|
|
||||||
keyPass: keyPass,
|
|
||||||
},
|
|
||||||
host: host,
|
|
||||||
port: port,
|
|
||||||
amount: amount,
|
|
||||||
minutes: minutes,
|
|
||||||
tiers: tiers,
|
|
||||||
captchaToken: infos.envvars["CAPTCHA_TOKEN"],
|
|
||||||
captchaSecret: infos.envvars["CAPTCHA_SECRET"],
|
|
||||||
noauth: infos.envvars["NO_AUTH"] == "true",
|
|
||||||
twitterToken: infos.envvars["TWITTER_TOKEN"],
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
// 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"
|
|
||||||
|
|
||||||
"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
|
|
||||||
container_name: {{.Network}}_nginx_1
|
|
||||||
ports:
|
|
||||||
- "{{.Port}}:80"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "10"
|
|
||||||
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, nocache bool) ([]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 reverse-proxy service
|
|
||||||
if nocache {
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
|
||||||
}
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
|
|
||||||
}
|
|
||||||
|
|
||||||
// nginxInfos is returned from an nginx reverse-proxy status check to allow
|
|
||||||
// reporting various configuration parameters.
|
|
||||||
type nginxInfos struct {
|
|
||||||
port int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report converts the typed struct into a plain string->string map, containing
|
|
||||||
// most - but not all - fields for reporting to the user.
|
|
||||||
func (info *nginxInfos) Report() map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
"Shared listener port": strconv.Itoa(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
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
// 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"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nodeDockerfile is the Dockerfile required to run an Ethereum node.
|
|
||||||
var nodeDockerfile = `
|
|
||||||
FROM ethereum/client-go:latest
|
|
||||||
|
|
||||||
ADD genesis.json /genesis.json
|
|
||||||
{{if .Unlock}}
|
|
||||||
ADD signer.json /signer.json
|
|
||||||
ADD signer.pass /signer.pass
|
|
||||||
{{end}}
|
|
||||||
RUN \
|
|
||||||
echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}}
|
|
||||||
echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
|
|
||||||
echo $'exec geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --nat extip:{{.IP}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--miner.etherbase {{.Etherbase}} --mine --miner.threads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --miner.gaslimit {{.GasLimit}} --miner.gasprice {{.GasPrice}}' >> 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}}
|
|
||||||
container_name: {{.Network}}_{{.Type}}_1
|
|
||||||
ports:
|
|
||||||
- "{{.Port}}:{{.Port}}"
|
|
||||||
- "{{.Port}}:{{.Port}}/udp"
|
|
||||||
volumes:
|
|
||||||
- {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
|
|
||||||
- {{.Ethashdir}}:/root/.ethash{{end}}
|
|
||||||
environment:
|
|
||||||
- PORT={{.Port}}/tcp
|
|
||||||
- TOTAL_PEERS={{.TotalPeers}}
|
|
||||||
- LIGHT_PEERS={{.LightPeers}}
|
|
||||||
- STATS_NAME={{.Ethstats}}
|
|
||||||
- MINER_NAME={{.Etherbase}}
|
|
||||||
- GAS_LIMIT={{.GasLimit}}
|
|
||||||
- GAS_PRICE={{.GasPrice}}
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "10"
|
|
||||||
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, nocache bool) ([]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("--light.maxpeers=%d --light.serve=50", config.peersLight)
|
|
||||||
}
|
|
||||||
dockerfile := new(bytes.Buffer)
|
|
||||||
template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
|
|
||||||
"NetworkID": config.network,
|
|
||||||
"Port": config.port,
|
|
||||||
"IP": client.address,
|
|
||||||
"Peers": config.peersTotal,
|
|
||||||
"LightFlag": lightFlag,
|
|
||||||
"Bootnodes": strings.Join(bootnodes, ","),
|
|
||||||
"Ethstats": config.ethstats,
|
|
||||||
"Etherbase": config.etherbase,
|
|
||||||
"GasLimit": uint64(1000000 * config.gasLimit),
|
|
||||||
"GasPrice": uint64(1000000000 * config.gasPrice),
|
|
||||||
"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,
|
|
||||||
"Ethashdir": config.ethashdir,
|
|
||||||
"Network": network,
|
|
||||||
"Port": config.port,
|
|
||||||
"TotalPeers": config.peersTotal,
|
|
||||||
"Light": config.peersLight > 0,
|
|
||||||
"LightPeers": config.peersLight,
|
|
||||||
"Ethstats": getEthName(config.ethstats),
|
|
||||||
"Etherbase": config.etherbase,
|
|
||||||
"GasLimit": config.gasLimit,
|
|
||||||
"GasPrice": config.gasPrice,
|
|
||||||
})
|
|
||||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
|
||||||
|
|
||||||
files[filepath.Join(workdir, "genesis.json")] = 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 boot or seal node service
|
|
||||||
if nocache {
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
|
|
||||||
}
|
|
||||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", 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
|
|
||||||
ethashdir string
|
|
||||||
ethstats string
|
|
||||||
port int
|
|
||||||
enode string
|
|
||||||
peersTotal int
|
|
||||||
peersLight int
|
|
||||||
etherbase string
|
|
||||||
keyJSON string
|
|
||||||
keyPass string
|
|
||||||
gasLimit float64
|
|
||||||
gasPrice float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report converts the typed struct into a plain string->string map, containing
|
|
||||||
// most - but not all - fields for reporting to the user.
|
|
||||||
func (info *nodeInfos) Report() map[string]string {
|
|
||||||
report := map[string]string{
|
|
||||||
"Data directory": info.datadir,
|
|
||||||
"Listener port": strconv.Itoa(info.port),
|
|
||||||
"Peer count (all total)": strconv.Itoa(info.peersTotal),
|
|
||||||
"Peer count (light nodes)": strconv.Itoa(info.peersLight),
|
|
||||||
"Ethstats username": info.ethstats,
|
|
||||||
}
|
|
||||||
if info.gasLimit > 0 {
|
|
||||||
// Miner or signer node
|
|
||||||
report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
|
|
||||||
report["Gas ceil (target maximum)"] = fmt.Sprintf("%0.3f MGas", info.gasLimit)
|
|
||||||
|
|
||||||
if info.etherbase != "" {
|
|
||||||
// Ethash proof-of-work miner
|
|
||||||
report["Ethash directory"] = info.ethashdir
|
|
||||||
report["Miner account"] = info.etherbase
|
|
||||||
}
|
|
||||||
if info.keyJSON != "" {
|
|
||||||
// Clique proof-of-authority signer
|
|
||||||
var key struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
|
|
||||||
report["Signer account"] = common.HexToAddress(key.Address).Hex()
|
|
||||||
} else {
|
|
||||||
log.Error("Failed to retrieve signer address", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return report
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNode does a health-check against a 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"])
|
|
||||||
gasLimit, _ := strconv.ParseFloat(infos.envvars["GAS_LIMIT"], 64)
|
|
||||||
gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
|
|
||||||
|
|
||||||
// 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.enode --cache=16 attach", network, kind)); err != nil {
|
|
||||||
return nil, ErrServiceUnreachable
|
|
||||||
}
|
|
||||||
enode := 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["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"],
|
|
||||||
ethashdir: infos.volumes["/root/.ethash"],
|
|
||||||
port: port,
|
|
||||||
peersTotal: totalPeers,
|
|
||||||
peersLight: lightPeers,
|
|
||||||
ethstats: infos.envvars["STATS_NAME"],
|
|
||||||
etherbase: infos.envvars["MINER_NAME"],
|
|
||||||
keyJSON: keyJSON,
|
|
||||||
keyPass: keyPass,
|
|
||||||
gasLimit: gasLimit,
|
|
||||||
gasPrice: gasPrice,
|
|
||||||
}
|
|
||||||
stats.enode = string(enode)
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
// 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"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 (no spaces or hyphens, please)",
|
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "loglevel",
|
|
||||||
Value: 3,
|
|
||||||
Usage: "log level to emit to the screen",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
app.Before = 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())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
app.Action = runWizard
|
|
||||||
app.Run(os.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runWizard start the wizard and relinquish control to it.
|
|
||||||
func runWizard(c *cli.Context) error {
|
|
||||||
network := c.String("network")
|
|
||||||
if strings.Contains(network, " ") || strings.Contains(network, "-") || strings.ToLower(network) != network {
|
|
||||||
log.Crit("No spaces, hyphens or capital letters allowed in network name")
|
|
||||||
}
|
|
||||||
makeWizard(c.String("network")).run()
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,271 +0,0 @@
|
||||||
// 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"
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"golang.org/x/crypto/ssh/agent"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
pubkey []byte // RSA public key to authenticate the server
|
|
||||||
client *ssh.Client
|
|
||||||
logger log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnvSSHAuthSock = "SSH_AUTH_SOCK"
|
|
||||||
|
|
||||||
// dial establishes an SSH connection to a remote node using the current user and
|
|
||||||
// the user's configured private RSA key. If that fails, password authentication
|
|
||||||
// is fallen back to. server can be a string like user:identity@server:port.
|
|
||||||
func dial(server string, pubkey []byte) (*sshClient, error) {
|
|
||||||
// Figure out username, identity, hostname and port
|
|
||||||
hostname := ""
|
|
||||||
hostport := server
|
|
||||||
username := ""
|
|
||||||
identity := "id_rsa" // default
|
|
||||||
|
|
||||||
if strings.Contains(server, "@") {
|
|
||||||
prefix := server[:strings.Index(server, "@")]
|
|
||||||
if strings.Contains(prefix, ":") {
|
|
||||||
username = prefix[:strings.Index(prefix, ":")]
|
|
||||||
identity = prefix[strings.Index(prefix, ":")+1:]
|
|
||||||
} else {
|
|
||||||
username = prefix
|
|
||||||
}
|
|
||||||
hostport = server[strings.Index(server, "@")+1:]
|
|
||||||
}
|
|
||||||
if strings.Contains(hostport, ":") {
|
|
||||||
hostname = hostport[:strings.Index(hostport, ":")]
|
|
||||||
} else {
|
|
||||||
hostname = hostport
|
|
||||||
hostport += ":22"
|
|
||||||
}
|
|
||||||
logger := log.New("server", server)
|
|
||||||
logger.Debug("Attempting to establish SSH connection")
|
|
||||||
|
|
||||||
user, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if username == "" {
|
|
||||||
username = user.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the supported authentication methods (ssh agent, private key and password)
|
|
||||||
var (
|
|
||||||
auths []ssh.AuthMethod
|
|
||||||
conn net.Conn
|
|
||||||
)
|
|
||||||
if conn, err = net.Dial("unix", os.Getenv(EnvSSHAuthSock)); err != nil {
|
|
||||||
log.Warn("Unable to dial SSH agent, falling back to private keys", "err", err)
|
|
||||||
} else {
|
|
||||||
client := agent.NewClient(conn)
|
|
||||||
auths = append(auths, ssh.PublicKeysCallback(client.Signers))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
path := filepath.Join(user.HomeDir, ".ssh", identity)
|
|
||||||
if buf, err := os.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 {
|
|
||||||
fmt.Printf("What's the decryption password for %s? (won't be echoed)\n>", path)
|
|
||||||
blob, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
fmt.Println()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Couldn't read password", "err", err)
|
|
||||||
}
|
|
||||||
key, err := ssh.ParsePrivateKeyWithPassphrase(buf, blob)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to decrypt SSH key, falling back to passwords", "path", path, "err", err)
|
|
||||||
} else {
|
|
||||||
auths = append(auths, ssh.PublicKeys(key))
|
|
||||||
}
|
|
||||||
} 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> ", username, server)
|
|
||||||
blob, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
return string(blob), err
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
// Resolve the IP address of the remote server
|
|
||||||
addr, err := net.LookupHost(hostname)
|
|
||||||
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", username)
|
|
||||||
keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
|
||||||
// If no public key is known for SSH, ask the user to confirm
|
|
||||||
if pubkey == nil {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote)
|
|
||||||
fmt.Printf("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key))
|
|
||||||
fmt.Printf("Are you sure you want to continue connecting (yes/no)? ")
|
|
||||||
|
|
||||||
for {
|
|
||||||
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
return err
|
|
||||||
case strings.TrimSpace(text) == "yes":
|
|
||||||
pubkey = key.Marshal()
|
|
||||||
return nil
|
|
||||||
case strings.TrimSpace(text) == "no":
|
|
||||||
return errors.New("users says no")
|
|
||||||
default:
|
|
||||||
fmt.Println("Please answer 'yes' or 'no'")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If a public key exists for this SSH server, check that it matches
|
|
||||||
if bytes.Equal(pubkey, key.Marshal()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// We have a mismatch, forbid connecting
|
|
||||||
return errors.New("ssh key mismatch, re-add the machine to update")
|
|
||||||
}
|
|
||||||
client, err := ssh.Dial("tcp", hostport, &ssh.ClientConfig{User: username, Auth: auths, HostKeyCallback: keycheck})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Connection established, return our utility wrapper
|
|
||||||
c := &sshClient{
|
|
||||||
server: hostname,
|
|
||||||
address: addr[0],
|
|
||||||
pubkey: pubkey,
|
|
||||||
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 copies the set of files to a remote server via SCP, creating any non-
|
|
||||||
// existing folders in the 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 ./")
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
{
|
|
||||||
"sealEngine": "Ethash",
|
|
||||||
"params": {
|
|
||||||
"accountStartNonce": "0x0",
|
|
||||||
"maximumExtraDataSize": "0x20",
|
|
||||||
"homesteadForkBlock": "0x2710",
|
|
||||||
"daoHardforkBlock": "0x0",
|
|
||||||
"EIP150ForkBlock": "0x3a98",
|
|
||||||
"EIP158ForkBlock": "0x59d8",
|
|
||||||
"byzantiumForkBlock": "0x7530",
|
|
||||||
"constantinopleForkBlock": "0x9c40",
|
|
||||||
"constantinopleFixForkBlock": "0x9c40",
|
|
||||||
"istanbulForkBlock": "0xc350",
|
|
||||||
"minGasLimit": "0x1388",
|
|
||||||
"maxGasLimit": "0x7fffffffffffffff",
|
|
||||||
"tieBreakingGas": false,
|
|
||||||
"gasLimitBoundDivisor": "0x400",
|
|
||||||
"minimumDifficulty": "0x20000",
|
|
||||||
"difficultyBoundDivisor": "0x800",
|
|
||||||
"durationLimit": "0xd",
|
|
||||||
"blockReward": "0x4563918244f40000",
|
|
||||||
"networkID": "0x4cb2e",
|
|
||||||
"chainID": "0x4cb2e",
|
|
||||||
"allowFutureBlocks": false
|
|
||||||
},
|
|
||||||
"genesis": {
|
|
||||||
"nonce": "0x0000000000000000",
|
|
||||||
"difficulty": "0x20000",
|
|
||||||
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"author": "0x0000000000000000000000000000000000000000",
|
|
||||||
"timestamp": "0x59a4e76d",
|
|
||||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"extraData": "0x0000000000000000000000000000000000000000000000000000000b4dc0ffee",
|
|
||||||
"gasLimit": "0x47b760"
|
|
||||||
},
|
|
||||||
"accounts": {
|
|
||||||
"0000000000000000000000000000000000000001": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "ecrecover",
|
|
||||||
"linear": {
|
|
||||||
"base": 3000,
|
|
||||||
"word": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000002": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "sha256",
|
|
||||||
"linear": {
|
|
||||||
"base": 60,
|
|
||||||
"word": 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000003": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "ripemd160",
|
|
||||||
"linear": {
|
|
||||||
"base": 600,
|
|
||||||
"word": 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000004": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "identity",
|
|
||||||
"linear": {
|
|
||||||
"base": 15,
|
|
||||||
"word": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000005": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "modexp",
|
|
||||||
"startingBlock": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000006": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "alt_bn128_G1_add",
|
|
||||||
"startingBlock": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000007": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "alt_bn128_G1_mul",
|
|
||||||
"startingBlock": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000008": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "alt_bn128_pairing_product",
|
|
||||||
"startingBlock": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000009": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"precompiled": {
|
|
||||||
"name": "blake2_compression",
|
|
||||||
"startingBlock": "0xc350"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"chainId": 314158,
|
|
||||||
"homesteadBlock": 10000,
|
|
||||||
"eip150Block": 15000,
|
|
||||||
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"eip155Block": 23000,
|
|
||||||
"eip158Block": 23000,
|
|
||||||
"byzantiumBlock": 30000,
|
|
||||||
"constantinopleBlock": 40000,
|
|
||||||
"petersburgBlock": 40000,
|
|
||||||
"istanbulBlock": 50000,
|
|
||||||
"ethash": {}
|
|
||||||
},
|
|
||||||
"nonce": "0x0",
|
|
||||||
"timestamp": "0x59a4e76d",
|
|
||||||
"extraData": "0x0000000000000000000000000000000000000000000000000000000b4dc0ffee",
|
|
||||||
"gasLimit": "0x47b760",
|
|
||||||
"difficulty": "0x20000",
|
|
||||||
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"coinbase": "0x0000000000000000000000000000000000000000",
|
|
||||||
"alloc": {
|
|
||||||
"0000000000000000000000000000000000000001": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000002": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000003": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000004": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000005": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000006": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000007": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000008": {
|
|
||||||
"balance": "0x1"
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000009": {
|
|
||||||
"balance": "0x1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"number": "0x0",
|
|
||||||
"gasUsed": "0x0",
|
|
||||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
}
|
|
|
@ -1,213 +0,0 @@
|
||||||
{
|
|
||||||
"name": "stureby",
|
|
||||||
"dataDir": "stureby",
|
|
||||||
"engine": {
|
|
||||||
"Ethash": {
|
|
||||||
"params": {
|
|
||||||
"minimumDifficulty": "0x20000",
|
|
||||||
"difficultyBoundDivisor": "0x800",
|
|
||||||
"durationLimit": "0xd",
|
|
||||||
"blockReward": {
|
|
||||||
"0x0": "0x4563918244f40000",
|
|
||||||
"0x7530": "0x29a2241af62c0000",
|
|
||||||
"0x9c40": "0x1bc16d674ec80000"
|
|
||||||
},
|
|
||||||
"difficultyBombDelays": {
|
|
||||||
"0x7530": "0x2dc6c0",
|
|
||||||
"0x9c40": "0x1e8480"
|
|
||||||
},
|
|
||||||
"homesteadTransition": "0x2710",
|
|
||||||
"eip100bTransition": "0x7530"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"params": {
|
|
||||||
"accountStartNonce": "0x0",
|
|
||||||
"maximumExtraDataSize": "0x20",
|
|
||||||
"minGasLimit": "0x1388",
|
|
||||||
"gasLimitBoundDivisor": "0x400",
|
|
||||||
"networkID": "0x4cb2e",
|
|
||||||
"chainID": "0x4cb2e",
|
|
||||||
"maxCodeSize": "0x6000",
|
|
||||||
"maxCodeSizeTransition": "0x0",
|
|
||||||
"eip98Transition": "0x7fffffffffffffff",
|
|
||||||
"eip150Transition": "0x3a98",
|
|
||||||
"eip160Transition": "0x59d8",
|
|
||||||
"eip161abcTransition": "0x59d8",
|
|
||||||
"eip161dTransition": "0x59d8",
|
|
||||||
"eip155Transition": "0x59d8",
|
|
||||||
"eip140Transition": "0x7530",
|
|
||||||
"eip211Transition": "0x7530",
|
|
||||||
"eip214Transition": "0x7530",
|
|
||||||
"eip658Transition": "0x7530",
|
|
||||||
"eip145Transition": "0x9c40",
|
|
||||||
"eip1014Transition": "0x9c40",
|
|
||||||
"eip1052Transition": "0x9c40",
|
|
||||||
"eip1283Transition": "0x9c40",
|
|
||||||
"eip1283DisableTransition": "0x9c40",
|
|
||||||
"eip1283ReenableTransition": "0xc350",
|
|
||||||
"eip1344Transition": "0xc350",
|
|
||||||
"eip1884Transition": "0xc350",
|
|
||||||
"eip2028Transition": "0xc350"
|
|
||||||
},
|
|
||||||
"genesis": {
|
|
||||||
"seal": {
|
|
||||||
"ethereum": {
|
|
||||||
"nonce": "0x0000000000000000",
|
|
||||||
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"difficulty": "0x20000",
|
|
||||||
"author": "0x0000000000000000000000000000000000000000",
|
|
||||||
"timestamp": "0x59a4e76d",
|
|
||||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"extraData": "0x0000000000000000000000000000000000000000000000000000000b4dc0ffee",
|
|
||||||
"gasLimit": "0x47b760"
|
|
||||||
},
|
|
||||||
"nodes": [],
|
|
||||||
"accounts": {
|
|
||||||
"0000000000000000000000000000000000000001": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "ecrecover",
|
|
||||||
"pricing": {
|
|
||||||
"linear": {
|
|
||||||
"base": 3000,
|
|
||||||
"word": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000002": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "sha256",
|
|
||||||
"pricing": {
|
|
||||||
"linear": {
|
|
||||||
"base": 60,
|
|
||||||
"word": 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000003": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "ripemd160",
|
|
||||||
"pricing": {
|
|
||||||
"linear": {
|
|
||||||
"base": 600,
|
|
||||||
"word": 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000004": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "identity",
|
|
||||||
"pricing": {
|
|
||||||
"linear": {
|
|
||||||
"base": 15,
|
|
||||||
"word": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000005": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "modexp",
|
|
||||||
"pricing": {
|
|
||||||
"modexp": {
|
|
||||||
"divisor": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activate_at": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000006": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "alt_bn128_add",
|
|
||||||
"pricing": {
|
|
||||||
"0x0": {
|
|
||||||
"price": {
|
|
||||||
"alt_bn128_const_operations": {
|
|
||||||
"price": 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0xc350": {
|
|
||||||
"price": {
|
|
||||||
"alt_bn128_const_operations": {
|
|
||||||
"price": 150
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activate_at": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000007": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "alt_bn128_mul",
|
|
||||||
"pricing": {
|
|
||||||
"0x0": {
|
|
||||||
"price": {
|
|
||||||
"alt_bn128_const_operations": {
|
|
||||||
"price": 40000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0xc350": {
|
|
||||||
"price": {
|
|
||||||
"alt_bn128_const_operations": {
|
|
||||||
"price": 6000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activate_at": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000008": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "alt_bn128_pairing",
|
|
||||||
"pricing": {
|
|
||||||
"0x0": {
|
|
||||||
"price": {
|
|
||||||
"alt_bn128_pairing": {
|
|
||||||
"base": 100000,
|
|
||||||
"pair": 80000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0xc350": {
|
|
||||||
"price": {
|
|
||||||
"alt_bn128_pairing": {
|
|
||||||
"base": 45000,
|
|
||||||
"pair": 34000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activate_at": "0x7530"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"0000000000000000000000000000000000000009": {
|
|
||||||
"balance": "0x1",
|
|
||||||
"builtin": {
|
|
||||||
"name": "blake2_f",
|
|
||||||
"pricing": {
|
|
||||||
"blake2_f": {
|
|
||||||
"gas_per_round": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activate_at": "0xc350"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,312 +0,0 @@
|
||||||
// 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"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/console/prompt"
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/peterh/liner"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
// config contains all the configurations needed by puppeth that should be saved
|
|
||||||
// between sessions.
|
|
||||||
type config struct {
|
|
||||||
path string // File containing the configuration values
|
|
||||||
bootnodes []string // Bootnodes to always connect to by all nodes
|
|
||||||
ethstats string // Ethstats settings to cache for node deploys
|
|
||||||
|
|
||||||
Genesis *core.Genesis `json:"genesis,omitempty"` // Genesis block to cache for node deploys
|
|
||||||
Servers map[string][]byte `json:"servers,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// servers retrieves an alphabetically sorted list of servers.
|
|
||||||
func (c config) servers() []string {
|
|
||||||
servers := make([]string, 0, len(c.Servers))
|
|
||||||
for server := range c.Servers {
|
|
||||||
servers = append(servers, server)
|
|
||||||
}
|
|
||||||
sort.Strings(servers)
|
|
||||||
|
|
||||||
return servers
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush dumps the contents of config to disk.
|
|
||||||
func (c config) flush() {
|
|
||||||
os.MkdirAll(filepath.Dir(c.path), 0755)
|
|
||||||
|
|
||||||
out, _ := json.MarshalIndent(c, "", " ")
|
|
||||||
if err := os.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
|
|
||||||
|
|
||||||
lock sync.Mutex // Lock to protect configs during concurrent service discovery
|
|
||||||
}
|
|
||||||
|
|
||||||
// prompts the user for input with the given prompt string. Returns when a value is entered.
|
|
||||||
// Causes the wizard to exit if ctrl-d is pressed
|
|
||||||
func promptInput(p string) string {
|
|
||||||
for {
|
|
||||||
text, err := prompt.Stdin.PromptInput(p)
|
|
||||||
if err != nil {
|
|
||||||
if err != liner.ErrPromptAborted {
|
|
||||||
log.Crit("Failed to read user input", "err", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read reads a single line from stdin, trimming if from spaces.
|
|
||||||
func (w *wizard) read() string {
|
|
||||||
text := promptInput("> ")
|
|
||||||
return strings.TrimSpace(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readString reads a single line from stdin, trimming if from spaces, enforcing
|
|
||||||
// non-emptyness.
|
|
||||||
func (w *wizard) readString() string {
|
|
||||||
for {
|
|
||||||
text := promptInput("> ")
|
|
||||||
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 {
|
|
||||||
text := promptInput("> ")
|
|
||||||
if text = strings.TrimSpace(text); text != "" {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
// readDefaultYesNo reads a single line from stdin, trimming if from spaces and
|
|
||||||
// interpreting it as a 'yes' or a 'no'. If an empty line is entered, the default
|
|
||||||
// value is returned.
|
|
||||||
func (w *wizard) readDefaultYesNo(def bool) bool {
|
|
||||||
for {
|
|
||||||
text := promptInput("> ")
|
|
||||||
if text = strings.ToLower(strings.TrimSpace(text)); text == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
if text == "y" || text == "yes" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if text == "n" || text == "no" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.Error("Invalid input, expected 'y', 'yes', 'n', 'no' or empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readURL reads a single line from stdin, trimming if from spaces and trying to
|
|
||||||
// interpret it as a URL (http, https or file).
|
|
||||||
func (w *wizard) readURL() *url.URL {
|
|
||||||
for {
|
|
||||||
text := promptInput("> ")
|
|
||||||
uri, err := url.Parse(strings.TrimSpace(text))
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Invalid input, expected URL", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readInt reads a single line from stdin, trimming if from spaces, enforcing it
|
|
||||||
// to parse into an integer.
|
|
||||||
func (w *wizard) readInt() int {
|
|
||||||
for {
|
|
||||||
text := promptInput("> ")
|
|
||||||
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 {
|
|
||||||
text := promptInput("> ")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readDefaultBigInt reads a single line from stdin, trimming if from spaces,
|
|
||||||
// enforcing it to parse into a big integer. If an empty line is entered, the
|
|
||||||
// default value is returned.
|
|
||||||
func (w *wizard) readDefaultBigInt(def *big.Int) *big.Int {
|
|
||||||
for {
|
|
||||||
text := promptInput("> ")
|
|
||||||
if text = strings.TrimSpace(text); text == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
val, ok := new(big.Int).SetString(text, 0)
|
|
||||||
if !ok {
|
|
||||||
log.Error("Invalid input, expected big integer")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readDefaultFloat reads a single line from stdin, trimming if from spaces, enforcing
|
|
||||||
// it to parse into a float. If an empty line is entered, the default value is returned.
|
|
||||||
func (w *wizard) readDefaultFloat(def float64) float64 {
|
|
||||||
for {
|
|
||||||
text := promptInput("> ")
|
|
||||||
if text = strings.TrimSpace(text); text == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
val, err := strconv.ParseFloat(strings.TrimSpace(text), 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Invalid input, expected float", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
fmt.Printf("> ")
|
|
||||||
text, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
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 {
|
|
||||||
text := promptInput("> 0x")
|
|
||||||
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
|
|
||||||
text := promptInput("> 0x")
|
|
||||||
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 {
|
|
||||||
text := promptInput("> ")
|
|
||||||
reader := strings.NewReader(text)
|
|
||||||
if err := json.NewDecoder(reader).Decode(&blob); err != nil {
|
|
||||||
log.Error("Invalid JSON, please try again", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return string(blob)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readIPAddress reads a single line from stdin, trimming if from spaces and
|
|
||||||
// returning it if it's convertible to an IP address. The reason for keeping
|
|
||||||
// the user input format instead of returning a Go net.IP is to match with
|
|
||||||
// weird formats used by ethstats, which compares IPs textually, not by value.
|
|
||||||
func (w *wizard) readIPAddress() string {
|
|
||||||
for {
|
|
||||||
// Read the IP address from the user
|
|
||||||
fmt.Printf("> ")
|
|
||||||
text := promptInput("> ")
|
|
||||||
if text = strings.TrimSpace(text); text == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// Make sure it looks ok and return it if so
|
|
||||||
if ip := net.ParseIP(text); ip == nil {
|
|
||||||
log.Error("Invalid IP address, please retry")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
// 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existed := err == nil
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, service := range []string{"ethstats", "explorer", "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 "explorer":
|
|
||||||
if infos, err := checkExplorer(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Prompt 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
|
|
||||||
}
|
|
||||||
var page string
|
|
||||||
switch {
|
|
||||||
case choice <= len(pages):
|
|
||||||
page = pages[choice-1]
|
|
||||||
case choice == len(pages)+1:
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which address is the external %s service at?\n", service)
|
|
||||||
page = w.readString()
|
|
||||||
default:
|
|
||||||
// No service hosting for this
|
|
||||||
}
|
|
||||||
// Save the users choice
|
|
||||||
switch service {
|
|
||||||
case "ethstats":
|
|
||||||
infos.ethstats = page
|
|
||||||
case "explorer":
|
|
||||||
infos.explorer = page
|
|
||||||
case "faucet":
|
|
||||||
infos.faucet = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we have ethstats running, ask whether to make the secret public or not
|
|
||||||
if w.conf.ethstats != "" {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)")
|
|
||||||
infos.trusted = w.readDefaultYesNo(true)
|
|
||||||
}
|
|
||||||
// Try to deploy the dashboard container on the host
|
|
||||||
nocache := false
|
|
||||||
if existed {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Should the dashboard be built from scratch (y/n)? (default = no)\n")
|
|
||||||
nocache = w.readDefaultYesNo(false)
|
|
||||||
}
|
|
||||||
if out, err := deployDashboard(client, w.network, &w.conf, infos, nocache); 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()
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
// 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"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"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: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existed := err == nil
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
// Gather any banned lists to ban from reporting
|
|
||||||
if existed {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Keep existing IP %v in the banned list (y/n)? (default = yes)\n", infos.banned)
|
|
||||||
if !w.readDefaultYesNo(true) {
|
|
||||||
// The user might want to clear the entire list, although generally probably not
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Clear out the banned list and start over (y/n)? (default = no)\n")
|
|
||||||
if w.readDefaultYesNo(false) {
|
|
||||||
infos.banned = nil
|
|
||||||
}
|
|
||||||
// Offer the user to explicitly add/remove certain IP addresses
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Which additional IP addresses should be in the banned list?")
|
|
||||||
for {
|
|
||||||
if ip := w.readIPAddress(); ip != "" {
|
|
||||||
infos.banned = append(infos.banned, ip)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Which IP addresses should not be in the banned list?")
|
|
||||||
for {
|
|
||||||
if ip := w.readIPAddress(); ip != "" {
|
|
||||||
for i, addr := range infos.banned {
|
|
||||||
if ip == addr {
|
|
||||||
infos.banned = append(infos.banned[:i], infos.banned[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sort.Strings(infos.banned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Try to deploy the ethstats server on the host
|
|
||||||
nocache := false
|
|
||||||
if existed {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Should the ethstats be built from scratch (y/n)? (default = no)\n")
|
|
||||||
nocache = w.readDefaultYesNo(false)
|
|
||||||
}
|
|
||||||
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, infos.banned, nocache); 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()
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
// 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/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// deployExplorer creates a new block explorer based on some user input.
|
|
||||||
func (w *wizard) deployExplorer() {
|
|
||||||
// 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 node configurations from the server
|
|
||||||
infos, err := checkExplorer(client, w.network)
|
|
||||||
if err != nil {
|
|
||||||
infos = &explorerInfos{
|
|
||||||
node: &nodeInfos{port: 30303},
|
|
||||||
port: 80,
|
|
||||||
host: client.server,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existed := err == nil
|
|
||||||
|
|
||||||
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 explorer 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 explorer host", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Figure out where the user wants to store the persistent data
|
|
||||||
fmt.Println()
|
|
||||||
if infos.node.datadir == "" {
|
|
||||||
fmt.Printf("Where should node data be stored on the remote machine?\n")
|
|
||||||
infos.node.datadir = w.readString()
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Where should node data be stored on the remote machine? (default = %s)\n", infos.node.datadir)
|
|
||||||
infos.node.datadir = w.readDefaultString(infos.node.datadir)
|
|
||||||
}
|
|
||||||
// Figure out where the user wants to store the persistent data for backend database
|
|
||||||
fmt.Println()
|
|
||||||
if infos.dbdir == "" {
|
|
||||||
fmt.Printf("Where should postgres data be stored on the remote machine?\n")
|
|
||||||
infos.dbdir = w.readString()
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Where should postgres data be stored on the remote machine? (default = %s)\n", infos.dbdir)
|
|
||||||
infos.dbdir = w.readDefaultString(infos.dbdir)
|
|
||||||
}
|
|
||||||
// Figure out which port to listen on
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which TCP/UDP port should the archive node listen on? (default = %d)\n", infos.node.port)
|
|
||||||
infos.node.port = w.readDefaultInt(infos.node.port)
|
|
||||||
|
|
||||||
// Set a proper name to report on the stats page
|
|
||||||
fmt.Println()
|
|
||||||
if infos.node.ethstats == "" {
|
|
||||||
fmt.Printf("What should the explorer be called on the stats page?\n")
|
|
||||||
infos.node.ethstats = w.readString() + ":" + w.conf.ethstats
|
|
||||||
} else {
|
|
||||||
fmt.Printf("What should the explorer be called on the stats page? (default = %s)\n", infos.node.ethstats)
|
|
||||||
infos.node.ethstats = w.readDefaultString(infos.node.ethstats) + ":" + w.conf.ethstats
|
|
||||||
}
|
|
||||||
// Try to deploy the explorer on the host
|
|
||||||
nocache := false
|
|
||||||
if existed {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Should the explorer be built from scratch (y/n)? (default = no)\n")
|
|
||||||
nocache = w.readDefaultYesNo(false)
|
|
||||||
}
|
|
||||||
if out, err := deployExplorer(client, w.network, w.conf.bootnodes, infos, nocache, w.conf.Genesis.Config.Clique != nil); err != nil {
|
|
||||||
log.Error("Failed to deploy explorer 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()
|
|
||||||
}
|
|
|
@ -1,195 +0,0 @@
|
||||||
// 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"
|
|
||||||
|
|
||||||
"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{port: 30303, peersTotal: 25},
|
|
||||||
port: 80,
|
|
||||||
host: client.server,
|
|
||||||
amount: 1,
|
|
||||||
minutes: 1440,
|
|
||||||
tiers: 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existed := err == nil
|
|
||||||
|
|
||||||
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 funding amount per period 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)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("How many funding tiers to feature (x2.5 amounts, x3 timeout)? (default = %d)\n", infos.tiers)
|
|
||||||
infos.tiers = w.readDefaultInt(infos.tiers)
|
|
||||||
if infos.tiers == 0 {
|
|
||||||
log.Error("At least one funding tier must be set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Accessing the reCaptcha service requires API authorizations, request it
|
|
||||||
if infos.captchaToken != "" {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Reuse previous reCaptcha API authorization (y/n)? (default = yes)")
|
|
||||||
if !w.readDefaultYesNo(true) {
|
|
||||||
infos.captchaToken, infos.captchaSecret = "", ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if infos.captchaToken == "" {
|
|
||||||
// No previous authorization (or old one discarded)
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Enable reCaptcha protection against robots (y/n)? (default = no)")
|
|
||||||
if !w.readDefaultYesNo(false) {
|
|
||||||
log.Warn("Users will be able to requests funds via automated scripts")
|
|
||||||
} else {
|
|
||||||
// Captcha protection explicitly requested, read the site and secret keys
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("What is the reCaptcha site key to authenticate human users?\n")
|
|
||||||
infos.captchaToken = w.readString()
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("What is the reCaptcha secret key to verify authentications? (won't be echoed)\n")
|
|
||||||
infos.captchaSecret = w.readPassword()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Accessing the Twitter API requires a bearer token, request it
|
|
||||||
if infos.twitterToken != "" {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Reuse previous Twitter API token (y/n)? (default = yes)")
|
|
||||||
if !w.readDefaultYesNo(true) {
|
|
||||||
infos.twitterToken = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if infos.twitterToken == "" {
|
|
||||||
// No previous twitter token (or old one discarded)
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("What is the Twitter API app Bearer token?\n")
|
|
||||||
infos.twitterToken = w.readString()
|
|
||||||
}
|
|
||||||
// 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.port)
|
|
||||||
infos.node.port = w.readDefaultInt(infos.node.port)
|
|
||||||
|
|
||||||
// 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 != "" {
|
|
||||||
if key, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); 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.readDefaultYesNo(true) {
|
|
||||||
infos.node.keyJSON, infos.node.keyPass = "", ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < 3 && infos.node.keyJSON == ""; i++ {
|
|
||||||
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 password")
|
|
||||||
infos.node.keyJSON = ""
|
|
||||||
infos.node.keyPass = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check if the user wants to run the faucet in debug mode (noauth)
|
|
||||||
noauth := "n"
|
|
||||||
if infos.noauth {
|
|
||||||
noauth = "y"
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Permit non-authenticated funding requests (y/n)? (default = %v)\n", infos.noauth)
|
|
||||||
infos.noauth = w.readDefaultString(noauth) != "n"
|
|
||||||
|
|
||||||
// Try to deploy the faucet server on the host
|
|
||||||
nocache := false
|
|
||||||
if existed {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Should the faucet be built from scratch (y/n)? (default = no)\n")
|
|
||||||
nocache = w.readDefaultYesNo(false)
|
|
||||||
}
|
|
||||||
if out, err := deployFaucet(client, w.network, w.conf.bootnodes, infos, nocache); 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()
|
|
||||||
}
|
|
|
@ -1,285 +0,0 @@
|
||||||
// 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"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/big"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"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(524288),
|
|
||||||
Alloc: make(core.GenesisAlloc),
|
|
||||||
Config: ¶ms.ChainConfig{
|
|
||||||
HomesteadBlock: big.NewInt(0),
|
|
||||||
EIP150Block: big.NewInt(0),
|
|
||||||
EIP155Block: big.NewInt(0),
|
|
||||||
EIP158Block: big.NewInt(0),
|
|
||||||
ByzantiumBlock: big.NewInt(0),
|
|
||||||
ConstantinopleBlock: big.NewInt(0),
|
|
||||||
PetersburgBlock: big.NewInt(0),
|
|
||||||
IstanbulBlock: big.NewInt(0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Should the precompile-addresses (0x1 .. 0xff) be pre-funded with 1 wei? (advisable yes)")
|
|
||||||
if w.readDefaultYesNo(true) {
|
|
||||||
// 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)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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 = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536))))
|
|
||||||
|
|
||||||
// All done, store the genesis and flush to disk
|
|
||||||
log.Info("Configured new genesis block")
|
|
||||||
|
|
||||||
w.conf.Genesis = genesis
|
|
||||||
w.conf.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// importGenesis imports a Geth genesis spec into puppeth.
|
|
||||||
func (w *wizard) importGenesis() {
|
|
||||||
// Request the genesis JSON spec URL from the user
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Where's the genesis file? (local file or http/https url)")
|
|
||||||
url := w.readURL()
|
|
||||||
|
|
||||||
// Convert the various allowed URLs to a reader stream
|
|
||||||
var reader io.Reader
|
|
||||||
|
|
||||||
switch url.Scheme {
|
|
||||||
case "http", "https":
|
|
||||||
// Remote web URL, retrieve it via an HTTP client
|
|
||||||
res, err := http.Get(url.String())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to retrieve remote genesis", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
reader = res.Body
|
|
||||||
|
|
||||||
case "":
|
|
||||||
// Schemaless URL, interpret as a local file
|
|
||||||
file, err := os.Open(url.String())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to open local genesis", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
reader = file
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Error("Unsupported genesis URL scheme", "scheme", url.Scheme)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Parse the genesis file and inject it successful
|
|
||||||
var genesis core.Genesis
|
|
||||||
if err := json.NewDecoder(reader).Decode(&genesis); err != nil {
|
|
||||||
log.Error("Invalid genesis spec", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Imported genesis block")
|
|
||||||
|
|
||||||
w.conf.Genesis = &genesis
|
|
||||||
w.conf.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// manageGenesis permits the modification of chain configuration parameters in
|
|
||||||
// a genesis config and the export of the entire genesis spec.
|
|
||||||
func (w *wizard) manageGenesis() {
|
|
||||||
// Figure out whether to modify or export the genesis
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println(" 1. Modify existing configurations")
|
|
||||||
fmt.Println(" 2. Export genesis configurations")
|
|
||||||
fmt.Println(" 3. Remove genesis configuration")
|
|
||||||
|
|
||||||
choice := w.read()
|
|
||||||
switch choice {
|
|
||||||
case "1":
|
|
||||||
// Fork rule updating requested, iterate over each fork
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.Genesis.Config.HomesteadBlock)
|
|
||||||
w.conf.Genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.Genesis.Config.HomesteadBlock)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should EIP150 (Tangerine Whistle) come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP150Block)
|
|
||||||
w.conf.Genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP150Block)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should EIP155 (Spurious Dragon) come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP155Block)
|
|
||||||
w.conf.Genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP155Block)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should EIP158/161 (also Spurious Dragon) come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP158Block)
|
|
||||||
w.conf.Genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP158Block)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.Genesis.Config.ByzantiumBlock)
|
|
||||||
w.conf.Genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.Genesis.Config.ByzantiumBlock)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should Constantinople come into effect? (default = %v)\n", w.conf.Genesis.Config.ConstantinopleBlock)
|
|
||||||
w.conf.Genesis.Config.ConstantinopleBlock = w.readDefaultBigInt(w.conf.Genesis.Config.ConstantinopleBlock)
|
|
||||||
if w.conf.Genesis.Config.PetersburgBlock == nil {
|
|
||||||
w.conf.Genesis.Config.PetersburgBlock = w.conf.Genesis.Config.ConstantinopleBlock
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should Petersburg come into effect? (default = %v)\n", w.conf.Genesis.Config.PetersburgBlock)
|
|
||||||
w.conf.Genesis.Config.PetersburgBlock = w.readDefaultBigInt(w.conf.Genesis.Config.PetersburgBlock)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should Istanbul come into effect? (default = %v)\n", w.conf.Genesis.Config.IstanbulBlock)
|
|
||||||
w.conf.Genesis.Config.IstanbulBlock = w.readDefaultBigInt(w.conf.Genesis.Config.IstanbulBlock)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should Berlin come into effect? (default = %v)\n", w.conf.Genesis.Config.BerlinBlock)
|
|
||||||
w.conf.Genesis.Config.BerlinBlock = w.readDefaultBigInt(w.conf.Genesis.Config.BerlinBlock)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which block should London come into effect? (default = %v)\n", w.conf.Genesis.Config.LondonBlock)
|
|
||||||
w.conf.Genesis.Config.LondonBlock = w.readDefaultBigInt(w.conf.Genesis.Config.LondonBlock)
|
|
||||||
|
|
||||||
out, _ := json.MarshalIndent(w.conf.Genesis.Config, "", " ")
|
|
||||||
fmt.Printf("Chain configuration updated:\n\n%s\n", out)
|
|
||||||
|
|
||||||
w.conf.flush()
|
|
||||||
|
|
||||||
case "2":
|
|
||||||
// Save whatever genesis configuration we currently have
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which folder to save the genesis spec into? (default = current)\n")
|
|
||||||
fmt.Printf(" Will create %s.json\n", w.network)
|
|
||||||
|
|
||||||
folder := w.readDefaultString(".")
|
|
||||||
if err := os.MkdirAll(folder, 0755); err != nil {
|
|
||||||
log.Error("Failed to create spec folder", "folder", folder, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out, _ := json.MarshalIndent(w.conf.Genesis, "", " ")
|
|
||||||
|
|
||||||
// Export the native genesis spec used by puppeth and Geth
|
|
||||||
gethJson := filepath.Join(folder, fmt.Sprintf("%s.json", w.network))
|
|
||||||
if err := os.WriteFile(gethJson, out, 0644); err != nil {
|
|
||||||
log.Error("Failed to save genesis file", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Saved native genesis chain spec", "path", gethJson)
|
|
||||||
|
|
||||||
case "3":
|
|
||||||
// Make sure we don't have any services running
|
|
||||||
if len(w.conf.servers()) > 0 {
|
|
||||||
log.Error("Genesis reset requires all services and servers torn down")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Genesis block destroyed")
|
|
||||||
|
|
||||||
w.conf.Genesis = nil
|
|
||||||
w.conf.flush()
|
|
||||||
default:
|
|
||||||
log.Error("That's not something I can do")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
// 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"
|
|
||||||
"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,
|
|
||||||
conf: config{
|
|
||||||
Servers: make(map[string][]byte),
|
|
||||||
},
|
|
||||||
servers: make(map[string]*sshClient),
|
|
||||||
services: make(map[string][]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
// Docker accepts hyphens in image names, but doesn't like it for container names
|
|
||||||
if w.network == "" {
|
|
||||||
fmt.Println("Please specify a network name to administer (no spaces, hyphens or capital letters please)")
|
|
||||||
for {
|
|
||||||
w.network = w.readString()
|
|
||||||
if !strings.Contains(w.network, " ") && !strings.Contains(w.network, "-") && strings.ToLower(w.network) == w.network {
|
|
||||||
fmt.Printf("\nSweet, 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, hyphens or capital letters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 := os.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 {
|
|
||||||
// Dial all previously known servers
|
|
||||||
for server, pubkey := range w.conf.Servers {
|
|
||||||
log.Info("Dialing previously configured server", "server", server)
|
|
||||||
client, err := dial(server, pubkey)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Previous server unreachable", "server", server, "err", err)
|
|
||||||
}
|
|
||||||
w.lock.Lock()
|
|
||||||
w.servers[server] = client
|
|
||||||
w.lock.Unlock()
|
|
||||||
}
|
|
||||||
w.networkStats()
|
|
||||||
}
|
|
||||||
// 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. Manage 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
choice := w.read()
|
|
||||||
switch {
|
|
||||||
case choice == "" || choice == "1":
|
|
||||||
w.networkStats()
|
|
||||||
|
|
||||||
case choice == "2":
|
|
||||||
if w.conf.Genesis == nil {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("What would you like to do? (default = create)")
|
|
||||||
fmt.Println(" 1. Create new genesis from scratch")
|
|
||||||
fmt.Println(" 2. Import already existing genesis")
|
|
||||||
|
|
||||||
choice := w.read()
|
|
||||||
switch {
|
|
||||||
case choice == "" || choice == "1":
|
|
||||||
w.makeGenesis()
|
|
||||||
case choice == "2":
|
|
||||||
w.importGenesis()
|
|
||||||
default:
|
|
||||||
log.Error("That's not something I can do")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w.manageGenesis()
|
|
||||||
}
|
|
||||||
case choice == "3":
|
|
||||||
if len(w.servers) == 0 {
|
|
||||||
if w.makeServer() != "" {
|
|
||||||
w.networkStats()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w.manageServers()
|
|
||||||
}
|
|
||||||
case choice == "4":
|
|
||||||
if len(w.services) == 0 {
|
|
||||||
w.deployComponent()
|
|
||||||
} else {
|
|
||||||
w.manageComponents()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Error("That's not something I can do")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,284 +0,0 @@
|
||||||
// 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"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"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() {
|
|
||||||
if len(w.servers) == 0 {
|
|
||||||
log.Info("No remote machines to gather stats from")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Clear out some previous configs to refill from current scan
|
|
||||||
w.conf.ethstats = ""
|
|
||||||
w.conf.bootnodes = w.conf.bootnodes[:0]
|
|
||||||
|
|
||||||
// Iterate over all the specified hosts and check their status
|
|
||||||
var pend sync.WaitGroup
|
|
||||||
|
|
||||||
stats := make(serverStats)
|
|
||||||
for server, pubkey := range w.conf.Servers {
|
|
||||||
pend.Add(1)
|
|
||||||
|
|
||||||
// Gather the service stats for each server concurrently
|
|
||||||
go func(server string, pubkey []byte) {
|
|
||||||
defer pend.Done()
|
|
||||||
|
|
||||||
stat := w.gatherStats(server, pubkey, w.servers[server])
|
|
||||||
|
|
||||||
// All status checks complete, report and check next server
|
|
||||||
w.lock.Lock()
|
|
||||||
defer w.lock.Unlock()
|
|
||||||
|
|
||||||
delete(w.services, server)
|
|
||||||
for service := range stat.services {
|
|
||||||
w.services[server] = append(w.services[server], service)
|
|
||||||
}
|
|
||||||
stats[server] = stat
|
|
||||||
}(server, pubkey)
|
|
||||||
}
|
|
||||||
pend.Wait()
|
|
||||||
|
|
||||||
// Print any collected stats and return
|
|
||||||
stats.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
// gatherStats gathers service statistics for a particular remote server.
|
|
||||||
func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat {
|
|
||||||
// Gather some global stats to feed into the wizard
|
|
||||||
var (
|
|
||||||
genesis string
|
|
||||||
ethstats string
|
|
||||||
bootnodes []string
|
|
||||||
)
|
|
||||||
// Ensure a valid SSH connection to the remote server
|
|
||||||
logger := log.New("server", server)
|
|
||||||
logger.Info("Starting remote server health-check")
|
|
||||||
|
|
||||||
stat := &serverStat{
|
|
||||||
services: make(map[string]map[string]string),
|
|
||||||
}
|
|
||||||
if client == nil {
|
|
||||||
conn, err := dial(server, pubkey)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to establish remote connection", "err", err)
|
|
||||||
stat.failure = err.Error()
|
|
||||||
return stat
|
|
||||||
}
|
|
||||||
client = conn
|
|
||||||
}
|
|
||||||
stat.address = client.address
|
|
||||||
|
|
||||||
// Client connected one way or another, run health-checks
|
|
||||||
logger.Debug("Checking for nginx availability")
|
|
||||||
if infos, err := checkNginx(client, w.network); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["nginx"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["nginx"] = infos.Report()
|
|
||||||
}
|
|
||||||
logger.Debug("Checking for ethstats availability")
|
|
||||||
if infos, err := checkEthstats(client, w.network); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["ethstats"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["ethstats"] = infos.Report()
|
|
||||||
ethstats = infos.config
|
|
||||||
}
|
|
||||||
logger.Debug("Checking for bootnode availability")
|
|
||||||
if infos, err := checkNode(client, w.network, true); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["bootnode"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["bootnode"] = infos.Report()
|
|
||||||
|
|
||||||
genesis = string(infos.genesis)
|
|
||||||
bootnodes = append(bootnodes, infos.enode)
|
|
||||||
}
|
|
||||||
logger.Debug("Checking for sealnode availability")
|
|
||||||
if infos, err := checkNode(client, w.network, false); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["sealnode"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["sealnode"] = infos.Report()
|
|
||||||
genesis = string(infos.genesis)
|
|
||||||
}
|
|
||||||
logger.Debug("Checking for explorer availability")
|
|
||||||
if infos, err := checkExplorer(client, w.network); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["explorer"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["explorer"] = infos.Report()
|
|
||||||
}
|
|
||||||
logger.Debug("Checking for faucet availability")
|
|
||||||
if infos, err := checkFaucet(client, w.network); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["faucet"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["faucet"] = infos.Report()
|
|
||||||
}
|
|
||||||
logger.Debug("Checking for dashboard availability")
|
|
||||||
if infos, err := checkDashboard(client, w.network); err != nil {
|
|
||||||
if err != ErrServiceUnknown {
|
|
||||||
stat.services["dashboard"] = map[string]string{"offline": err.Error()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat.services["dashboard"] = infos.Report()
|
|
||||||
}
|
|
||||||
// Feed and newly discovered information into the wizard
|
|
||||||
w.lock.Lock()
|
|
||||||
defer w.lock.Unlock()
|
|
||||||
|
|
||||||
if genesis != "" && w.conf.Genesis == nil {
|
|
||||||
g := new(core.Genesis)
|
|
||||||
if err := json.Unmarshal([]byte(genesis), g); err != nil {
|
|
||||||
log.Error("Failed to parse remote genesis", "err", err)
|
|
||||||
} else {
|
|
||||||
w.conf.Genesis = g
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ethstats != "" {
|
|
||||||
w.conf.ethstats = ethstats
|
|
||||||
}
|
|
||||||
w.conf.bootnodes = append(w.conf.bootnodes, bootnodes...)
|
|
||||||
|
|
||||||
return stat
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverStat is a collection of service configuration parameters and health
|
|
||||||
// check reports to print to the user.
|
|
||||||
type serverStat struct {
|
|
||||||
address string
|
|
||||||
failure string
|
|
||||||
services map[string]map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverStats is a collection of server stats for multiple hosts.
|
|
||||||
type serverStats map[string]*serverStat
|
|
||||||
|
|
||||||
// render converts the gathered statistics into a user friendly tabular report
|
|
||||||
// and prints it to the standard output.
|
|
||||||
func (stats serverStats) render() {
|
|
||||||
// Start gathering service statistics and config parameters
|
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
|
||||||
|
|
||||||
table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
|
|
||||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
|
||||||
table.SetColWidth(40)
|
|
||||||
|
|
||||||
// Find the longest lines for all columns for the hacked separator
|
|
||||||
separator := make([]string, 5)
|
|
||||||
for server, stat := range stats {
|
|
||||||
if len(server) > len(separator[0]) {
|
|
||||||
separator[0] = strings.Repeat("-", len(server))
|
|
||||||
}
|
|
||||||
if len(stat.address) > len(separator[1]) {
|
|
||||||
separator[1] = strings.Repeat("-", len(stat.address))
|
|
||||||
}
|
|
||||||
if len(stat.failure) > len(separator[1]) {
|
|
||||||
separator[1] = strings.Repeat("-", len(stat.failure))
|
|
||||||
}
|
|
||||||
for service, configs := range stat.services {
|
|
||||||
if len(service) > len(separator[2]) {
|
|
||||||
separator[2] = strings.Repeat("-", len(service))
|
|
||||||
}
|
|
||||||
for config, value := range configs {
|
|
||||||
if len(config) > len(separator[3]) {
|
|
||||||
separator[3] = strings.Repeat("-", len(config))
|
|
||||||
}
|
|
||||||
for _, val := range strings.Split(value, "\n") {
|
|
||||||
if len(val) > len(separator[4]) {
|
|
||||||
separator[4] = strings.Repeat("-", len(val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fill up the server report in alphabetical order
|
|
||||||
servers := make([]string, 0, len(stats))
|
|
||||||
for server := range stats {
|
|
||||||
servers = append(servers, server)
|
|
||||||
}
|
|
||||||
sort.Strings(servers)
|
|
||||||
|
|
||||||
for i, server := range servers {
|
|
||||||
// Add a separator between all servers
|
|
||||||
if i > 0 {
|
|
||||||
table.Append(separator)
|
|
||||||
}
|
|
||||||
// Fill up the service report in alphabetical order
|
|
||||||
services := make([]string, 0, len(stats[server].services))
|
|
||||||
for service := range stats[server].services {
|
|
||||||
services = append(services, service)
|
|
||||||
}
|
|
||||||
sort.Strings(services)
|
|
||||||
|
|
||||||
if len(services) == 0 {
|
|
||||||
if stats[server].failure != "" {
|
|
||||||
table.Append([]string{server, stats[server].failure, "", "", ""})
|
|
||||||
} else {
|
|
||||||
table.Append([]string{server, stats[server].address, "", "", ""})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for j, service := range services {
|
|
||||||
// Add an empty line between all services
|
|
||||||
if j > 0 {
|
|
||||||
table.Append([]string{"", "", "", separator[3], separator[4]})
|
|
||||||
}
|
|
||||||
// Fill up the config report in alphabetical order
|
|
||||||
configs := make([]string, 0, len(stats[server].services[service]))
|
|
||||||
for service := range stats[server].services[service] {
|
|
||||||
configs = append(configs, service)
|
|
||||||
}
|
|
||||||
sort.Strings(configs)
|
|
||||||
|
|
||||||
for k, config := range configs {
|
|
||||||
for l, value := range strings.Split(stats[server].services[service][config], "\n") {
|
|
||||||
switch {
|
|
||||||
case j == 0 && k == 0 && l == 0:
|
|
||||||
table.Append([]string{server, stats[server].address, service, config, value})
|
|
||||||
case k == 0 && l == 0:
|
|
||||||
table.Append([]string{"", "", service, config, value})
|
|
||||||
case l == 0:
|
|
||||||
table.Append([]string{"", "", "", config, value})
|
|
||||||
default:
|
|
||||||
table.Append([]string{"", "", "", "", value})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table.Render()
|
|
||||||
}
|
|
|
@ -1,197 +0,0 @@
|
||||||
// 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()
|
|
||||||
|
|
||||||
servers := w.conf.servers()
|
|
||||||
for i, server := range 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 := servers[choice-1]
|
|
||||||
client := w.servers[server]
|
|
||||||
|
|
||||||
delete(w.servers, server)
|
|
||||||
if client != nil {
|
|
||||||
client.Close()
|
|
||||||
}
|
|
||||||
delete(w.conf.Servers, server)
|
|
||||||
w.conf.flush()
|
|
||||||
|
|
||||||
log.Info("Disconnected existing server", "server", server)
|
|
||||||
w.networkStats()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If the user requested connecting a new server, do it
|
|
||||||
if w.makeServer() != "" {
|
|
||||||
w.networkStats()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeServer reads a single line from stdin and interprets it as
|
|
||||||
// username:identity@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("What is the remote server's address ([username[:identity]@]hostname[:port])?")
|
|
||||||
|
|
||||||
// Read and dial the server to ensure docker is present
|
|
||||||
input := w.readString()
|
|
||||||
|
|
||||||
client, err := dial(input, nil)
|
|
||||||
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[input] = client.pubkey
|
|
||||||
w.conf.flush()
|
|
||||||
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectServer lists the user all the currently 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?")
|
|
||||||
|
|
||||||
servers := w.conf.servers()
|
|
||||||
for i, server := range 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 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 components 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. Explorer - Chain analysis webservice")
|
|
||||||
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":
|
|
||||||
w.deployExplorer()
|
|
||||||
case "5":
|
|
||||||
w.deployFaucet()
|
|
||||||
case "6":
|
|
||||||
w.deployDashboard()
|
|
||||||
default:
|
|
||||||
log.Error("That's not something I can do")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
// 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) {
|
|
||||||
proxy, _ := checkNginx(client, w.network)
|
|
||||||
if 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.readDefaultYesNo(true) {
|
|
||||||
nocache := false
|
|
||||||
if proxy != nil {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Should the reverse-proxy be rebuilt from scratch (y/n)? (default = no)\n")
|
|
||||||
nocache = w.readDefaultYesNo(false)
|
|
||||||
}
|
|
||||||
if out, err := deployNginx(client, w.network, port, nocache); 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
|
|
||||||
}
|
|
|
@ -1,178 +0,0 @@
|
||||||
// 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 node configurations from the server
|
|
||||||
infos, err := checkNode(client, w.network, boot)
|
|
||||||
if err != nil {
|
|
||||||
if boot {
|
|
||||||
infos = &nodeInfos{port: 30303, peersTotal: 512, peersLight: 256}
|
|
||||||
} else {
|
|
||||||
infos = &nodeInfos{port: 30303, peersTotal: 50, peersLight: 0, gasLimit: 10, gasPrice: 1}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existed := err == nil
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if w.conf.Genesis.Config.Ethash != nil && !boot {
|
|
||||||
fmt.Println()
|
|
||||||
if infos.ethashdir == "" {
|
|
||||||
fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine?\n")
|
|
||||||
infos.ethashdir = w.readString()
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine? (default = %s)\n", infos.ethashdir)
|
|
||||||
infos.ethashdir = w.readDefaultString(infos.ethashdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Figure out which port to listen on
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.port)
|
|
||||||
infos.port = w.readDefaultInt(infos.port)
|
|
||||||
|
|
||||||
// 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 use?\n")
|
|
||||||
for {
|
|
||||||
if address := w.readAddress(); address != nil {
|
|
||||||
infos.etherbase = address.Hex()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("What address should the miner use? (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 != "" {
|
|
||||||
if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); 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.readDefaultYesNo(true) {
|
|
||||||
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 password")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Establish the gas dynamics to be enforced by the signer
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("What gas limit should full blocks target (MGas)? (default = %0.3f)\n", infos.gasLimit)
|
|
||||||
infos.gasLimit = w.readDefaultFloat(infos.gasLimit)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("What gas price should the signer require (GWei)? (default = %0.3f)\n", infos.gasPrice)
|
|
||||||
infos.gasPrice = w.readDefaultFloat(infos.gasPrice)
|
|
||||||
}
|
|
||||||
// Try to deploy the full node on the host
|
|
||||||
nocache := false
|
|
||||||
if existed {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Should the node be built from scratch (y/n)? (default = no)\n")
|
|
||||||
nocache = w.readDefaultYesNo(false)
|
|
||||||
}
|
|
||||||
if out, err := deployNode(client, w.network, w.conf.bootnodes, infos, nocache); 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()
|
|
||||||
}
|
|
1
go.mod
1
go.mod
|
@ -61,7 +61,6 @@ require (
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||||
golang.org/x/sys v0.2.0
|
golang.org/x/sys v0.2.0
|
||||||
golang.org/x/term v0.1.0
|
|
||||||
golang.org/x/text v0.4.0
|
golang.org/x/text v0.4.0
|
||||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||||
golang.org/x/tools v0.1.12
|
golang.org/x/tools v0.1.12
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -550,8 +550,6 @@ golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
|
Loading…
Reference in New Issue