mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
267 lines
8.8 KiB
Go
267 lines
8.8 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package nodecmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/dependencies"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/node"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/ansible"
|
|
"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/ssh"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
"github.com/luxfi/cli/v2/pkg/vm"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
type nodeUpgradeInfo struct {
|
|
LuxdVersion string // lux go version to update to on cloud server
|
|
SubnetEVMVersion string // subnet EVM version to update to on cloud server
|
|
SubnetEVMIDsToUpgrade []string // list of ID of Subnet EVM to be upgraded to subnet EVM version to update to
|
|
}
|
|
|
|
func newUpgradeCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "upgrade",
|
|
Short: "(ALPHA Warning) Update luxd or VM version for all node in a cluster",
|
|
Long: `(ALPHA Warning) This command is currently in experimental mode.
|
|
|
|
The node update command suite provides a collection of commands for nodes to update
|
|
their luxd or VM version.
|
|
|
|
You can check the status after upgrade by calling lux node status`,
|
|
Args: cobrautils.ExactArgs(1),
|
|
RunE: upgrade,
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func upgrade(_ *cobra.Command, args []string) error {
|
|
clusterName := args[0]
|
|
if err := node.CheckCluster(app, clusterName); err != nil {
|
|
return err
|
|
}
|
|
clusterConfig, err := app.GetClusterConfig(clusterName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if clusterConfig.Local {
|
|
return notImplementedForLocal("upgrade")
|
|
}
|
|
hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer node.DisconnectHosts(hosts)
|
|
toUpgradeNodesMap, err := getNodesUpgradeInfo(hosts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spinSession := ux.NewUserSpinner()
|
|
for host, upgradeInfo := range toUpgradeNodesMap {
|
|
if upgradeInfo.LuxdVersion != "" {
|
|
spinner := spinSession.SpinToUser(utils.ScriptLog(host.NodeID, fmt.Sprintf("Upgrading luxd to version %s...", upgradeInfo.LuxdVersion)))
|
|
if err := upgradeLuxd(host, upgradeInfo.LuxdVersion); err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return err
|
|
}
|
|
ux.SpinComplete(spinner)
|
|
}
|
|
if upgradeInfo.SubnetEVMVersion != "" {
|
|
subnetEVMVersionToUpgradeToWoPrefix := strings.TrimPrefix(upgradeInfo.SubnetEVMVersion, "v")
|
|
subnetEVMArchive := fmt.Sprintf(constants.SubnetEVMArchive, subnetEVMVersionToUpgradeToWoPrefix)
|
|
subnetEVMReleaseURL := fmt.Sprintf(constants.SubnetEVMReleaseURL, upgradeInfo.SubnetEVMVersion, subnetEVMArchive)
|
|
spinner := spinSession.SpinToUser(utils.ScriptLog(host.NodeID, fmt.Sprintf("Upgrading SubnetEVM to version %s...", upgradeInfo.SubnetEVMVersion)))
|
|
if err := getNewSubnetEVMRelease(host, subnetEVMReleaseURL, subnetEVMArchive); err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return err
|
|
}
|
|
if err := ssh.RunSSHStopNode(host); err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return err
|
|
}
|
|
for _, vmID := range upgradeInfo.SubnetEVMIDsToUpgrade {
|
|
subnetEVMBinaryPath := fmt.Sprintf(constants.CloudNodeSubnetEvmBinaryPath, vmID)
|
|
if err := upgradeSubnetEVM(host, subnetEVMBinaryPath); err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return err
|
|
}
|
|
}
|
|
if err := ssh.RunSSHStartNode(host); err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return err
|
|
}
|
|
ux.SpinComplete(spinner)
|
|
}
|
|
}
|
|
spinSession.Stop()
|
|
return nil
|
|
}
|
|
|
|
// getNodesUpgradeInfo gets the node versions of all given nodes and checks which
|
|
// nodes needs to have Lux Go & SubnetEVM upgraded. It first checks the subnet EVM version -
|
|
// it will install the newest subnet EVM version and install the latest lux Go that is still compatible with the Subnet EVM version
|
|
// if the node is not tracking any subnet, it will just install latestLuxdVersion
|
|
func getNodesUpgradeInfo(hosts []*models.Host) (map[*models.Host]nodeUpgradeInfo, error) {
|
|
latestLuxdVersion, err := app.Downloader.GetLatestReleaseVersion(
|
|
constants.LuxOrg,
|
|
constants.LuxdRepoName,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
latestSubnetEVMVersion, err := app.Downloader.GetLatestReleaseVersion(
|
|
constants.LuxOrg,
|
|
constants.SubnetEVMRepoName,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rpcVersion, err := vm.GetRPCProtocolVersion(app, models.SubnetEvm, latestSubnetEVMVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodeErrors := map[string]error{}
|
|
nodesToUpgrade := make(map[*models.Host]nodeUpgradeInfo)
|
|
|
|
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.NodeID, nil, err)
|
|
return
|
|
} else {
|
|
if vmVersions, err := parseNodeVersionOutput(resp); err != nil {
|
|
nodeResults.AddResult(host.NodeID, nil, err)
|
|
} else {
|
|
nodeResults.AddResult(host.NodeID, vmVersions, err)
|
|
}
|
|
}
|
|
}(&wgResults, host)
|
|
}
|
|
wg.Wait()
|
|
if wgResults.HasErrors() {
|
|
return nil, fmt.Errorf("failed to get luxd version for node(s) %s", wgResults.GetErrorHostMap())
|
|
}
|
|
|
|
nodeIDToHost := map[string]*models.Host{}
|
|
for _, host := range hosts {
|
|
nodeIDToHost[host.NodeID] = host
|
|
}
|
|
|
|
for hostID, vmVersionsInterface := range wgResults.GetResultMap() {
|
|
vmVersions, err := utils.ConvertInterfaceToMap(vmVersionsInterface)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
currentLuxdVersion := vmVersions[constants.PlatformKeyName]
|
|
luxdVersionToUpdateTo := latestLuxdVersion
|
|
nodeUpgradeInfo := nodeUpgradeInfo{}
|
|
nodeUpgradeInfo.SubnetEVMIDsToUpgrade = []string{}
|
|
for vmName, vmVersion := range vmVersions {
|
|
// when calling info.getNodeVersion, this is what we get
|
|
// "vmVersions":{"avm":"v1.10.12","evm":"v0.12.5","n8Anw9kErmgk7KHviddYtecCmziLZTphDwfL1V2DfnFjWZXbE":"v0.5.6","platform":"v1.10.12"}},
|
|
// we need to get the VM ID of the subnets that the node is currently validating, in the example above it is n8Anw9kErmgk7KHviddYtecCmziLZTphDwfL1V2DfnFjWZXbE
|
|
if !checkIfKeyIsStandardVMName(vmName) {
|
|
if vmVersion != latestSubnetEVMVersion {
|
|
// update subnet EVM version
|
|
ux.Logger.PrintToUser("Upgrading Subnet EVM version for node %s from version %s to version %s", hostID, vmVersion, latestSubnetEVMVersion)
|
|
nodeUpgradeInfo.SubnetEVMVersion = latestSubnetEVMVersion
|
|
nodeUpgradeInfo.SubnetEVMIDsToUpgrade = append(nodeUpgradeInfo.SubnetEVMIDsToUpgrade, vmName)
|
|
}
|
|
// find the highest version of lux go that is still compatible with current highest rpc
|
|
luxdVersionToUpdateTo, err = dependencies.GetLatestLuxdByProtocolVersion(app, rpcVersion)
|
|
if err != nil {
|
|
nodeErrors[hostID] = err
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if _, hasFailed := nodeErrors[hostID]; hasFailed {
|
|
continue
|
|
}
|
|
if currentLuxdVersion != luxdVersionToUpdateTo {
|
|
ux.Logger.PrintToUser("Upgrading Lux Go version for node %s from version %s to version %s", hostID, currentLuxdVersion, luxdVersionToUpdateTo)
|
|
nodeUpgradeInfo.LuxdVersion = luxdVersionToUpdateTo
|
|
}
|
|
nodesToUpgrade[nodeIDToHost[hostID]] = nodeUpgradeInfo
|
|
}
|
|
if len(nodeErrors) > 0 {
|
|
ux.Logger.PrintToUser("Failed to upgrade nodes: ")
|
|
for node, nodeErr := range nodeErrors {
|
|
ux.Logger.PrintToUser("node %s failed to upgrade due to %s", node, nodeErr)
|
|
}
|
|
return nil, fmt.Errorf("failed to upgrade node(s) %s", maps.Keys(nodeErrors))
|
|
}
|
|
return nodesToUpgrade, nil
|
|
}
|
|
|
|
// checks if vmName is "avm", "evm" or "platform"
|
|
func checkIfKeyIsStandardVMName(vmName string) bool {
|
|
standardVMNames := []string{constants.PlatformKeyName, constants.EVMKeyName, constants.AVMKeyName}
|
|
return slices.Contains(standardVMNames, vmName)
|
|
}
|
|
|
|
func upgradeLuxd(
|
|
host *models.Host,
|
|
luxdVersionToUpdateTo string,
|
|
) error {
|
|
if err := ssh.RunSSHUpgradeLuxgo(host, luxdVersionToUpdateTo); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func upgradeSubnetEVM(
|
|
host *models.Host,
|
|
subnetEVMBinaryPath string,
|
|
) error {
|
|
if _, err := host.Command(fmt.Sprintf("cp -f subnet-evm %s", subnetEVMBinaryPath), nil, constants.SSHFileOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getNewSubnetEVMRelease(
|
|
host *models.Host,
|
|
subnetEVMReleaseURL string,
|
|
subnetEVMArchive string,
|
|
) error {
|
|
if err := ssh.RunSSHGetNewSubnetEVMRelease(host, subnetEVMReleaseURL, subnetEVMArchive); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseNodeVersionOutput(byteValue []byte) (map[string]interface{}, error) {
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(byteValue, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
nodeIDInterface, ok := result["result"].(map[string]interface{})
|
|
if ok {
|
|
vmVersions, ok := nodeIDInterface["vmVersions"].(map[string]interface{})
|
|
if ok {
|
|
return vmVersions, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|