Files

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)
}
}