Files

234 lines
7.6 KiB
Go

// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package nodecmd
import (
"fmt"
"os"
"path/filepath"
"sync"
"github.com/luxfi/cli/v2/pkg/dependencies"
"github.com/luxfi/cli/v2/pkg/prompts"
"github.com/luxfi/cli/v2/pkg/docker"
"github.com/luxfi/cli/v2/pkg/cobrautils"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/networkoptions"
"github.com/luxfi/cli/v2/pkg/ssh"
"github.com/luxfi/cli/v2/pkg/utils"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/node/v2/v2/utils/logging"
"github.com/spf13/cobra"
)
var (
nodeIPs []string
sshKeyPaths []string
overrideExisting bool
)
func newSetupCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "setup",
Short: "Sets up a new Lux Node on remote server",
Long: `The node setup command installs Lux Go on specified remote servers.
To run the command, the remote servers' IP addresses and SSH private keys are required.
Currently, only Ubuntu operating system is supported.`,
Args: cobrautils.ExactArgs(0),
RunE: setupNode,
PersistentPostRun: handlePostRun,
}
networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, networkoptions.NonLocalSupportedNetworkOptions)
cmd.Flags().BoolVar(&useLatestLuxgoReleaseVersion, "latest-luxd-version", false, "install latest luxd release version on node/s")
cmd.Flags().BoolVar(&useLatestLuxgoPreReleaseVersion, "latest-luxd-pre-release-version", false, "install latest luxd pre-release version on node/s")
cmd.Flags().StringVar(&useCustomLuxgoVersion, "custom-luxd-version", "", "install given luxd version on node/s")
cmd.Flags().StringVar(&useLuxgoVersionFromSubnet, "luxd-version-from-subnet", "", "install latest luxd version, that is compatible with the given subnet, on node/s")
cmd.Flags().BoolVar(&publicHTTPPortAccess, "public-http-port", false, "allow public access to luxd HTTP port")
cmd.Flags().StringArrayVar(&nodeIPs, "node-ips", []string{}, "IP addresses of nodes")
cmd.Flags().StringArrayVar(&sshKeyPaths, "ssh-key-paths", []string{}, "ssh key paths")
cmd.Flags().BoolVar(&useSSHAgent, "use-ssh-agent", false, "use ssh agent(ex: Yubikey) for ssh auth")
cmd.Flags().StringVar(&genesisPath, "genesis", "", "path to genesis file")
cmd.Flags().StringVar(&upgradePath, "upgrade", "", "path to upgrade file")
cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "primary network partial sync")
cmd.Flags().BoolVar(&overrideExisting, "override-existing", false, "override existing staking files")
return cmd
}
func setup(hosts []*models.Host, luxdVersion string, network models.Network) error {
if globalNetworkFlags.UseDevnet {
partialSync = false
ux.Logger.PrintToUser("disabling partial sync default for devnet")
}
ux.Logger.PrintToUser("Setting up Lux node(s)...")
wg := sync.WaitGroup{}
wgResults := models.NodeResults{}
spinSession := ux.NewUserSpinner()
for _, host := range hosts {
wg.Add(1)
go func(nodeResults *models.NodeResults, host *models.Host) {
defer wg.Done()
if err := host.Connect(0); err != nil {
nodeResults.AddResult(host.IP, nil, err)
return
}
if err := provideStakingCertAndKey(host); err != nil {
nodeResults.AddResult(host.IP, nil, err)
return
}
spinner := spinSession.SpinToUser(utils.ScriptLog(host.IP, "Setup Node"))
if err := ssh.RunSSHSetupNode(host, app.Conf.GetConfigPath()); err != nil {
nodeResults.AddResult(host.IP, nil, err)
ux.SpinFailWithError(spinner, "", err)
return
}
if err := ssh.RunSSHSetupDockerService(host); err != nil {
nodeResults.AddResult(host.IP, nil, err)
ux.SpinFailWithError(spinner, "", err)
return
}
ux.SpinComplete(spinner)
spinner = spinSession.SpinToUser(utils.ScriptLog(host.IP, "Setup Luxd"))
// check if host is a API host
if err := docker.ComposeSSHSetupNode(host,
network,
luxdVersion,
bootstrapIDs,
bootstrapIPs,
partialSync,
genesisPath,
upgradePath,
addMonitoring,
host.APINode); err != nil {
nodeResults.AddResult(host.IP, nil, err)
ux.SpinFailWithError(spinner, "", err)
return
}
ux.SpinComplete(spinner)
}(&wgResults, host)
}
wg.Wait()
spinSession.Stop()
for _, node := range hosts {
if wgResults.HasIDWithError(node.NodeID) {
ux.Logger.RedXToUser("Node %s has ERROR: %s", node.IP, wgResults.GetErrorHostMap()[node.IP])
}
}
if wgResults.HasErrors() {
return fmt.Errorf("failed to deploy node(s) %s", wgResults.GetErrorHostMap())
} else {
ux.Logger.PrintToUser(logging.Green.Wrap("Luxd and Lux-CLI installed and node(s) are bootstrapping!"))
}
return nil
}
func setupNode(_ *cobra.Command, _ []string) error {
network, err := networkoptions.GetNetworkFromCmdLineFlags(
app,
"",
globalNetworkFlags,
false,
true,
networkoptions.NonLocalSupportedNetworkOptions,
"",
)
if err != nil {
return err
}
luxdVersionSetting := dependencies.LuxdVersionSettings{
UseLuxgoVersionFromSubnet: useLuxgoVersionFromSubnet,
UseLatestLuxgoReleaseVersion: useLatestLuxgoReleaseVersion,
UseLatestLuxgoPreReleaseVersion: useLatestLuxgoPreReleaseVersion,
UseCustomLuxgoVersion: useCustomLuxgoVersion,
}
luxdVersion, err := dependencies.GetLuxdVersion(app, luxdVersionSetting, network)
if err != nil {
return err
}
if !useSSHAgent {
if len(nodeIPs) != len(sshKeyPaths) {
return fmt.Errorf("--node-ips and --ssh-key-paths should have same number of values")
}
}
if err = promptSetupNodes(); err != nil {
return err
}
hosts := []*models.Host{}
for i, nodeIP := range nodeIPs {
sshKeyPath := ""
if !useSSHAgent {
sshKeyPath = sshKeyPaths[i]
}
hosts = append(hosts, &models.Host{
SSHUser: constants.RemoteSSHUser,
IP: nodeIP,
SSHPrivateKeyPath: sshKeyPath,
})
}
if err = setup(hosts, luxdVersion, network); err != nil {
return err
}
printSetupResults(hosts)
return nil
}
func printSetupResults(hosts []*models.Host) {
for _, host := range hosts {
nodePath := app.GetNodeStakingDir(host.IP)
certBytes, err := os.ReadFile(filepath.Join(nodePath, constants.StakerCertFileName))
if err != nil {
continue
}
nodeID, err := utils.ToNodeID(certBytes)
if err != nil {
continue
}
ux.Logger.PrintToUser("%s Public IP: %s | %s ", logging.Green.Wrap(">"), host.IP, logging.Green.Wrap(nodeID.String()))
ux.Logger.PrintToUser("staker.crt, staker.key and signer.key are stored at %s. Please keep them safe, as these files can be used to fully recreate your node.", nodePath)
ux.Logger.PrintLineSeparator()
}
}
func promptSetupNodes() error {
var err error
var numNodes int
ux.Logger.PrintToUser("Only Ubuntu operating system is supported")
if len(nodeIPs) == 0 && len(sshKeyPaths) == 0 {
numNodes, err = app.Prompt.CaptureInt(
"How many Lux nodes do you want to setup?",
prompts.ValidatePositiveInt,
)
}
if err != nil {
return err
}
for len(nodeIPs) < numNodes {
ux.Logger.PrintToUser("Getting info for node %d", len(nodeIPs)+1)
ipAddress, err := app.Prompt.CaptureString("What is the IP address of the node to be set up?")
if err != nil {
return err
}
nodeIPs = append(nodeIPs, ipAddress)
ux.Logger.GreenCheckmarkToUser("Node %d:", len(nodeIPs))
ux.Logger.PrintToUser("- IP Address: %s", ipAddress)
if !useSSHAgent {
sshKeyPath, err := app.Prompt.CaptureString("What is the key path of the private key that can be used to ssh into this node?")
if err != nil {
return err
}
sshKeyPaths = append(sshKeyPaths, sshKeyPath)
ux.Logger.PrintToUser("- SSH Key Path: %s", sshKeyPath)
}
}
return nil
}