Files
netrunner/cmd/control/control.go
T
Zach Kelling 4e2a4e432e chore: remove local replace directives and update dependencies
- 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
2026-01-03 07:05:17 -08:00

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
}