diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index aa7707f6e..9ec481220 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -13,6 +13,8 @@ import ( "time" "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ethereum/go-ethereum/common" @@ -663,7 +665,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } deployer.CleanCacheWallet() managerAddress := common.HexToAddress(validatormanager.ValidatorContractAddress) - isFullySigned, ConvertL1TxID, tx, remainingSubnetAuthKeys, err := deployer.ConvertL1( + isFullySigned, convertL1TxID, tx, remainingSubnetAuthKeys, err := deployer.ConvertL1( controlKeys, subnetAuthKeys, subnetID, @@ -677,7 +679,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } savePartialTx = !isFullySigned && err == nil - ux.Logger.PrintToUser("ConvertL1Tx ID: %s", ConvertL1TxID) + ux.Logger.PrintToUser("ConvertL1Tx ID: %s", convertL1TxID) if savePartialTx { if err := SaveNotFullySignedTx( @@ -695,7 +697,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { bar, err := ux.TimedProgressBar( 30*time.Second, - "Waiting for Blockchain to be converted into Subnet Only Validator (SOV) Blockchain ...", + "Waiting for L1 to be converted into sovereign blockchain ...", 2, ) if err != nil { @@ -725,11 +727,24 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { return err } - if false { + if !generateNodeID { + 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, } - genesisAddress, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( + _, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( app, network, chainSpec, @@ -737,23 +752,6 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { 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, @@ -775,17 +773,25 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { contract.ChainSpec{ BlockchainName: blockchainName, }, - privateKey, + genesisPrivateKey, common.HexToAddress(sidecar.PoAValidatorManagerOwner), avaGoBootstrapValidators, aggregatorExtraPeerEndpoints, ); err != nil { return err } - ux.Logger.GreenCheckmarkToUser("Subnet is successfully converted into Subnet Only Validator") + ux.Logger.GreenCheckmarkToUser("L1 is successfully converted to sovereign blockchain") + } else { + ux.Logger.GreenCheckmarkToUser("Generated Node ID and BLS info for bootstrap validator(s)") + ux.Logger.PrintToUser("To convert L1 to sovereign blockchain, create the corresponding Avalanche node(s) with the provided Node ID and BLS Info") + ux.Logger.PrintToUser("Created Node ID and BLS Info can be found at %s", app.GetSidecarPath(blockchainName)) + ux.Logger.PrintToUser("Once the Avalanche Node(s) are created and are tracking the blockchain, call `avalanche contract initPoaManager %s` to finish converting L1 to sovereign blockchain", blockchainName) + } + } 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/blockchaincmd/prompt_genesis_input.go b/cmd/blockchaincmd/prompt_genesis_input.go index ea3d2bb44..25d66379c 100644 --- a/cmd/blockchaincmd/prompt_genesis_input.go +++ b/cmd/blockchaincmd/prompt_genesis_input.go @@ -129,12 +129,13 @@ func promptBootstrapValidators(network models.Network) ([]models.SubnetValidator } var setUpNodes bool if generateNodeID { - setUpNodes = true + setUpNodes = false } else { setUpNodes, err = promptSetUpNodes() if err != nil { return nil, err } + generateNodeID = !setUpNodes } previousAddr := "" for len(subnetValidators) < numBootstrapValidators { diff --git a/cmd/nodecmd/create.go b/cmd/nodecmd/create.go index 10e911909..96d3f1a72 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" @@ -211,7 +213,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) } @@ -399,7 +401,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) @@ -471,7 +473,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 41275c8fe..497d25ac2 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 { @@ -61,12 +63,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 b7e58f9db..154db3a2f 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" @@ -146,7 +148,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 } clusterConfig, err := app.GetClusterConfig(clusterName) @@ -167,7 +169,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 } @@ -234,7 +236,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 { @@ -256,7 +258,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 { @@ -322,35 +324,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, err := app.GetClustersConfig() - 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 - } - if clustersConfig.Clusters[clusterName].Local { - return []string{fmt.Sprintf("local: %s", clusterName)}, nil - } - 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 e5b0975f2..f9bd0a018 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 0a4902b58..895bf1ce8 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/ux" @@ -39,7 +41,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 35c5f8d0d..cbcb1f60d 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 62ab1b8f4..4a68360af 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" @@ -94,7 +96,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 } @@ -206,7 +208,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 { @@ -221,7 +223,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 a486d0f82..b4a56efba 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" @@ -68,13 +70,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 } @@ -112,7 +114,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 3149d0314..b722e68c9 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" @@ -69,11 +71,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 6d6dd8278..ee78f0961 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" @@ -71,7 +73,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()) @@ -221,7 +223,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 47273d4ca..9cae4bcf8 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) @@ -92,11 +94,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 @@ -104,7 +106,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 @@ -122,7 +124,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 0590bacaf..8a71328f8 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) @@ -51,14 +53,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 9600c57c3..6d8ec56a0 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) @@ -62,7 +64,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 ff8555f95..ddf755492 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 } @@ -305,7 +307,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( @@ -324,10 +326,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 3a200a30d..e73138d2d 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 { @@ -200,7 +202,7 @@ func validateSubnet(_ *cobra.Command, args []string) error { return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) nodeIDMap, failedNodesMap := getNodeIDs(hosts) nonPrimaryValidators := 0 @@ -235,10 +237,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 d3b954224..27a56b94e 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 { @@ -124,7 +126,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 } @@ -242,7 +244,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 56852d20d..e1b111f53 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.20241014184529-5bcdd0c507ec - 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.20241010130039-bceba83023b8 github.com/ava-labs/coreth v0.13.8 github.com/ava-labs/subnet-evm v0.6.10 diff --git a/go.sum b/go.sum index 3672efa9d..ed6cff884 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.20241014184529-5bcdd0c507ec h1:SPDvCcpYkG7kVDdogbW9tDYTYTIJkAeBzU4gAm3ZhXs= github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241014184529-5bcdd0c507ec/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.20241010130039-bceba83023b8 h1:M58jcqAG51RrKKVCfhAZpPqCFdkqRzahEgkFqQA5EME= github.com/ava-labs/awm-relayer v1.4.1-0.20241010130039-bceba83023b8/go.mod h1:K01Md6zPkOFRWeQyxmZ/t9HJfoNgUGqa1L8rOp35GXw= github.com/ava-labs/coreth v0.13.8 h1:f14X3KgwHl9LwzfxlN6S4bbn5VA2rhEsNnHaRLSTo/8= diff --git a/pkg/node/helper.go b/pkg/node/helper.go new file mode 100644 index 000000000..d97fec3f9 --- /dev/null +++ b/pkg/node/helper.go @@ -0,0 +1,298 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +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" + "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" +) + +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 { + 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, + 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 +} + +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") +} + +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 new file mode 100644 index 000000000..d30f1c38f --- /dev/null +++ b/pkg/node/sync.go @@ -0,0 +1,236 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +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" + "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" +) + +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, 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) +} + +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..332602b68 100644 --- a/pkg/subnet/helpers.go +++ b/pkg/subnet/helpers.go @@ -3,12 +3,23 @@ package subnet 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" ) +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) keyPath := app.GetKeyPath(keyName) @@ -67,3 +78,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 +}