mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
273 lines
8.4 KiB
Go
273 lines
8.4 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package node
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/ansible"
|
|
"github.com/luxfi/cli/v2/pkg/application"
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/ssh"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
|
|
"github.com/luxfi/node/v2/v2/api/info"
|
|
)
|
|
|
|
const (
|
|
HealthCheckPoolTime = 60 * time.Second
|
|
HealthCheckTimeout = 3 * time.Minute
|
|
)
|
|
|
|
func AuthorizedAccessFromSettings(app *application.Lux) bool {
|
|
return app.Conf.GetConfigBoolValue(constants.ConfigAuthorizeCloudAccessKey)
|
|
}
|
|
|
|
func CheckCluster(app *application.Lux, clusterName string) error {
|
|
_, err := GetClusterNodes(app, clusterName)
|
|
return err
|
|
}
|
|
|
|
func GetClusterNodes(app *application.Lux, clusterName string) ([]string, error) {
|
|
if exists, err := CheckClusterExists(app, clusterName); err != nil || !exists {
|
|
return nil, fmt.Errorf("cluster %q not found", clusterName)
|
|
}
|
|
clusterConfig, err := app.GetClusterConfig(clusterName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clusterNodes := clusterConfig.Nodes
|
|
if len(clusterNodes) == 0 && !clusterConfig.Local {
|
|
return nil, fmt.Errorf("no nodes found in cluster %s", clusterName)
|
|
}
|
|
return clusterNodes, nil
|
|
}
|
|
|
|
func CheckClusterExists(app *application.Lux, clusterName string) (bool, error) {
|
|
clustersConfig := models.ClustersConfig{}
|
|
if app.ClustersConfigExists() {
|
|
var err error
|
|
clustersConfig, err = app.LoadClustersConfig()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
_, ok := clustersConfig.Clusters[clusterName]
|
|
return ok, nil
|
|
}
|
|
|
|
func CheckHostsAreRPCCompatible(app *application.Lux, hosts []*models.Host, subnetName string) error {
|
|
incompatibleNodes, err := getRPCIncompatibleNodes(app, hosts, subnetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(incompatibleNodes) > 0 {
|
|
sc, err := app.LoadSidecar(subnetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ux.Logger.PrintToUser("Either modify your Lux Go version or modify your VM version")
|
|
ux.Logger.PrintToUser("To modify your Lux Go version: https://docs.lux.network/nodes/maintain/upgrade-your-luxd-node")
|
|
switch sc.VM {
|
|
case models.SubnetEvm:
|
|
ux.Logger.PrintToUser("To modify your Subnet-EVM version: https://docs.lux.network/build/subnet/upgrade/upgrade-subnet-vm")
|
|
case models.CustomVM:
|
|
ux.Logger.PrintToUser("To modify your Custom VM binary: lux blockchain upgrade vm %s --config", subnetName)
|
|
}
|
|
ux.Logger.PrintToUser("Yoy can use \"lux node upgrade\" to upgrade Lux Go and/or Subnet-EVM to their latest versions")
|
|
return fmt.Errorf("the Lux Go version of node(s) %s is incompatible with VM RPC version of %s", incompatibleNodes, subnetName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getRPCIncompatibleNodes(app *application.Lux, hosts []*models.Host, subnetName string) ([]string, error) {
|
|
ux.Logger.PrintToUser("Checking compatibility of node(s) lux go RPC protocol version with Subnet EVM RPC of blockchain %s ...", subnetName)
|
|
sc, err := app.LoadSidecar(subnetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wg := sync.WaitGroup{}
|
|
wgResults := models.NodeResults{}
|
|
for _, host := range hosts {
|
|
wg.Add(1)
|
|
go func(nodeResults *models.NodeResults, host *models.Host) {
|
|
defer wg.Done()
|
|
if resp, err := ssh.RunSSHCheckLuxdVersion(host); err != nil {
|
|
nodeResults.AddResult(host.GetCloudID(), nil, err)
|
|
return
|
|
} else {
|
|
if _, rpcVersion, err := ParseLuxdOutput(resp); err != nil {
|
|
nodeResults.AddResult(host.GetCloudID(), nil, err)
|
|
} else {
|
|
nodeResults.AddResult(host.GetCloudID(), rpcVersion, err)
|
|
}
|
|
}
|
|
}(&wgResults, host)
|
|
}
|
|
wg.Wait()
|
|
if wgResults.HasErrors() {
|
|
return nil, fmt.Errorf("failed to get rpc protocol version for node(s) %s", wgResults.GetErrorHostMap())
|
|
}
|
|
incompatibleNodes := []string{}
|
|
for nodeID, rpcVersionI := range wgResults.GetResultMap() {
|
|
rpcVersion := rpcVersionI.(uint32)
|
|
if rpcVersion != uint32(sc.RPCVersion) {
|
|
incompatibleNodes = append(incompatibleNodes, nodeID)
|
|
}
|
|
}
|
|
if len(incompatibleNodes) > 0 {
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Compatible Lux Go RPC version is %d", sc.RPCVersion))
|
|
}
|
|
return incompatibleNodes, nil
|
|
}
|
|
|
|
func ParseLuxdOutput(byteValue []byte) (string, uint32, error) {
|
|
reply := map[string]interface{}{}
|
|
if err := json.Unmarshal(byteValue, &reply); err != nil {
|
|
return "", 0, err
|
|
}
|
|
resultMap := reply["result"]
|
|
resultJSON, err := json.Marshal(resultMap)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
nodeVersionReply := info.GetNodeVersionReply{}
|
|
if err := json.Unmarshal(resultJSON, &nodeVersionReply); err != nil {
|
|
return "", 0, err
|
|
}
|
|
return nodeVersionReply.VMVersions["platform"], uint32(nodeVersionReply.RPCProtocolVersion), nil
|
|
}
|
|
|
|
func DisconnectHosts(hosts []*models.Host) {
|
|
for _, host := range hosts {
|
|
_ = host.Disconnect()
|
|
}
|
|
}
|
|
|
|
func getPublicEndpoints(
|
|
app *application.Lux,
|
|
clusterName string,
|
|
trackers []*models.Host,
|
|
) ([]string, error) {
|
|
clusterConfig, err := app.GetClusterConfig(clusterName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
publicNodes := clusterConfig.APINodes
|
|
if clusterConfig.Network.Kind == models.Devnet {
|
|
publicNodes = clusterConfig.Nodes
|
|
}
|
|
publicTrackers := utils.Filter(trackers, func(tracker *models.Host) bool {
|
|
return sdkutils.Belongs(publicNodes, tracker.GetCloudID())
|
|
})
|
|
endpoints := sdkutils.Map(publicTrackers, func(tracker *models.Host) string {
|
|
return GetLuxdEndpoint(tracker.IP)
|
|
})
|
|
return endpoints, nil
|
|
}
|
|
|
|
func GetLuxdEndpoint(ip string) string {
|
|
return fmt.Sprintf("http://%s:%d", ip, constants.LuxdAPIPort)
|
|
}
|
|
|
|
func GetUnhealthyNodes(hosts []*models.Host) ([]string, error) {
|
|
wg := sync.WaitGroup{}
|
|
wgResults := models.NodeResults{}
|
|
for _, host := range hosts {
|
|
wg.Add(1)
|
|
go func(nodeResults *models.NodeResults, host *models.Host) {
|
|
defer wg.Done()
|
|
if resp, err := ssh.RunSSHCheckHealthy(host); err != nil {
|
|
nodeResults.AddResult(host.GetCloudID(), nil, err)
|
|
return
|
|
} else {
|
|
if isHealthy, err := parseHealthyOutput(resp); err != nil {
|
|
nodeResults.AddResult(host.GetCloudID(), nil, err)
|
|
} else {
|
|
nodeResults.AddResult(host.GetCloudID(), isHealthy, err)
|
|
}
|
|
}
|
|
}(&wgResults, host)
|
|
}
|
|
wg.Wait()
|
|
if wgResults.HasErrors() {
|
|
return nil, fmt.Errorf("failed to get health status for node(s) %s", wgResults.GetErrorHostMap())
|
|
}
|
|
return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool {
|
|
return !wgResults.GetResultMap()[nodeID].(bool)
|
|
}), nil
|
|
}
|
|
|
|
func parseHealthyOutput(byteValue []byte) (bool, error) {
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(byteValue, &result); err != nil {
|
|
return false, err
|
|
}
|
|
isHealthyInterface, ok := result["result"].(map[string]interface{})
|
|
if ok {
|
|
isHealthy, ok := isHealthyInterface["healthy"].(bool)
|
|
if ok {
|
|
return isHealthy, nil
|
|
}
|
|
}
|
|
return false, fmt.Errorf("unable to parse node healthy status")
|
|
}
|
|
|
|
func WaitForHealthyCluster(
|
|
app *application.Lux,
|
|
clusterName string,
|
|
timeout time.Duration,
|
|
poolTime time.Duration,
|
|
) error {
|
|
ux.Logger.PrintToUser("")
|
|
ux.Logger.PrintToUser("Waiting for node(s) in cluster %s to be healthy...", clusterName)
|
|
clustersConfig, err := app.LoadClustersConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cluster, ok := clustersConfig.Clusters[clusterName]
|
|
if !ok {
|
|
return fmt.Errorf("cluster %s does not exist", clusterName)
|
|
}
|
|
allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hosts := cluster.GetValidatorHosts(allHosts) // exlude api nodes
|
|
defer DisconnectHosts(hosts)
|
|
startTime := time.Now()
|
|
spinSession := ux.NewUserSpinner()
|
|
spinner := spinSession.SpinToUser("Checking if node(s) are healthy...")
|
|
for {
|
|
unhealthyNodes, err := GetUnhealthyNodes(hosts)
|
|
if err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return err
|
|
}
|
|
if len(unhealthyNodes) == 0 {
|
|
ux.SpinComplete(spinner)
|
|
spinSession.Stop()
|
|
ux.Logger.GreenCheckmarkToUser("Nodes healthy after %d seconds", uint32(time.Since(startTime).Seconds()))
|
|
return nil
|
|
}
|
|
if time.Since(startTime) > timeout {
|
|
ux.SpinFailWithError(spinner, "", fmt.Errorf("cluster not healthy after %d seconds", uint32(timeout.Seconds())))
|
|
spinSession.Stop()
|
|
ux.Logger.PrintToUser("")
|
|
ux.Logger.RedXToUser("Unhealthy Nodes")
|
|
for _, failedNode := range unhealthyNodes {
|
|
ux.Logger.PrintToUser(" " + failedNode)
|
|
}
|
|
ux.Logger.PrintToUser("")
|
|
return fmt.Errorf("cluster not healthy after %d seconds", uint32(timeout.Seconds()))
|
|
}
|
|
time.Sleep(poolTime)
|
|
}
|
|
}
|