mirror of https://github.com/ethereum/go-ethereum
parent
bd6a05e1ee
commit
8ded6a9fcd
@ -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() |
||||
} |
Loading…
Reference in new issue