From 6028c53a4463842cfb3443069258e0ef6949f375 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 8 Oct 2024 16:53:38 -0400 Subject: [PATCH 1/3] refactor subnet sync --- pkg/node/helper.go | 213 ++++++++++++++++++++++++++++++++++++++ pkg/node/sync.go | 235 ++++++++++++++++++++++++++++++++++++++++++ pkg/subnet/helpers.go | 75 ++++++++++++++ 3 files changed, 523 insertions(+) create mode 100644 pkg/node/helper.go create mode 100644 pkg/node/sync.go diff --git a/pkg/node/helper.go b/pkg/node/helper.go new file mode 100644 index 000000000..4e94f1e25 --- /dev/null +++ b/pkg/node/helper.go @@ -0,0 +1,213 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +import ( + "encoding/json" + "fmt" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ssh" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/api/info" + "sync" +) + +func checkCluster(app *application.Avalanche, clusterName string) error { + _, err := getClusterNodes(app, clusterName) + return err +} + +func getClusterNodes(app *application.Avalanche, clusterName string) ([]string, error) { + if exists, err := checkClusterExists(app, clusterName); err != nil || !exists { + return nil, fmt.Errorf("cluster %q not found", clusterName) + } + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return nil, err + } + clusterNodes := clustersConfig.Clusters[clusterName].Nodes + if len(clusterNodes) == 0 { + return nil, fmt.Errorf("no nodes found in cluster %s", clusterName) + } + return clusterNodes, nil +} + +func checkClusterExists(app *application.Avalanche, 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.Avalanche, 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 Avalanche Go version or modify your VM version") + ux.Logger.PrintToUser("To modify your Avalanche Go version: https://docs.avax.network/nodes/maintain/upgrade-your-avalanchego-node") + switch sc.VM { + case models.SubnetEvm: + ux.Logger.PrintToUser("To modify your Subnet-EVM version: https://docs.avax.network/build/subnet/upgrade/upgrade-subnet-vm") + case models.CustomVM: + ux.Logger.PrintToUser("To modify your Custom VM binary: avalanche subnet upgrade vm %s --config", subnetName) + } + ux.Logger.PrintToUser("Yoy can use \"avalanche node upgrade\" to upgrade Avalanche Go and/or Subnet-EVM to their latest versions") + return fmt.Errorf("the Avalanche Go version of node(s) %s is incompatible with VM RPC version of %s", incompatibleNodes, subnetName) + } + return nil +} + +func getRPCIncompatibleNodes(app *application.Avalanche, hosts []*models.Host, subnetName string) ([]string, error) { + ux.Logger.PrintToUser("Checking compatibility of node(s) avalanche go RPC protocol version with Subnet EVM RPC of subnet %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.RunSSHCheckAvalancheGoVersion(host); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + return + } else { + if _, rpcVersion, err := parseAvalancheGoOutput(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 Avalanche Go RPC version is %d", sc.RPCVersion)) + } + return incompatibleNodes, nil +} + +func parseAvalancheGoOutput(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 getWSEndpoint(endpoint string, blockchainID string) string { + return models.NewDevnetNetwork(endpoint, 0).BlockchainWSEndpoint(blockchainID) +} + +func getPublicEndpoints(app *application.Avalanche, clusterName string) ([]string, error) { + endpoints := []string{} + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return nil, err + } + publicNodes := clusterConfig.APINodes + if clusterConfig.Network.Kind == models.Devnet { + publicNodes = clusterConfig.Nodes + } + for _, cloudID := range publicNodes { + nodeConfig, err := app.LoadClusterNodeConfig(cloudID) + if err != nil { + return nil, err + } + endpoints = append(endpoints, getAvalancheGoEndpoint(nodeConfig.ElasticIP)) + } + return endpoints, nil +} + +func getRPCEndpoint(endpoint string, blockchainID string) string { + return models.NewDevnetNetwork(endpoint, 0).BlockchainEndpoint(blockchainID) +} + +func getAvalancheGoEndpoint(ip string) string { + return fmt.Sprintf("http://%s:%d", ip, constants.AvalanchegoAPIPort) +} + +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") +} diff --git a/pkg/node/sync.go b/pkg/node/sync.go new file mode 100644 index 000000000..9f5d609c1 --- /dev/null +++ b/pkg/node/sync.go @@ -0,0 +1,235 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/ava-labs/avalanche-cli/pkg/ansible" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ssh" + "github.com/ava-labs/avalanche-cli/pkg/subnet" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/utils/set" + "sync" +) + +func SyncSubnet(app *application.Avalanche, clusterName, blockchainName string, avoidChecks bool, subnetAliases []string) error { + if err := checkCluster(app, clusterName); err != nil { + return err + } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + if _, err := subnet.ValidateSubnetNameAndGetChains(app, []string{blockchainName}); err != nil { + return err + } + hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) + if err != nil { + return err + } + defer disconnectHosts(hosts) + if !avoidChecks { + if err := checkHostsAreBootstrapped(hosts); err != nil { + return err + } + if err := checkHostsAreHealthy(hosts); err != nil { + return err + } + if err := checkHostsAreRPCCompatible(app, hosts, blockchainName); err != nil { + return err + } + } + if err := prepareSubnetPlugin(app, hosts, blockchainName); err != nil { + return err + } + if err := trackSubnet(app, hosts, clusterName, clusterConfig.Network, blockchainName, subnetAliases); err != nil { + return err + } + ux.Logger.PrintToUser("Node(s) successfully started syncing with Blockchain!") + ux.Logger.PrintToUser(fmt.Sprintf("Check node blockchain syncing status with avalanche node status %s --blockchain %s", clusterName, blockchainName)) + return nil +} + +// prepareSubnetPlugin creates subnet plugin to all nodes in the cluster +func prepareSubnetPlugin(app *application.Avalanche, hosts []*models.Host, blockchainName string) error { + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return 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 err := ssh.RunSSHCreatePlugin(host, sc); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return fmt.Errorf("failed to upload plugin to node(s) %s", wgResults.GetErrorHostMap()) + } + return nil +} + +func trackSubnet( + app *application.Avalanche, + hosts []*models.Host, + clusterName string, + network models.Network, + blockchainName string, + subnetAliases []string, +) error { + // load cluster config + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + // and get list of subnets + allSubnets := utils.Unique(append(clusterConfig.Subnets, blockchainName)) + + // load sidecar to get subnet blockchain ID + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return err + } + blockchainID := sc.Networks[network.Name()].BlockchainID + + wg := sync.WaitGroup{} + wgResults := models.NodeResults{} + subnetAliases = append([]string{blockchainName}, subnetAliases...) + for _, host := range hosts { + wg.Add(1) + go func(nodeResults *models.NodeResults, host *models.Host) { + defer wg.Done() + if err := ssh.RunSSHStopNode(host); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + + if err := ssh.RunSSHRenderAvagoAliasConfigFile( + host, + blockchainID.String(), + subnetAliases, + ); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHRenderAvalancheNodeConfig( + app, + host, + network, + allSubnets, + clusterConfig.IsAPIHost(host.GetCloudID()), + ); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHSyncSubnetData(app, host, network, blockchainName); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHStartNode(host); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + return + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return fmt.Errorf("failed to track subnet for node(s) %s", wgResults.GetErrorHostMap()) + } + + // update slice of subnets synced by the cluster + clusterConfig.Subnets = allSubnets + err = app.SetClusterConfig(network.ClusterName, clusterConfig) + if err != nil { + return err + } + + // update slice of blockchain endpoints with the cluster ones + networkInfo := sc.Networks[clusterConfig.Network.Name()] + rpcEndpoints := set.Of(networkInfo.RPCEndpoints...) + wsEndpoints := set.Of(networkInfo.WSEndpoints...) + publicEndpoints, err := getPublicEndpoints(app, clusterName) + if err != nil { + return err + } + for _, publicEndpoint := range publicEndpoints { + rpcEndpoints.Add(getRPCEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) + wsEndpoints.Add(getWSEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) + } + networkInfo.RPCEndpoints = rpcEndpoints.List() + networkInfo.WSEndpoints = wsEndpoints.List() + sc.Networks[clusterConfig.Network.Name()] = networkInfo + return app.UpdateSidecar(&sc) +} + +func checkHostsAreBootstrapped(hosts []*models.Host) error { + notBootstrappedNodes, err := getNotBootstrappedNodes(hosts) + if err != nil { + return err + } + if len(notBootstrappedNodes) > 0 { + return fmt.Errorf("node(s) %s are not bootstrapped yet, please try again later", notBootstrappedNodes) + } + return nil +} + +func checkHostsAreHealthy(hosts []*models.Host) error { + ux.Logger.PrintToUser("Checking if node(s) are healthy...") + unhealthyNodes, err := getUnhealthyNodes(hosts) + if err != nil { + return err + } + if len(unhealthyNodes) > 0 { + return fmt.Errorf("node(s) %s are not healthy, please check the issue and try again later", unhealthyNodes) + } + return nil +} + +func getNotBootstrappedNodes(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.RunSSHCheckBootstrapped(host); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + return + } else { + if isBootstrapped, err := parseBootstrappedOutput(resp); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + } else { + nodeResults.AddResult(host.GetCloudID(), isBootstrapped, err) + } + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return nil, fmt.Errorf("failed to get avalanchego bootstrap status for node(s) %s", wgResults.GetErrorHostMap()) + } + return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool { + return !wgResults.GetResultMap()[nodeID].(bool) + }), nil +} + +func parseBootstrappedOutput(byteValue []byte) (bool, error) { + var result map[string]interface{} + if err := json.Unmarshal(byteValue, &result); err != nil { + return false, err + } + isBootstrappedInterface, ok := result["result"].(map[string]interface{}) + if ok { + isBootstrapped, ok := isBootstrappedInterface["isBootstrapped"].(bool) + if ok { + return isBootstrapped, nil + } + } + return false, errors.New("unable to parse node bootstrap status") +} diff --git a/pkg/subnet/helpers.go b/pkg/subnet/helpers.go index 49b1e0b80..9a55ea23a 100644 --- a/pkg/subnet/helpers.go +++ b/pkg/subnet/helpers.go @@ -3,10 +3,22 @@ package subnet import ( + "encoding/json" + "errors" + "fmt" "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/key" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" + "os" + "path/filepath" + "unicode" +) + +var ( + errIllegalNameCharacter = errors.New( + "illegal name character: only letters, no special characters allowed") ) func GetDefaultSubnetAirdropKeyInfo(app *application.Avalanche, subnetName string) (string, string, string, error) { @@ -67,3 +79,66 @@ func GetSubnetAirdropKeyInfo( } return "", "", "", nil } + +func ValidateSubnetNameAndGetChains(app *application.Avalanche, args []string) ([]string, error) { + // this should not be necessary but some bright guy might just be creating + // the genesis by hand or something... + if err := checkInvalidSubnetNames(args[0]); err != nil { + return nil, fmt.Errorf("subnet name %s is invalid: %w", args[0], err) + } + // Check subnet exists + // TODO create a file that lists chains by subnet for fast querying + chains, err := getChainsInSubnet(app, args[0]) + if err != nil { + return nil, fmt.Errorf("failed to getChainsInSubnet: %w", err) + } + + if len(chains) == 0 { + return nil, errors.New("Invalid subnet " + args[0]) + } + + return chains, nil +} + +func checkInvalidSubnetNames(name string) error { + // this is currently exactly the same code as in avalanchego/vms/platformvm/create_chain_tx.go + for _, r := range name { + if r > unicode.MaxASCII || !(unicode.IsLetter(r) || unicode.IsNumber(r) || r == ' ') { + return errIllegalNameCharacter + } + } + return nil +} + +func getChainsInSubnet(app *application.Avalanche, blockchainName string) ([]string, error) { + subnets, err := os.ReadDir(app.GetSubnetDir()) + if err != nil { + return nil, fmt.Errorf("failed to read baseDir: %w", err) + } + + chains := []string{} + + for _, s := range subnets { + if !s.IsDir() { + continue + } + sidecarFile := filepath.Join(app.GetSubnetDir(), s.Name(), constants.SidecarFileName) + if _, err := os.Stat(sidecarFile); err == nil { + // read in sidecar file + jsonBytes, err := os.ReadFile(sidecarFile) + if err != nil { + return nil, fmt.Errorf("failed reading file %s: %w", sidecarFile, err) + } + + var sc models.Sidecar + err = json.Unmarshal(jsonBytes, &sc) + if err != nil { + return nil, fmt.Errorf("failed unmarshaling file %s: %w", sidecarFile, err) + } + if sc.Subnet == blockchainName { + chains = append(chains, sc.Name) + } + } + } + return chains, nil +} From bb7d561947afced549bb18f515ba3b310f5638a7 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 8 Oct 2024 17:44:32 -0400 Subject: [PATCH 2/3] fix lint --- pkg/node/helper.go | 3 ++- pkg/node/sync.go | 3 ++- pkg/subnet/helpers.go | 13 ++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/node/helper.go b/pkg/node/helper.go index 4e94f1e25..ba3566f06 100644 --- a/pkg/node/helper.go +++ b/pkg/node/helper.go @@ -5,6 +5,8 @@ package node import ( "encoding/json" "fmt" + "sync" + "github.com/ava-labs/avalanche-cli/pkg/application" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -12,7 +14,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/api/info" - "sync" ) func checkCluster(app *application.Avalanche, clusterName string) error { diff --git a/pkg/node/sync.go b/pkg/node/sync.go index 9f5d609c1..56ac65ed8 100644 --- a/pkg/node/sync.go +++ b/pkg/node/sync.go @@ -6,6 +6,8 @@ import ( "encoding/json" "errors" "fmt" + "sync" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/application" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -14,7 +16,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/utils/set" - "sync" ) func SyncSubnet(app *application.Avalanche, clusterName, blockchainName string, avoidChecks bool, subnetAliases []string) error { diff --git a/pkg/subnet/helpers.go b/pkg/subnet/helpers.go index 9a55ea23a..332602b68 100644 --- a/pkg/subnet/helpers.go +++ b/pkg/subnet/helpers.go @@ -6,20 +6,19 @@ import ( "encoding/json" "errors" "fmt" + "os" + "path/filepath" + "unicode" + "github.com/ava-labs/avalanche-cli/pkg/application" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/key" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" - "os" - "path/filepath" - "unicode" ) -var ( - errIllegalNameCharacter = errors.New( - "illegal name character: only letters, no special characters allowed") -) +var errIllegalNameCharacter = errors.New( + "illegal name character: only letters, no special characters allowed") func GetDefaultSubnetAirdropKeyInfo(app *application.Avalanche, subnetName string) (string, string, string, error) { keyName := utils.GetDefaultBlockchainAirdropKeyName(subnetName) From cc135b17e185fba520ab1237d337e46a224d5ffd Mon Sep 17 00:00:00 2001 From: sukantoraymond Date: Wed, 9 Oct 2024 17:10:16 -0400 Subject: [PATCH 3/3] Remove redundancies (#2229) * remove redundant code * fix lint * fix lint * Convert Subnet in 1 command (#2230) * one command deploy * use default private key to deploy validator manager contract * fix lint * remove redundant code * address comments --------- Signed-off-by: sukantoraymond --- cmd/blockchaincmd/deploy.go | 105 +++++++------- cmd/nodecmd/create.go | 8 +- cmd/nodecmd/create_devnet.go | 6 +- cmd/nodecmd/deploy.go | 10 +- cmd/nodecmd/destroy.go | 43 +----- cmd/nodecmd/dynamic_ips.go | 6 +- cmd/nodecmd/export.go | 4 +- cmd/nodecmd/helpers.go | 249 -------------------------------- cmd/nodecmd/import.go | 4 +- cmd/nodecmd/list.go | 4 +- cmd/nodecmd/load_test_start.go | 9 +- cmd/nodecmd/load_test_stop.go | 8 +- cmd/nodecmd/refresh_ips.go | 4 +- cmd/nodecmd/resize.go | 8 +- cmd/nodecmd/scp.go | 6 +- cmd/nodecmd/ssh.go | 6 +- cmd/nodecmd/status.go | 12 +- cmd/nodecmd/sync.go | 166 +-------------------- cmd/nodecmd/update_subnet.go | 12 +- cmd/nodecmd/upgrade.go | 6 +- cmd/nodecmd/validate_primary.go | 10 +- cmd/nodecmd/validate_subnet.go | 10 +- cmd/nodecmd/whitelist.go | 8 +- cmd/nodecmd/wiz.go | 63 +------- go.mod | 3 +- go.sum | 8 +- pkg/node/helper.go | 124 +++++++++++++--- pkg/node/sync.go | 24 +-- 28 files changed, 274 insertions(+), 652 deletions(-) diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index 78e19cf7a..11e3239d4 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ethereum/go-ethereum/common" @@ -684,62 +686,59 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { return err } - if false { - chainSpec := contract.ChainSpec{ + clusterName, err := node.GetClusterNameFromList(app) + if err != nil { + return err + } + + if err = node.SyncSubnet(app, clusterName, blockchainName, true, nil); err != nil { + return err + } + + if err := node.WaitForHealthyCluster(app, clusterName, node.HealthCheckTimeout, node.HealthCheckPoolTime); err != nil { + return err + } + + chainSpec := contract.ChainSpec{ + BlockchainName: blockchainName, + } + _, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + rpcURL, _, err := contract.GetBlockchainEndpoints( + app, + network, + chainSpec, + true, + false, + ) + if err != nil { + return err + } + if err := validatormanager.SetupPoA( + app, + network, + rpcURL, + contract.ChainSpec{ BlockchainName: blockchainName, - } - genesisAddress, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( - app, - network, - chainSpec, - ) - if err != nil { - return err - } - privateKey, err := privateKeyFlags.GetPrivateKey(app, genesisPrivateKey) - if err != nil { - return err - } - if privateKey == "" { - privateKey, err = prompts.PromptPrivateKey( - app.Prompt, - "Which key to you want to use to pay for initializing Validator Manager contract? (Uses Blockchain gas token)", - app.GetKeyDir(), - app.GetKey, - genesisAddress, - genesisPrivateKey, - ) - if err != nil { - return err - } - } - rpcURL, _, err := contract.GetBlockchainEndpoints( - app, - network, - chainSpec, - true, - false, - ) - if err != nil { - return err - } - if err := validatormanager.SetupPoA( - app, - network, - rpcURL, - contract.ChainSpec{ - BlockchainName: blockchainName, - }, - privateKey, - common.HexToAddress(sidecar.PoAValidatorManagerOwner), - avaGoBootstrapValidators, - ); err != nil { - return err - } - ux.Logger.GreenCheckmarkToUser("Subnet is successfully converted into Subnet Only Validator") + }, + genesisPrivateKey, + common.HexToAddress(sidecar.PoAValidatorManagerOwner), + avaGoBootstrapValidators, + ); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("L1 is successfully converted to sovereign blockchain") + } else { + if err := app.UpdateSidecarNetworks(&sidecar, network, subnetID, blockchainID, "", "", nil); err != nil { + return err } } - flags := make(map[string]string) flags[constants.MetricsNetwork] = network.Name() metrics.HandleTracking(cmd, constants.MetricsSubnetDeployCommand, app, flags) diff --git a/cmd/nodecmd/create.go b/cmd/nodecmd/create.go index 5f8d66553..1d1a14a2f 100644 --- a/cmd/nodecmd/create.go +++ b/cmd/nodecmd/create.go @@ -14,6 +14,8 @@ import ( "sync" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" "github.com/ava-labs/avalanche-cli/pkg/docker" @@ -204,7 +206,7 @@ func preCreateChecks(clusterName string) error { } func checkClusterExternal(clusterName string) (bool, error) { - clusterExists, err := checkClusterExists(clusterName) + clusterExists, err := node.CheckClusterExists(app, clusterName) if err != nil { return false, fmt.Errorf("error checking cluster: %w", err) } @@ -392,7 +394,7 @@ func createNodes(cmd *cobra.Command, args []string) error { } else { if cloudService == constants.AWSCloudService { // Get AWS Credential, region and AMI - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.AWSCloudService) != nil) { + if !(authorizeAccess || node.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) { return fmt.Errorf("cloud access is required") } ec2SvcMap, ami, numNodesMap, err := getAWSCloudConfig(awsProfile, false, nil, nodeType) @@ -464,7 +466,7 @@ func createNodes(cmd *cobra.Command, args []string) error { } } } else { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || node.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return fmt.Errorf("cloud access is required") } // Get GCP Credential, zone, Image ID, service account key file path, and GCP project name diff --git a/cmd/nodecmd/create_devnet.go b/cmd/nodecmd/create_devnet.go index 397556b3f..08caae6bb 100644 --- a/cmd/nodecmd/create_devnet.go +++ b/cmd/nodecmd/create_devnet.go @@ -13,6 +13,8 @@ import ( "sync" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + "golang.org/x/exp/slices" "github.com/ava-labs/avalanche-cli/pkg/ansible" @@ -155,7 +157,7 @@ func generateCustomGenesis( } func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[string]string) error { - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } inventoryPath := app.GetAnsibleInventoryDirPath(clusterName) @@ -178,7 +180,7 @@ func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[stri } else { endpointIP = ansibleHosts[ansibleHostIDs[0]].IP } - endpoint := getAvalancheGoEndpoint(endpointIP) + endpoint := node.GetAvalancheGoEndpoint(endpointIP) network := models.NewDevnetNetwork(endpoint, 0) network = models.NewNetworkFromCluster(network, clusterName) diff --git a/cmd/nodecmd/deploy.go b/cmd/nodecmd/deploy.go index 1ba839e58..b5fe10511 100644 --- a/cmd/nodecmd/deploy.go +++ b/cmd/nodecmd/deploy.go @@ -5,6 +5,8 @@ package nodecmd import ( "fmt" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -41,7 +43,7 @@ It saves the deploy info both locally and remotely. func deploySubnet(cmd *cobra.Command, args []string) error { clusterName := args[0] subnetName := args[1] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { @@ -58,12 +60,12 @@ func deploySubnet(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) if !avoidChecks { - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } - if err := checkHostsAreRPCCompatible(hosts, subnetName); err != nil { + if err := node.CheckHostsAreRPCCompatible(app, hosts, subnetName); err != nil { return err } } diff --git a/cmd/nodecmd/destroy.go b/cmd/nodecmd/destroy.go index 9153dc0f3..a3a3f6997 100644 --- a/cmd/nodecmd/destroy.go +++ b/cmd/nodecmd/destroy.go @@ -8,6 +8,8 @@ import ( "os" "strings" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -155,7 +157,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { return Cleanup() } clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := nodePkg.CheckCluster(app, clusterName); err != nil { return err } isExternalCluster, err := checkClusterExternal(clusterName) @@ -169,7 +171,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { if err := getDeleteConfigConfirmation(); err != nil { return err } - nodesToStop, err := getClusterNodes(clusterName) + nodesToStop, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -236,7 +238,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { continue } if nodeConfig.CloudService == "" || nodeConfig.CloudService == constants.AWSCloudService { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.AWSCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) { return fmt.Errorf("cloud access is required") } if err = ec2SvcMap[nodeConfig.Region].DestroyAWSNode(nodeConfig, clusterName); err != nil { @@ -258,7 +260,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { } } } else { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return fmt.Errorf("cloud access is required") } if gcpCloud == nil { @@ -328,36 +330,3 @@ func getClusterMonitoringNode(clusterName string) (string, error) { } return clustersConfig.Clusters[clusterName].MonitoringInstance, nil } - -func checkCluster(clusterName string) error { - _, err := getClusterNodes(clusterName) - return err -} - -func checkClusterExists(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 getClusterNodes(clusterName string) ([]string, error) { - if exists, err := checkClusterExists(clusterName); err != nil || !exists { - return nil, fmt.Errorf("cluster %q not found", clusterName) - } - clustersConfig, err := app.LoadClustersConfig() - if err != nil { - return nil, err - } - clusterNodes := clustersConfig.Clusters[clusterName].Nodes - if len(clusterNodes) == 0 { - return nil, fmt.Errorf("no nodes found in cluster %s", clusterName) - } - return clusterNodes, nil -} diff --git a/cmd/nodecmd/dynamic_ips.go b/cmd/nodecmd/dynamic_ips.go index 0916d2560..4f5599efe 100644 --- a/cmd/nodecmd/dynamic_ips.go +++ b/cmd/nodecmd/dynamic_ips.go @@ -6,6 +6,8 @@ import ( "context" "fmt" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" @@ -51,7 +53,7 @@ func getPublicIPsForNodesWithDynamicIP(nodesWithDynamicIP []models.NodeConfig) ( } var publicIP map[string]string if node.CloudService == constants.GCPCloudService { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return nil, fmt.Errorf("cloud access is required") } if gcpCloud == nil { @@ -87,7 +89,7 @@ func getPublicIPsForNodesWithDynamicIP(nodesWithDynamicIP []models.NodeConfig) ( // - in ansible inventory file // - in host config file func updatePublicIPs(clusterName string) error { - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } diff --git a/cmd/nodecmd/export.go b/cmd/nodecmd/export.go index 02baa4dba..d25b27e12 100644 --- a/cmd/nodecmd/export.go +++ b/cmd/nodecmd/export.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -51,7 +53,7 @@ func exportFile(_ *cobra.Command, args []string) error { ux.Logger.RedXToUser("file already exists, use --force to overwrite") return nil } - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { ux.Logger.RedXToUser("cluster not found: %v", err) return err } diff --git a/cmd/nodecmd/helpers.go b/cmd/nodecmd/helpers.go index bf004988f..5df43d2c2 100644 --- a/cmd/nodecmd/helpers.go +++ b/cmd/nodecmd/helpers.go @@ -2,20 +2,6 @@ // See the file LICENSE for licensing terms. package nodecmd -import ( - "encoding/json" - "errors" - "fmt" - "sync" - - "github.com/ava-labs/avalanche-cli/pkg/constants" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/ssh" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/api/info" -) - // NumNodes is a struct to hold number of nodes with and without stake type NumNodes struct { numValidators int // with stake @@ -25,238 +11,3 @@ type NumNodes struct { func (n NumNodes) All() int { return n.numValidators + n.numAPI } - -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 getNotBootstrappedNodes(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.RunSSHCheckBootstrapped(host); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - return - } else { - if isBootstrapped, err := parseBootstrappedOutput(resp); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - } else { - nodeResults.AddResult(host.GetCloudID(), isBootstrapped, err) - } - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return nil, fmt.Errorf("failed to get avalanchego bootstrap status for node(s) %s", wgResults.GetErrorHostMap()) - } - return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool { - return !wgResults.GetResultMap()[nodeID].(bool) - }), nil -} - -func parseBootstrappedOutput(byteValue []byte) (bool, error) { - var result map[string]interface{} - if err := json.Unmarshal(byteValue, &result); err != nil { - return false, err - } - isBootstrappedInterface, ok := result["result"].(map[string]interface{}) - if ok { - isBootstrapped, ok := isBootstrappedInterface["isBootstrapped"].(bool) - if ok { - return isBootstrapped, nil - } - } - return false, errors.New("unable to parse node bootstrap status") -} - -func getRPCIncompatibleNodes(hosts []*models.Host, subnetName string) ([]string, error) { - ux.Logger.PrintToUser("Checking compatibility of node(s) avalanche go RPC protocol version with Subnet EVM RPC of subnet %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.RunSSHCheckAvalancheGoVersion(host); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - return - } else { - if _, rpcVersion, err := parseAvalancheGoOutput(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 Avalanche Go RPC version is %d", sc.RPCVersion)) - } - return incompatibleNodes, nil -} - -func parseAvalancheGoOutput(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 authorizedAccessFromSettings() bool { - return app.Conf.GetConfigBoolValue(constants.ConfigAuthorizeCloudAccessKey) -} - -func checkHostsAreRPCCompatible(hosts []*models.Host, subnetName string) error { - incompatibleNodes, err := getRPCIncompatibleNodes(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 Avalanche Go version or modify your VM version") - ux.Logger.PrintToUser("To modify your Avalanche Go version: https://docs.avax.network/nodes/maintain/upgrade-your-avalanchego-node") - switch sc.VM { - case models.SubnetEvm: - ux.Logger.PrintToUser("To modify your Subnet-EVM version: https://docs.avax.network/build/subnet/upgrade/upgrade-subnet-vm") - case models.CustomVM: - ux.Logger.PrintToUser("To modify your Custom VM binary: avalanche subnet upgrade vm %s --config", subnetName) - } - ux.Logger.PrintToUser("Yoy can use \"avalanche node upgrade\" to upgrade Avalanche Go and/or Subnet-EVM to their latest versions") - return fmt.Errorf("the Avalanche Go version of node(s) %s is incompatible with VM RPC version of %s", incompatibleNodes, subnetName) - } - return nil -} - -func checkHostsAreHealthy(hosts []*models.Host) error { - ux.Logger.PrintToUser("Checking if node(s) are healthy...") - unhealthyNodes, err := getUnhealthyNodes(hosts) - if err != nil { - return err - } - if len(unhealthyNodes) > 0 { - return fmt.Errorf("node(s) %s are not healthy, please check the issue and try again later", unhealthyNodes) - } - return nil -} - -func checkHostsAreBootstrapped(hosts []*models.Host) error { - notBootstrappedNodes, err := getNotBootstrappedNodes(hosts) - if err != nil { - return err - } - if len(notBootstrappedNodes) > 0 { - return fmt.Errorf("node(s) %s are not bootstrapped yet, please try again later", notBootstrappedNodes) - } - return nil -} - -func getAvalancheGoEndpoint(ip string) string { - return fmt.Sprintf("http://%s:%d", ip, constants.AvalanchegoAPIPort) -} - -func getRPCEndpoint(endpoint string, blockchainID string) string { - return models.NewDevnetNetwork(endpoint, 0).BlockchainEndpoint(blockchainID) -} - -func getWSEndpoint(endpoint string, blockchainID string) string { - return models.NewDevnetNetwork(endpoint, 0).BlockchainWSEndpoint(blockchainID) -} - -func getPublicEndpoints( - 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 utils.Belongs(publicNodes, tracker.GetCloudID()) - }) - endpoints := utils.Map(publicTrackers, func(tracker *models.Host) string { - return getAvalancheGoEndpoint(tracker.IP) - }) - return endpoints, nil -} diff --git a/cmd/nodecmd/import.go b/cmd/nodecmd/import.go index e3c397653..6c76a8c03 100644 --- a/cmd/nodecmd/import.go +++ b/cmd/nodecmd/import.go @@ -9,6 +9,8 @@ import ( "os" "path/filepath" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -42,7 +44,7 @@ affecting cloud nodes like node create or node destroy will be not applicable to func importFile(_ *cobra.Command, args []string) error { clusterName := args[0] - if clusterExists, err := checkClusterExists(clusterName); clusterExists || err != nil { + if clusterExists, err := node.CheckClusterExists(app, clusterName); clusterExists || err != nil { ux.Logger.RedXToUser("cluster %s already exists, please use a different name", clusterName) return nil } diff --git a/cmd/nodecmd/list.go b/cmd/nodecmd/list.go index 16588681d..39d86ddc0 100644 --- a/cmd/nodecmd/list.go +++ b/cmd/nodecmd/list.go @@ -6,6 +6,8 @@ import ( "sort" "strings" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/ux" @@ -44,7 +46,7 @@ func list(_ *cobra.Command, _ []string) error { sort.Strings(clusterNames) for _, clusterName := range clusterNames { clusterConf := clustersConfig.Clusters[clusterName] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } nodeIDs := []string{} diff --git a/cmd/nodecmd/load_test_start.go b/cmd/nodecmd/load_test_start.go index dea2296d1..dab9720dd 100644 --- a/cmd/nodecmd/load_test_start.go +++ b/cmd/nodecmd/load_test_start.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/docker" "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/node" "github.com/ava-labs/avalanche-cli/pkg/prompts" "github.com/ava-labs/avalanche-cli/pkg/ssh" "github.com/ava-labs/avalanche-cli/pkg/utils" @@ -83,7 +84,7 @@ The command will then run the load test binary based on the provided load test r } func preLoadTestChecks(clusterName string) error { - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if useAWS && useGCP { @@ -98,7 +99,7 @@ func preLoadTestChecks(clusterName string) error { if useSSHAgent && !utils.IsSSHAgentAvailable() { return fmt.Errorf("ssh agent is not available") } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := node.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -134,7 +135,7 @@ func startLoadTest(_ *cobra.Command, args []string) error { return err } } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := node.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -409,7 +410,7 @@ func createClusterYAMLFile(clusterName, subnetID, chainID string, separateHost * if err != nil { return err } - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } var apiNodes []nodeInfo diff --git a/cmd/nodecmd/load_test_stop.go b/cmd/nodecmd/load_test_stop.go index 8c5f42fbb..00a31881b 100644 --- a/cmd/nodecmd/load_test_stop.go +++ b/cmd/nodecmd/load_test_stop.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" @@ -102,7 +104,7 @@ func stopLoadTest(_ *cobra.Command, args []string) error { if err != nil { return err } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -214,7 +216,7 @@ func destroyNode(node, clusterName, loadTestName string, ec2Svc *awsAPI.AwsCloud return err } if nodeConfig.CloudService == "" || nodeConfig.CloudService == constants.AWSCloudService { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.AWSCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) { return fmt.Errorf("cloud access is required") } if err = ec2Svc.DestroyAWSNode(nodeConfig, ""); err != nil { @@ -229,7 +231,7 @@ func destroyNode(node, clusterName, loadTestName string, ec2Svc *awsAPI.AwsCloud ux.Logger.PrintToUser("node %s is already destroyed", nodeConfig.NodeID) } } else { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return fmt.Errorf("cloud access is required") } if err = gcpClient.DestroyGCPNode(nodeConfig, ""); err != nil { diff --git a/cmd/nodecmd/refresh_ips.go b/cmd/nodecmd/refresh_ips.go index c6f9f21c2..f4d7bbde1 100644 --- a/cmd/nodecmd/refresh_ips.go +++ b/cmd/nodecmd/refresh_ips.go @@ -5,6 +5,8 @@ package nodecmd import ( "fmt" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/spf13/cobra" @@ -29,7 +31,7 @@ and updates the local node information used by CLI commands.`, func refreshIPs(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if err := failForExternal(clusterName); err != nil { diff --git a/cmd/nodecmd/resize.go b/cmd/nodecmd/resize.go index fe67b1557..2d495f706 100644 --- a/cmd/nodecmd/resize.go +++ b/cmd/nodecmd/resize.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" @@ -61,13 +63,13 @@ func preResizeChecks(clusterName string) error { func resize(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := nodePkg.CheckCluster(app, clusterName); err != nil { return err } if err := preResizeChecks(clusterName); err != nil { return err } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -105,7 +107,7 @@ func resize(_ *cobra.Command, args []string) error { if err != nil { return err } - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(nodeConfig.CloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(nodeConfig.CloudService) != nil) { return fmt.Errorf("cloud access is required") } spinSession := ux.NewUserSpinner() diff --git a/cmd/nodecmd/scp.go b/cmd/nodecmd/scp.go index 44f6fc55b..20c426319 100644 --- a/cmd/nodecmd/scp.go +++ b/cmd/nodecmd/scp.go @@ -9,6 +9,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" @@ -73,11 +75,11 @@ func scpNode(_ *cobra.Command, args []string) error { destClusterNameOrNodeID, destPath := utils.SplitSCPPath(destPath) // check if source and destination are both clusters - sourceClusterExists, err := checkClusterExists(sourceClusterNameOrNodeID) + sourceClusterExists, err := node.CheckClusterExists(app, sourceClusterNameOrNodeID) if err != nil { return err } - destClusterExists, err := checkClusterExists(destClusterNameOrNodeID) + destClusterExists, err := node.CheckClusterExists(app, destClusterNameOrNodeID) if err != nil { return err } diff --git a/cmd/nodecmd/ssh.go b/cmd/nodecmd/ssh.go index b3b8d33b5..effb613cd 100644 --- a/cmd/nodecmd/ssh.go +++ b/cmd/nodecmd/ssh.go @@ -10,6 +10,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -72,7 +74,7 @@ func sshNode(_ *cobra.Command, args []string) error { } else { clusterNameOrNodeID := args[0] cmd := strings.Join(args[1:], " ") - if err := checkCluster(clusterNameOrNodeID); err == nil { + if err := node.CheckCluster(app, clusterNameOrNodeID); err == nil { // clusterName detected if len(args[1:]) == 0 { return printClusterConnectionString(clusterNameOrNodeID, clustersConfig.Clusters[clusterNameOrNodeID].Network.Kind.String()) @@ -219,7 +221,7 @@ func printClusterConnectionString(clusterName string, networkName string) error // GetAllClusterHosts returns all hosts in a cluster including loadtest and monitoring hosts func GetAllClusterHosts(clusterName string) ([]*models.Host, error) { - if exists, err := checkClusterExists(clusterName); err != nil || !exists { + if exists, err := node.CheckClusterExists(app, clusterName); err != nil || !exists { return nil, fmt.Errorf("cluster %s not found", clusterName) } clusterHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) diff --git a/cmd/nodecmd/status.go b/cmd/nodecmd/status.go index dc499cc18..58b83e013 100644 --- a/cmd/nodecmd/status.go +++ b/cmd/nodecmd/status.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -50,7 +52,7 @@ func statusNode(_ *cobra.Command, args []string) error { return list(nil, nil) } clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clusterConf, err := app.GetClusterConfig(clusterName) @@ -87,11 +89,11 @@ func statusNode(_ *cobra.Command, args []string) error { if err != nil { return err } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) spinSession := ux.NewUserSpinner() spinner := spinSession.SpinToUser("Checking node(s) status...") - notBootstrappedNodes, err := getNotBootstrappedNodes(hosts) + notBootstrappedNodes, err := node.GetNotBootstrappedNodes(hosts) if err != nil { ux.SpinFailWithError(spinner, "", err) return err @@ -99,7 +101,7 @@ func statusNode(_ *cobra.Command, args []string) error { ux.SpinComplete(spinner) spinner = spinSession.SpinToUser("Checking if node(s) are healthy...") - unhealthyNodes, err := getUnhealthyNodes(hosts) + unhealthyNodes, err := node.GetUnhealthyNodes(hosts) if err != nil { ux.SpinFailWithError(spinner, "", err) return err @@ -117,7 +119,7 @@ func statusNode(_ *cobra.Command, args []string) error { nodeResults.AddResult(host.GetCloudID(), nil, err) return } else { - if avalancheGoVersion, _, err := parseAvalancheGoOutput(resp); err != nil { + if avalancheGoVersion, _, err := node.ParseAvalancheGoOutput(resp); err != nil { nodeResults.AddResult(host.GetCloudID(), nil, err) } else { nodeResults.AddResult(host.GetCloudID(), avalancheGoVersion, err) diff --git a/cmd/nodecmd/sync.go b/cmd/nodecmd/sync.go index 1eeea041e..134117739 100644 --- a/cmd/nodecmd/sync.go +++ b/cmd/nodecmd/sync.go @@ -3,17 +3,8 @@ package nodecmd import ( - "fmt" - "sync" - - "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" - "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/ssh" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanche-cli/pkg/node" "github.com/spf13/cobra" ) @@ -39,158 +30,5 @@ You can check the blockchain bootstrap status by calling avalanche node status < func syncSubnet(_ *cobra.Command, args []string) error { clusterName := args[0] blockchainName := args[1] - if err := checkCluster(clusterName); err != nil { - return err - } - clusterConfig, err := app.GetClusterConfig(clusterName) - if err != nil { - return err - } - if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{blockchainName}); err != nil { - return err - } - hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) - if err != nil { - return err - } - if len(validators) != 0 { - hosts, err = filterHosts(hosts, validators) - if err != nil { - return err - } - } - defer disconnectHosts(hosts) - if !avoidChecks { - if err := checkHostsAreBootstrapped(hosts); err != nil { - return err - } - if err := checkHostsAreHealthy(hosts); err != nil { - return err - } - if err := checkHostsAreRPCCompatible(hosts, blockchainName); err != nil { - return err - } - } - if err := prepareSubnetPlugin(hosts, blockchainName); err != nil { - return err - } - if err := trackSubnet(hosts, clusterName, clusterConfig.Network, blockchainName); err != nil { - return err - } - ux.Logger.PrintToUser("Node(s) successfully started syncing with Blockchain!") - ux.Logger.PrintToUser(fmt.Sprintf("Check node blockchain syncing status with avalanche node status %s --blockchain %s", clusterName, blockchainName)) - return nil -} - -// prepareSubnetPlugin creates subnet plugin to all nodes in the cluster -func prepareSubnetPlugin(hosts []*models.Host, blockchainName string) error { - sc, err := app.LoadSidecar(blockchainName) - if err != nil { - return 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 err := ssh.RunSSHCreatePlugin(host, sc); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return fmt.Errorf("failed to upload plugin to node(s) %s", wgResults.GetErrorHostMap()) - } - return nil -} - -// trackSubnet exports deployed subnet in user's local machine to cloud server and calls node to -// start tracking the specified subnet (similar to avalanche subnet join command) -func trackSubnet( - hosts []*models.Host, - clusterName string, - network models.Network, - blockchainName string, -) error { - // load cluster config - clusterConfig, err := app.GetClusterConfig(clusterName) - if err != nil { - return err - } - // and get list of subnets - allSubnets := utils.Unique(append(clusterConfig.Subnets, blockchainName)) - - // load sidecar to get subnet blockchain ID - sc, err := app.LoadSidecar(blockchainName) - if err != nil { - return err - } - blockchainID := sc.Networks[network.Name()].BlockchainID - - wg := sync.WaitGroup{} - wgResults := models.NodeResults{} - subnetAliases := append([]string{blockchainName}, subnetAliases...) - for _, host := range hosts { - wg.Add(1) - go func(nodeResults *models.NodeResults, host *models.Host) { - defer wg.Done() - if err := ssh.RunSSHStopNode(host); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - - if err := ssh.RunSSHRenderAvagoAliasConfigFile( - host, - blockchainID.String(), - subnetAliases, - ); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - if err := ssh.RunSSHRenderAvalancheNodeConfig( - app, - host, - network, - allSubnets, - clusterConfig.IsAPIHost(host.GetCloudID()), - ); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - if err := ssh.RunSSHSyncSubnetData(app, host, network, blockchainName); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - if err := ssh.RunSSHStartNode(host); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - return - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return fmt.Errorf("failed to track subnet for node(s) %s", wgResults.GetErrorHostMap()) - } - - // update slice of subnets synced by the cluster - clusterConfig.Subnets = allSubnets - err = app.SetClusterConfig(network.ClusterName, clusterConfig) - if err != nil { - return err - } - - // update slice of blockchain endpoints with the cluster ones - networkInfo := sc.Networks[clusterConfig.Network.Name()] - rpcEndpoints := set.Of(networkInfo.RPCEndpoints...) - wsEndpoints := set.Of(networkInfo.WSEndpoints...) - publicEndpoints, err := getPublicEndpoints(clusterName, hosts) - if err != nil { - return err - } - for _, publicEndpoint := range publicEndpoints { - rpcEndpoints.Add(getRPCEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) - wsEndpoints.Add(getWSEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) - } - networkInfo.RPCEndpoints = rpcEndpoints.List() - networkInfo.WSEndpoints = wsEndpoints.List() - sc.Networks[clusterConfig.Network.Name()] = networkInfo - return app.UpdateSidecar(&sc) + return node.SyncSubnet(app, clusterName, blockchainName, avoidChecks, subnetAliases) } diff --git a/cmd/nodecmd/update_subnet.go b/cmd/nodecmd/update_subnet.go index 810aaf5c0..44f06ce32 100644 --- a/cmd/nodecmd/update_subnet.go +++ b/cmd/nodecmd/update_subnet.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -34,7 +36,7 @@ You can check the updated subnet bootstrap status by calling avalanche node stat func updateSubnet(_ *cobra.Command, args []string) error { clusterName := args[0] subnetName := args[1] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clusterConfig, err := app.GetClusterConfig(clusterName) @@ -48,14 +50,14 @@ func updateSubnet(_ *cobra.Command, args []string) error { if err != nil { return err } - defer disconnectHosts(hosts) - if err := checkHostsAreBootstrapped(hosts); err != nil { + defer node.DisconnectHosts(hosts) + if err := node.CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } - if err := checkHostsAreRPCCompatible(hosts, subnetName); err != nil { + if err := node.CheckHostsAreRPCCompatible(app, hosts, subnetName); err != nil { return err } nonUpdatedNodes, err := doUpdateSubnet(hosts, clusterName, clusterConfig.Network, subnetName) diff --git a/cmd/nodecmd/upgrade.go b/cmd/nodecmd/upgrade.go index 0a8d3200a..c00b23f63 100644 --- a/cmd/nodecmd/upgrade.go +++ b/cmd/nodecmd/upgrade.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -47,7 +49,7 @@ You can check the status after upgrade by calling avalanche node status`, func upgrade(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clusterConfig, err := app.GetClusterConfig(clusterName) @@ -59,7 +61,7 @@ func upgrade(_ *cobra.Command, args []string) error { if err != nil { return err } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) toUpgradeNodesMap, err := getNodesUpgradeInfo(hosts) if err != nil { return err diff --git a/cmd/nodecmd/validate_primary.go b/cmd/nodecmd/validate_primary.go index 44f1f8fec..45aaece9e 100644 --- a/cmd/nodecmd/validate_primary.go +++ b/cmd/nodecmd/validate_primary.go @@ -9,6 +9,8 @@ import ( "strconv" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + blockchaincmd "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -287,7 +289,7 @@ func addNodeAsPrimaryNetworkValidator( func validatePrimaryNetwork(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } @@ -302,7 +304,7 @@ func validatePrimaryNetwork(_ *cobra.Command, args []string) error { return err } hosts := clusterConfig.GetValidatorHosts(allHosts) // exlude api nodes - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.AddPrimaryNetworkValidatorFee * uint64(len(hosts)) kc, err := keychain.GetKeychainFromCmdLineFlags( @@ -321,10 +323,10 @@ func validatePrimaryNetwork(_ *cobra.Command, args []string) error { deployer := subnet.NewPublicDeployer(app, kc, network) - if err := checkHostsAreBootstrapped(hosts); err != nil { + if err := node.CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } diff --git a/cmd/nodecmd/validate_subnet.go b/cmd/nodecmd/validate_subnet.go index 552115426..674fa0c78 100644 --- a/cmd/nodecmd/validate_subnet.go +++ b/cmd/nodecmd/validate_subnet.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + blockchaincmd "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -173,7 +175,7 @@ func validateSubnet(_ *cobra.Command, args []string) error { clusterName := args[0] subnetName := args[1] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { @@ -197,7 +199,7 @@ func validateSubnet(_ *cobra.Command, args []string) error { return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) nodeIDMap, failedNodesMap := getNodeIDs(hosts) nonPrimaryValidators := 0 @@ -232,10 +234,10 @@ func validateSubnet(_ *cobra.Command, args []string) error { deployer := subnet.NewPublicDeployer(app, kc, network) if !avoidChecks { - if err := checkHostsAreBootstrapped(hosts); err != nil { + if err := node.CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } } diff --git a/cmd/nodecmd/whitelist.go b/cmd/nodecmd/whitelist.go index d7ec66172..4a991f097 100644 --- a/cmd/nodecmd/whitelist.go +++ b/cmd/nodecmd/whitelist.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/application" awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" @@ -61,7 +63,7 @@ type regionSecurityGroup struct { func whitelist(_ *cobra.Command, args []string) error { var err error clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if err := failForExternal(clusterName); err != nil { @@ -116,7 +118,7 @@ func whitelist(_ *cobra.Command, args []string) error { if userIPAddress != "" { ux.Logger.GreenCheckmarkToUser("Whitelisting IP: %s", logging.LightBlue.Wrap(userIPAddress)) cloudSecurityGroupList := []regionSecurityGroup{} - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := node.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -234,7 +236,7 @@ func GrantAccessToIPinGCP(userIPAddress string) error { func whitelistSSHPubKey(clusterName string, pubkey string) error { sshPubKey := strings.Trim(pubkey, "\"'") - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clustersConfig, err := app.LoadClustersConfig() diff --git a/cmd/nodecmd/wiz.go b/cmd/nodecmd/wiz.go index 640241782..6256f9721 100644 --- a/cmd/nodecmd/wiz.go +++ b/cmd/nodecmd/wiz.go @@ -257,7 +257,7 @@ func wiz(cmd *cobra.Command, args []string) error { } } - if err := waitForHealthyCluster(clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { + if err := node.WaitForHealthyCluster(app, clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { return err } @@ -349,7 +349,7 @@ func wiz(cmd *cobra.Command, args []string) error { if err := syncSubnet(cmd, []string{clusterName, subnetName}); err != nil { return err } - if err := waitForHealthyCluster(clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { + if err := node.WaitForHealthyCluster(app, clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { return err } blockchainID := sc.Networks[network.Name()].BlockchainID @@ -632,59 +632,8 @@ func checkRPCCompatibility( return err } } - defer disconnectHosts(hosts) - return checkHostsAreRPCCompatible(hosts, subnetName) -} - -func waitForHealthyCluster( - 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) - } + defer node.DisconnectHosts(hosts) + return node.CheckHostsAreRPCCompatible(app, hosts, subnetName) } func waitForSubnetValidators( @@ -710,7 +659,7 @@ func waitForSubnetValidators( return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) nodeIDMap, failedNodesMap := getNodeIDs(hosts) startTime := time.Now() for { @@ -777,7 +726,7 @@ func waitForClusterSubnetStatus( return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) startTime := time.Now() for { wg := sync.WaitGroup{} diff --git a/go.mod b/go.mod index 3776779d4..743d658dc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.8 require ( github.com/ava-labs/apm v1.0.0 github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241005224128-cc3c07bb1344 - github.com/ava-labs/avalanchego v1.12.0-initial-poc.3 + github.com/ava-labs/avalanchego v1.12.0-initial-poc.5 github.com/ava-labs/awm-relayer v1.4.1-0.20241003162124-807fd305670f github.com/ava-labs/coreth v0.13.8 github.com/ava-labs/subnet-evm v0.6.10 @@ -115,7 +115,6 @@ require ( github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/gliderlabs/ssh v0.3.7 // indirect - github.com/go-cmd/cmd v1.4.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-logr/logr v1.4.2 // indirect diff --git a/go.sum b/go.sum index f7f98a76c..fec835498 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/ava-labs/apm v1.0.0 h1:6FwozH67hEkbWVsOXNZGexBy5KLpNeYucN9zcFUHv+Q= github.com/ava-labs/apm v1.0.0/go.mod h1:TJL7pTlZNvQatsQPsLUtDHApEwVZ/qS7iSNtRFU83mc= github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241005224128-cc3c07bb1344 h1:wD/rBr+QKztcKtRtBNqPjzMhwcxnVcuJ3GT62DdgS2Q= github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241005224128-cc3c07bb1344/go.mod h1:l4QzFnujbyyyeq6oBQ4F6sw9TrTQCjD2V4vUd7ZBCCo= -github.com/ava-labs/avalanchego v1.12.0-initial-poc.3 h1:JfVooBCdMzpeGUT9/phJNl2GHflkGehlMJokXeWKa2A= -github.com/ava-labs/avalanchego v1.12.0-initial-poc.3/go.mod h1:qSHmog3wMVjo/ruIAQo0ppXAilyni07NIu5K88RyhWE= +github.com/ava-labs/avalanchego v1.12.0-initial-poc.5 h1:gW4xAqZNvkA4gP8M9yDyd7YUzuwfQbbCR+hgd1ztOto= +github.com/ava-labs/avalanchego v1.12.0-initial-poc.5/go.mod h1:qSHmog3wMVjo/ruIAQo0ppXAilyni07NIu5K88RyhWE= github.com/ava-labs/awm-relayer v1.4.1-0.20241003162124-807fd305670f h1:YUQF1wQJeEcTMC5W/OrwgSFTFMS4zeCM8O02rLeEDow= github.com/ava-labs/awm-relayer v1.4.1-0.20241003162124-807fd305670f/go.mod h1:K01Md6zPkOFRWeQyxmZ/t9HJfoNgUGqa1L8rOp35GXw= github.com/ava-labs/coreth v0.13.8 h1:f14X3KgwHl9LwzfxlN6S4bbn5VA2rhEsNnHaRLSTo/8= @@ -332,8 +332,6 @@ github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7 github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc= -github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= @@ -368,8 +366,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= -github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= diff --git a/pkg/node/helper.go b/pkg/node/helper.go index ba3566f06..d97fec3f9 100644 --- a/pkg/node/helper.go +++ b/pkg/node/helper.go @@ -6,6 +6,9 @@ import ( "encoding/json" "fmt" "sync" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/application" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -16,13 +19,22 @@ import ( "github.com/ava-labs/avalanchego/api/info" ) -func checkCluster(app *application.Avalanche, clusterName string) error { - _, err := getClusterNodes(app, clusterName) +const ( + HealthCheckPoolTime = 60 * time.Second + HealthCheckTimeout = 3 * time.Minute +) + +func AuthorizedAccessFromSettings(app *application.Avalanche) bool { + return app.Conf.GetConfigBoolValue(constants.ConfigAuthorizeCloudAccessKey) +} + +func CheckCluster(app *application.Avalanche, clusterName string) error { + _, err := GetClusterNodes(app, clusterName) return err } -func getClusterNodes(app *application.Avalanche, clusterName string) ([]string, error) { - if exists, err := checkClusterExists(app, clusterName); err != nil || !exists { +func GetClusterNodes(app *application.Avalanche, clusterName string) ([]string, error) { + if exists, err := CheckClusterExists(app, clusterName); err != nil || !exists { return nil, fmt.Errorf("cluster %q not found", clusterName) } clustersConfig, err := app.LoadClustersConfig() @@ -36,7 +48,7 @@ func getClusterNodes(app *application.Avalanche, clusterName string) ([]string, return clusterNodes, nil } -func checkClusterExists(app *application.Avalanche, clusterName string) (bool, error) { +func CheckClusterExists(app *application.Avalanche, clusterName string) (bool, error) { clustersConfig := models.ClustersConfig{} if app.ClustersConfigExists() { var err error @@ -49,7 +61,7 @@ func checkClusterExists(app *application.Avalanche, clusterName string) (bool, e return ok, nil } -func checkHostsAreRPCCompatible(app *application.Avalanche, hosts []*models.Host, subnetName string) error { +func CheckHostsAreRPCCompatible(app *application.Avalanche, hosts []*models.Host, subnetName string) error { incompatibleNodes, err := getRPCIncompatibleNodes(app, hosts, subnetName) if err != nil { return err @@ -89,7 +101,7 @@ func getRPCIncompatibleNodes(app *application.Avalanche, hosts []*models.Host, s nodeResults.AddResult(host.GetCloudID(), nil, err) return } else { - if _, rpcVersion, err := parseAvalancheGoOutput(resp); err != nil { + if _, rpcVersion, err := ParseAvalancheGoOutput(resp); err != nil { nodeResults.AddResult(host.GetCloudID(), nil, err) } else { nodeResults.AddResult(host.GetCloudID(), rpcVersion, err) @@ -114,7 +126,7 @@ func getRPCIncompatibleNodes(app *application.Avalanche, hosts []*models.Host, s return incompatibleNodes, nil } -func parseAvalancheGoOutput(byteValue []byte) (string, uint32, error) { +func ParseAvalancheGoOutput(byteValue []byte) (string, uint32, error) { reply := map[string]interface{}{} if err := json.Unmarshal(byteValue, &reply); err != nil { return "", 0, err @@ -132,7 +144,7 @@ func parseAvalancheGoOutput(byteValue []byte) (string, uint32, error) { return nodeVersionReply.VMVersions["platform"], uint32(nodeVersionReply.RPCProtocolVersion), nil } -func disconnectHosts(hosts []*models.Host) { +func DisconnectHosts(hosts []*models.Host) { for _, host := range hosts { _ = host.Disconnect() } @@ -142,8 +154,11 @@ func getWSEndpoint(endpoint string, blockchainID string) string { return models.NewDevnetNetwork(endpoint, 0).BlockchainWSEndpoint(blockchainID) } -func getPublicEndpoints(app *application.Avalanche, clusterName string) ([]string, error) { - endpoints := []string{} +func getPublicEndpoints( + app *application.Avalanche, + clusterName string, + trackers []*models.Host, +) ([]string, error) { clusterConfig, err := app.GetClusterConfig(clusterName) if err != nil { return nil, err @@ -152,13 +167,12 @@ func getPublicEndpoints(app *application.Avalanche, clusterName string) ([]strin if clusterConfig.Network.Kind == models.Devnet { publicNodes = clusterConfig.Nodes } - for _, cloudID := range publicNodes { - nodeConfig, err := app.LoadClusterNodeConfig(cloudID) - if err != nil { - return nil, err - } - endpoints = append(endpoints, getAvalancheGoEndpoint(nodeConfig.ElasticIP)) - } + publicTrackers := utils.Filter(trackers, func(tracker *models.Host) bool { + return utils.Belongs(publicNodes, tracker.GetCloudID()) + }) + endpoints := utils.Map(publicTrackers, func(tracker *models.Host) string { + return GetAvalancheGoEndpoint(tracker.IP) + }) return endpoints, nil } @@ -166,11 +180,11 @@ func getRPCEndpoint(endpoint string, blockchainID string) string { return models.NewDevnetNetwork(endpoint, 0).BlockchainEndpoint(blockchainID) } -func getAvalancheGoEndpoint(ip string) string { +func GetAvalancheGoEndpoint(ip string) string { return fmt.Sprintf("http://%s:%d", ip, constants.AvalanchegoAPIPort) } -func getUnhealthyNodes(hosts []*models.Host) ([]string, error) { +func GetUnhealthyNodes(hosts []*models.Host) ([]string, error) { wg := sync.WaitGroup{} wgResults := models.NodeResults{} for _, host := range hosts { @@ -212,3 +226,73 @@ func parseHealthyOutput(byteValue []byte) (bool, error) { } return false, fmt.Errorf("unable to parse node healthy status") } + +func WaitForHealthyCluster( + app *application.Avalanche, + 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) + } +} + +func GetClusterNameFromList(app *application.Avalanche) (string, error) { + clusterNames, err := app.ListClusterNames() + if err != nil { + return "", err + } + if len(clusterNames) == 0 { + return "", fmt.Errorf("no Avalanche nodes found that can track the blockchain, please create Avalanche nodes first through `avalanche node create`") + } + clusterName, err := app.Prompt.CaptureList( + "Which cluster of Avalanche nodes would you like to use to track the blockchain?", + clusterNames, + ) + if err != nil { + return "", err + } + return clusterName, nil +} diff --git a/pkg/node/sync.go b/pkg/node/sync.go index 56ac65ed8..d30f1c38f 100644 --- a/pkg/node/sync.go +++ b/pkg/node/sync.go @@ -19,7 +19,7 @@ import ( ) func SyncSubnet(app *application.Avalanche, clusterName, blockchainName string, avoidChecks bool, subnetAliases []string) error { - if err := checkCluster(app, clusterName); err != nil { + if err := CheckCluster(app, clusterName); err != nil { return err } clusterConfig, err := app.GetClusterConfig(clusterName) @@ -33,15 +33,15 @@ func SyncSubnet(app *application.Avalanche, clusterName, blockchainName string, if err != nil { return err } - defer disconnectHosts(hosts) + defer DisconnectHosts(hosts) if !avoidChecks { - if err := checkHostsAreBootstrapped(hosts); err != nil { + if err := CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := CheckHostsAreHealthy(hosts); err != nil { return err } - if err := checkHostsAreRPCCompatible(app, hosts, blockchainName); err != nil { + if err := CheckHostsAreRPCCompatible(app, hosts, blockchainName); err != nil { return err } } @@ -51,7 +51,7 @@ func SyncSubnet(app *application.Avalanche, clusterName, blockchainName string, if err := trackSubnet(app, hosts, clusterName, clusterConfig.Network, blockchainName, subnetAliases); err != nil { return err } - ux.Logger.PrintToUser("Node(s) successfully started syncing with Blockchain!") + ux.Logger.PrintToUser("Node(s) successfully started syncing with blockchain!") ux.Logger.PrintToUser(fmt.Sprintf("Check node blockchain syncing status with avalanche node status %s --blockchain %s", clusterName, blockchainName)) return nil } @@ -155,7 +155,7 @@ func trackSubnet( networkInfo := sc.Networks[clusterConfig.Network.Name()] rpcEndpoints := set.Of(networkInfo.RPCEndpoints...) wsEndpoints := set.Of(networkInfo.WSEndpoints...) - publicEndpoints, err := getPublicEndpoints(app, clusterName) + publicEndpoints, err := getPublicEndpoints(app, clusterName, hosts) if err != nil { return err } @@ -169,8 +169,8 @@ func trackSubnet( return app.UpdateSidecar(&sc) } -func checkHostsAreBootstrapped(hosts []*models.Host) error { - notBootstrappedNodes, err := getNotBootstrappedNodes(hosts) +func CheckHostsAreBootstrapped(hosts []*models.Host) error { + notBootstrappedNodes, err := GetNotBootstrappedNodes(hosts) if err != nil { return err } @@ -180,9 +180,9 @@ func checkHostsAreBootstrapped(hosts []*models.Host) error { return nil } -func checkHostsAreHealthy(hosts []*models.Host) error { +func CheckHostsAreHealthy(hosts []*models.Host) error { ux.Logger.PrintToUser("Checking if node(s) are healthy...") - unhealthyNodes, err := getUnhealthyNodes(hosts) + unhealthyNodes, err := GetUnhealthyNodes(hosts) if err != nil { return err } @@ -192,7 +192,7 @@ func checkHostsAreHealthy(hosts []*models.Host) error { return nil } -func getNotBootstrappedNodes(hosts []*models.Host) ([]string, error) { +func GetNotBootstrappedNodes(hosts []*models.Host) ([]string, error) { wg := sync.WaitGroup{} wgResults := models.NodeResults{} for _, host := range hosts {