mirror of
https://github.com/luxfi/netrunner.git
synced 2026-07-27 00:04:23 +00:00
- Update github.com/luxfi/keys v1.0.5 → v1.0.6 - Update github.com/luxfi/genesis v1.5.18 → v1.5.19 - Remove local replace directives for keys and genesis - Use published package versions for reproducible builds
1303 lines
30 KiB
Go
1303 lines
30 KiB
Go
// Copyright (C) 2021-2025, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package control
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/log/level"
|
|
"github.com/luxfi/netrunner/client"
|
|
"github.com/luxfi/netrunner/local"
|
|
"github.com/luxfi/netrunner/rpcpb"
|
|
"github.com/luxfi/netrunner/utils"
|
|
"github.com/luxfi/netrunner/utils/constants"
|
|
"github.com/luxfi/netrunner/ux"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func init() {
|
|
cobra.EnablePrefixMatching = true
|
|
}
|
|
|
|
const clientRootDirPrefix = "client"
|
|
|
|
var (
|
|
logLevel string
|
|
logDir string
|
|
trackChains string
|
|
endpoint string
|
|
dialTimeout time.Duration
|
|
requestTimeout time.Duration
|
|
logger log.Logger
|
|
)
|
|
|
|
// NOTE: Naming convention for node names is currently `node` + number, i.e. `node1,node2,node3,...node101`
|
|
|
|
func NewCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "control [options]",
|
|
Short: "Start a network runner controller.",
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVar(&logLevel, "log-level", level.Info.String(), "log level")
|
|
cmd.PersistentFlags().StringVar(&logDir, "log-dir", "", "log directory")
|
|
cmd.PersistentFlags().StringVar(&endpoint, "endpoint", "0.0.0.0:9000", "server endpoint")
|
|
cmd.PersistentFlags().DurationVar(&dialTimeout, "dial-timeout", 10*time.Second, "server dial timeout")
|
|
cmd.PersistentFlags().DurationVar(&requestTimeout, "request-timeout", 3*time.Minute, "client request timeout")
|
|
|
|
cmd.AddCommand(
|
|
newRPCVersionCommand(),
|
|
newStartCommand(),
|
|
newCreateChainsCommand(),
|
|
newCreateParticipantGroupsCommand(),
|
|
newTransformElasticChainsCommand(),
|
|
newAddPermissionlessValidatorCommand(),
|
|
newRemoveChainValidatorCommand(),
|
|
newHealthCommand(),
|
|
newWaitForHealthyCommand(),
|
|
newURIsCommand(),
|
|
newStatusCommand(),
|
|
newStreamStatusCommand(),
|
|
newAddNodeCommand(),
|
|
newRemoveNodeCommand(),
|
|
newPauseNodeCommand(),
|
|
newResumeNodeCommand(),
|
|
newRestartNodeCommand(),
|
|
newAttachPeerCommand(),
|
|
newSendOutboundMessageCommand(),
|
|
newStopCommand(),
|
|
newSaveSnapshotCommand(),
|
|
newLoadSnapshotCommand(),
|
|
newRemoveSnapshotCommand(),
|
|
newGetSnapshotNamesCommand(),
|
|
)
|
|
|
|
return cmd
|
|
}
|
|
|
|
var (
|
|
luxdBinPath string
|
|
numNodes uint32
|
|
pluginDir string
|
|
globalNodeConfig string
|
|
addNodeConfig string
|
|
chainSpecsStr string
|
|
customNodeConfigs string
|
|
rootDataDir string
|
|
chainConfigs string
|
|
upgradeConfigs string
|
|
pChainConfigs string
|
|
reassignPortsIfUsed bool
|
|
dynamicPorts bool
|
|
)
|
|
|
|
func setLogs() error {
|
|
var err error
|
|
if logDir == "" {
|
|
anrRootDir := filepath.Join(os.TempDir(), constants.RootDirPrefix)
|
|
err = os.MkdirAll(anrRootDir, os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clientRootDir := filepath.Join(anrRootDir, clientRootDirPrefix)
|
|
logDir, err = utils.MkDirWithTimestamp(clientRootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
lvl, err := log.ToLevel(logLevel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logFactory := log.NewFactoryWithConfig(log.Config{
|
|
RotatingWriterConfig: log.RotatingWriterConfig{
|
|
Directory: logDir,
|
|
},
|
|
DisplayLevel: lvl,
|
|
LogLevel: lvl,
|
|
})
|
|
logger, err = logFactory.Make(constants.LogNameControl)
|
|
return err
|
|
}
|
|
|
|
func newRPCVersionCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "rpc_version",
|
|
Short: "Requests RPC server version.",
|
|
RunE: RPCVersionFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func RPCVersionFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.RPCVersion(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("version response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newStartCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "start [options]",
|
|
Short: "Starts the server.",
|
|
RunE: startFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
cmd.PersistentFlags().StringVar(
|
|
&luxdBinPath,
|
|
"node-path",
|
|
"",
|
|
"node binary path",
|
|
)
|
|
cmd.PersistentFlags().Uint32Var(
|
|
&numNodes,
|
|
"number-of-nodes",
|
|
local.DefaultNumNodes,
|
|
"number of nodes of the network",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&pluginDir,
|
|
"plugin-dir",
|
|
"",
|
|
"[optional] plugin directory",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&rootDataDir,
|
|
"root-data-dir",
|
|
"",
|
|
"[optional] root data directory to store logs and configurations",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&chainSpecsStr,
|
|
"blockchain-specs",
|
|
"",
|
|
"[optional] JSON string of array of [(VM name, genesis file path)]",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&globalNodeConfig,
|
|
"global-node-config",
|
|
"",
|
|
"[optional] global node config as JSON string, applied to all nodes",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&customNodeConfigs,
|
|
"custom-node-configs",
|
|
"",
|
|
"[optional] custom node configs as JSON string of map, for each node individually. Common entries override `global-node-config`, but can be combined. Invalidates `number-of-nodes` (provide all node configs if used).",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&trackChains,
|
|
"whitelisted-chains",
|
|
"",
|
|
"[optional] whitelisted chains (comma-separated)",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&chainConfigs,
|
|
"chain-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its config file contents",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&upgradeConfigs,
|
|
"upgrade-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its upgrade file contents",
|
|
)
|
|
cmd.PersistentFlags().BoolVar(
|
|
&reassignPortsIfUsed,
|
|
"reassign-ports-if-used",
|
|
false,
|
|
"true to reassign default/given ports if already taken",
|
|
)
|
|
cmd.PersistentFlags().BoolVar(
|
|
&dynamicPorts,
|
|
"dynamic-ports",
|
|
false,
|
|
"true to assign dynamic ports",
|
|
)
|
|
if err := cmd.MarkPersistentFlagRequired("node-path"); err != nil {
|
|
panic(err)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func startFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
opts := []client.OpOption{
|
|
client.WithNumNodes(numNodes),
|
|
client.WithPluginDir(pluginDir),
|
|
client.WithTrackChains(trackChains),
|
|
client.WithRootDataDir(rootDataDir),
|
|
client.WithReassignPortsIfUsed(reassignPortsIfUsed),
|
|
client.WithDynamicPorts(dynamicPorts),
|
|
}
|
|
|
|
if globalNodeConfig != "" {
|
|
ux.Print(logger, log.Yellow.Wrap("global node config provided, will be applied to all nodes: %s"), globalNodeConfig)
|
|
// validate it's valid JSON
|
|
var js json.RawMessage
|
|
if err := json.Unmarshal([]byte(globalNodeConfig), &js); err != nil {
|
|
return fmt.Errorf("failed to validate JSON for provided config file: %w", err)
|
|
}
|
|
opts = append(opts, client.WithGlobalNodeConfig(globalNodeConfig))
|
|
}
|
|
|
|
if customNodeConfigs != "" {
|
|
nodeConfigs := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(customNodeConfigs), &nodeConfigs); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithCustomNodeConfigs(nodeConfigs))
|
|
}
|
|
|
|
if chainSpecsStr != "" {
|
|
chainSpecs := []*rpcpb.BlockchainSpec{}
|
|
if err := json.Unmarshal([]byte(chainSpecsStr), &chainSpecs); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainSpecs(chainSpecs))
|
|
}
|
|
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigs(chainConfigsMap))
|
|
}
|
|
if upgradeConfigs != "" {
|
|
upgradeConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(upgradeConfigs), &upgradeConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithUpgradeConfigs(upgradeConfigsMap))
|
|
}
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigFiles(chainConfigsMap))
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
info, err := cli.Start(
|
|
ctx,
|
|
luxdBinPath,
|
|
opts...,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("start response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newCreateChainsCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "create-blockchains blockchain-specs [options]",
|
|
Short: "Create blockchains.",
|
|
RunE: createChainsFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func createChainsFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
chainSpecsStr := args[0]
|
|
|
|
chainSpecs := []*rpcpb.BlockchainSpec{}
|
|
if err := json.Unmarshal([]byte(chainSpecsStr), &chainSpecs); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
info, err := cli.CreateChains(
|
|
ctx,
|
|
chainSpecs,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("create-blockchains response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newCreateParticipantGroupsCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "create-participant-groups [options]",
|
|
Short: "Create participant groups.",
|
|
RunE: createParticipantGroupsFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func createParticipantGroupsFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
participantsSpecsStr := args[0]
|
|
|
|
participantsSpecs := []*rpcpb.ChainSpec{}
|
|
if err := json.Unmarshal([]byte(participantsSpecsStr), &participantsSpecs); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
info, err := cli.CreateParticipantGroups(
|
|
ctx,
|
|
participantsSpecs,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("create-chains response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newTransformElasticChainsCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "elastic-chains elastic_chains_specs [options]",
|
|
Short: "Transform chains to elastic chains.",
|
|
RunE: transformElasticChainsFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func newAddPermissionlessValidatorCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "add-permissionless-validator permissionlessValidatorSpecs [options]",
|
|
Short: "Add permissionless validator to elastic chains.",
|
|
RunE: addPermissionlessValidatorFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func newRemoveChainValidatorCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "remove-chain-validator removeValidatorSpec [options]",
|
|
Short: "Remove chain validator",
|
|
RunE: removeChainValidatorFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func transformElasticChainsFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
elasticParticipantsSpecsStr := args[0]
|
|
|
|
elasticParticipantsSpecs := []*rpcpb.ElasticChainSpec{}
|
|
if err := json.Unmarshal([]byte(elasticParticipantsSpecsStr), &elasticParticipantsSpecs); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
info, err := cli.TransformElasticChains(
|
|
ctx,
|
|
elasticParticipantsSpecs,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("elastic-chains response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func addPermissionlessValidatorFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
validatorSpecStr := args[0]
|
|
|
|
validatorSpec := []*rpcpb.PermissionlessValidatorSpec{}
|
|
if err := json.Unmarshal([]byte(validatorSpecStr), &validatorSpec); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
info, err := cli.AddPermissionlessValidator(
|
|
ctx,
|
|
validatorSpec,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("add-permissionless-validator response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func removeChainValidatorFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
validatorSpecStr := args[0]
|
|
|
|
validatorSpec := []*rpcpb.RemoveChainValidatorSpec{}
|
|
if err := json.Unmarshal([]byte(validatorSpecStr), &validatorSpec); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
info, err := cli.RemoveChainValidator(
|
|
ctx,
|
|
validatorSpec,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("remove-chain-validator response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newHealthCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "health [options]",
|
|
Short: "Requests server health.",
|
|
RunE: healthFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func healthFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.Health(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("health response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newWaitForHealthyCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "wait-for-healthy [options]",
|
|
Short: "Wait until local cluster + custom vms are ready.",
|
|
RunE: waitForHealthy,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func waitForHealthy(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
defer cancel()
|
|
resp, err := cli.WaitForHealthy(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("wait for healthy response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newURIsCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "uris [options]",
|
|
Short: "Requests server uris.",
|
|
RunE: urisFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func urisFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
uris, err := cli.URIs(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("URIs: %s"), uris)
|
|
return nil
|
|
}
|
|
|
|
func newStatusCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "status [options]",
|
|
Short: "Requests server status.",
|
|
RunE: statusFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func statusFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.Status(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("status response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
var pushInterval time.Duration
|
|
|
|
func newStreamStatusCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "stream-status [options]",
|
|
Short: "Requests server bootstrap status.",
|
|
RunE: streamStatusFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
cmd.PersistentFlags().DurationVar(
|
|
&pushInterval,
|
|
"push-interval",
|
|
5*time.Second,
|
|
"interval that server pushes status updates to the client",
|
|
)
|
|
return cmd
|
|
}
|
|
|
|
func streamStatusFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
// poll until the cluster is healthy or os signal
|
|
sigc := make(chan os.Signal, 1)
|
|
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
donec := make(chan struct{})
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
go func() {
|
|
select {
|
|
case sig := <-sigc:
|
|
log.Warn("received signal", log.String("signal", sig.String()))
|
|
case <-ctx.Done():
|
|
}
|
|
cancel()
|
|
close(donec)
|
|
}()
|
|
|
|
ch, err := cli.StreamStatus(ctx, pushInterval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for info := range ch {
|
|
ux.Print(logger, log.Cyan.Wrap("cluster info: %+v"), info)
|
|
}
|
|
cancel() // receiver channel is closed, so cancel goroutine
|
|
<-donec
|
|
return nil
|
|
}
|
|
|
|
func newRemoveNodeCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "remove-node node-name [options]",
|
|
Short: "Removes a node.",
|
|
RunE: removeNodeFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func removeNodeFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
info, err := cli.RemoveNode(ctx, nodeName)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("remove node response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newPauseNodeCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "pause-node node-name [options]",
|
|
Short: "Pauses a node.",
|
|
RunE: pauseNodeFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func pauseNodeFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
info, err := cli.PauseNode(ctx, nodeName)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("pause node response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newResumeNodeCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "resume-node node-name [options]",
|
|
Short: "Resumes a node.",
|
|
RunE: resumeNodeFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func resumeNodeFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
info, err := cli.ResumeNode(ctx, nodeName)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("resume node response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newAddNodeCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "add-node node-name [options]",
|
|
Short: "Add a new node to the network",
|
|
RunE: addNodeFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
cmd.PersistentFlags().StringVar(
|
|
&luxdBinPath,
|
|
"node-path",
|
|
"",
|
|
"node binary path",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&addNodeConfig,
|
|
"node-config",
|
|
"",
|
|
"node config as string",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&pluginDir,
|
|
"plugin-dir",
|
|
"",
|
|
"[optional] plugin directory",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&chainConfigs,
|
|
"chain-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its config file contents",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&upgradeConfigs,
|
|
"upgrade-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its upgrade file contents",
|
|
)
|
|
return cmd
|
|
}
|
|
|
|
func addNodeFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
opts := []client.OpOption{
|
|
client.WithPluginDir(pluginDir),
|
|
}
|
|
|
|
if addNodeConfig != "" {
|
|
ux.Print(logger, log.Yellow.Wrap("WARNING: overriding node configs with custom provided config %s"), addNodeConfig)
|
|
// validate it's valid JSON
|
|
var js json.RawMessage
|
|
if err := json.Unmarshal([]byte(addNodeConfig), &js); err != nil {
|
|
return fmt.Errorf("failed to validate JSON for provided config file: %w", err)
|
|
}
|
|
opts = append(opts, client.WithGlobalNodeConfig(addNodeConfig))
|
|
}
|
|
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigs(chainConfigsMap))
|
|
}
|
|
if upgradeConfigs != "" {
|
|
upgradeConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(upgradeConfigs), &upgradeConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithUpgradeConfigs(upgradeConfigsMap))
|
|
}
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigFiles(chainConfigsMap))
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
info, err := cli.AddNode(
|
|
ctx,
|
|
nodeName,
|
|
luxdBinPath,
|
|
opts...,
|
|
)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("add node response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newRestartNodeCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "restart-node node-name [options]",
|
|
Short: "Restarts a node.",
|
|
RunE: restartNodeFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
cmd.PersistentFlags().StringVar(
|
|
&luxdBinPath,
|
|
"node-path",
|
|
"",
|
|
"node binary path",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&trackChains,
|
|
"whitelisted-chains",
|
|
"",
|
|
"[optional] whitelisted chains (comma-separated)",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&pluginDir,
|
|
"plugin-dir",
|
|
"",
|
|
"[optional] plugin directory",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&chainConfigs,
|
|
"chain-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its config file contents",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&upgradeConfigs,
|
|
"upgrade-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its upgrade file contents",
|
|
)
|
|
return cmd
|
|
}
|
|
|
|
func restartNodeFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
opts := []client.OpOption{
|
|
client.WithExecPath(luxdBinPath),
|
|
client.WithPluginDir(pluginDir),
|
|
client.WithTrackChains(trackChains),
|
|
}
|
|
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigs(chainConfigsMap))
|
|
}
|
|
if upgradeConfigs != "" {
|
|
upgradeConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(upgradeConfigs), &upgradeConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithUpgradeConfigs(upgradeConfigsMap))
|
|
}
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigFiles(chainConfigsMap))
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
info, err := cli.RestartNode(
|
|
ctx,
|
|
nodeName,
|
|
opts...,
|
|
)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("restart node response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newAttachPeerCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "attach-peer node-name [options]",
|
|
Short: "Attaches a peer to the node.",
|
|
RunE: attachPeerFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func attachPeerFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.AttachPeer(ctx, nodeName)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("attach peer response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
peerID string
|
|
msgOp uint32
|
|
msgBytesB64 string
|
|
)
|
|
|
|
func newSendOutboundMessageCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "send-outbound-message node-name [options]",
|
|
Short: "Sends an outbound message to an attached peer.",
|
|
RunE: sendOutboundMessageFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgs: []string{"node-name"},
|
|
}
|
|
cmd.PersistentFlags().StringVar(
|
|
&peerID,
|
|
"peer-id",
|
|
"",
|
|
"peer ID to send a message to",
|
|
)
|
|
cmd.PersistentFlags().Uint32Var(
|
|
&msgOp,
|
|
"message-op",
|
|
0,
|
|
"Message operation type",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&msgBytesB64,
|
|
"message-bytes-b64",
|
|
"",
|
|
"Message bytes in base64 encoding",
|
|
)
|
|
if err := cmd.MarkPersistentFlagRequired("peer-id"); err != nil {
|
|
panic(err)
|
|
}
|
|
if err := cmd.MarkPersistentFlagRequired("message-op"); err != nil {
|
|
panic(err)
|
|
}
|
|
if err := cmd.MarkPersistentFlagRequired("message-bytes-b64"); err != nil {
|
|
panic(err)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func sendOutboundMessageFunc(_ *cobra.Command, args []string) error {
|
|
// no validation for empty string required, as covered by `cobra.ExactArgs`
|
|
nodeName := args[0]
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
b, err := base64.StdEncoding.DecodeString(msgBytesB64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.SendOutboundMessage(ctx, nodeName, peerID, msgOp, b)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("send outbound message response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newStopCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "stop [options]",
|
|
Short: "Requests server stop.",
|
|
RunE: stopFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func stopFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
info, err := cli.Stop(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("stop response: %+v"), info)
|
|
return nil
|
|
}
|
|
|
|
func newSaveSnapshotCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "save-snapshot snapshot-name",
|
|
Short: "Requests server to save network snapshot.",
|
|
RunE: saveSnapshotFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func saveSnapshotFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.SaveSnapshot(ctx, args[0])
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("save-snapshot response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newLoadSnapshotCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "load-snapshot snapshot-name",
|
|
Short: "Requests server to load network snapshot.",
|
|
RunE: loadSnapshotFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
cmd.PersistentFlags().StringVar(
|
|
&luxdBinPath,
|
|
"node-path",
|
|
"",
|
|
"node binary path",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&pluginDir,
|
|
"plugin-dir",
|
|
"",
|
|
"plugin directory",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&rootDataDir,
|
|
"root-data-dir",
|
|
"",
|
|
"root data directory to store logs and configurations",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&chainConfigs,
|
|
"chain-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its config file contents",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&upgradeConfigs,
|
|
"upgrade-configs",
|
|
"",
|
|
"[optional] JSON string of map from chain id to its upgrade file contents",
|
|
)
|
|
cmd.PersistentFlags().StringVar(
|
|
&globalNodeConfig,
|
|
"global-node-config",
|
|
"",
|
|
"[optional] global node config as JSON string, applied to all nodes",
|
|
)
|
|
cmd.PersistentFlags().BoolVar(
|
|
&reassignPortsIfUsed,
|
|
"reassign-ports-if-used",
|
|
false,
|
|
"true to reassign snapshot ports if already taken",
|
|
)
|
|
return cmd
|
|
}
|
|
|
|
func loadSnapshotFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
opts := []client.OpOption{
|
|
client.WithExecPath(luxdBinPath),
|
|
client.WithPluginDir(pluginDir),
|
|
client.WithRootDataDir(rootDataDir),
|
|
client.WithReassignPortsIfUsed(reassignPortsIfUsed),
|
|
}
|
|
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigs(chainConfigsMap))
|
|
}
|
|
if upgradeConfigs != "" {
|
|
upgradeConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(upgradeConfigs), &upgradeConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithUpgradeConfigs(upgradeConfigsMap))
|
|
}
|
|
if chainConfigs != "" {
|
|
chainConfigsMap := make(map[string]string)
|
|
if err := json.Unmarshal([]byte(chainConfigs), &chainConfigsMap); err != nil {
|
|
return err
|
|
}
|
|
opts = append(opts, client.WithChainConfigFiles(chainConfigsMap))
|
|
}
|
|
|
|
if globalNodeConfig != "" {
|
|
ux.Print(logger, log.Yellow.Wrap("global node config provided, will be applied to all nodes: %s"), globalNodeConfig)
|
|
|
|
// validate it's valid JSON
|
|
var js json.RawMessage
|
|
if err := json.Unmarshal([]byte(globalNodeConfig), &js); err != nil {
|
|
return fmt.Errorf("failed to validate JSON for provided config file: %w", err)
|
|
}
|
|
opts = append(opts, client.WithGlobalNodeConfig(globalNodeConfig))
|
|
}
|
|
|
|
ctx := getAsyncContext()
|
|
|
|
resp, err := cli.LoadSnapshot(ctx, args[0], opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("load-snapshot response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newRemoveSnapshotCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "remove-snapshot snapshot-name",
|
|
Short: "Requests server to remove network snapshot.",
|
|
RunE: removeSnapshotFunc,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func removeSnapshotFunc(_ *cobra.Command, args []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
resp, err := cli.RemoveSnapshot(ctx, args[0])
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("remove-snapshot response: %+v"), resp)
|
|
return nil
|
|
}
|
|
|
|
func newGetSnapshotNamesCommand() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "get-snapshot-names [options]",
|
|
Short: "Requests server to get list of snapshot.",
|
|
RunE: getSnapshotNamesFunc,
|
|
Args: cobra.ExactArgs(0),
|
|
}
|
|
}
|
|
|
|
func getSnapshotNamesFunc(*cobra.Command, []string) error {
|
|
cli, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
snapshotNames, err := cli.GetSnapshotNames(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Print(logger, log.Green.Wrap("Snapshots: %s"), snapshotNames)
|
|
return nil
|
|
}
|
|
|
|
func newClient() (client.Client, error) {
|
|
if err := setLogs(); err != nil {
|
|
return nil, err
|
|
}
|
|
return client.New(client.Config{
|
|
Endpoint: endpoint,
|
|
DialTimeout: dialTimeout,
|
|
}, logger)
|
|
}
|
|
|
|
func getAsyncContext() context.Context {
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
// don't call since function using it is async
|
|
// and the top-level context here "ctx" is passed
|
|
// to all underlying function calls
|
|
// just set the timeout to halt "Start" async ops
|
|
// when the deadline is reached
|
|
_ = cancel
|
|
return ctx
|
|
}
|