Skip to content

Commit

Permalink
Wiz add subnet only, with validator's subset (ava-labs#1314)
Browse files Browse the repository at this point in the history
* add flows to only create a devnet and only add a subnet into a devnet

* nit

* add validators filtering to node sync, node validate subnet, node devnet
wiz

* filter health check only ask for P chain

* fix panic

* address PR comments
  • Loading branch information
felipemadero authored Dec 15, 2023
1 parent 8d19253 commit cfd20b4
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 19 deletions.
13 changes: 11 additions & 2 deletions cmd/nodecmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"strings"
"sync"

"github.com/ava-labs/avalanche-cli/cmd/flags"
"github.com/ava-labs/avalanche-cli/cmd/subnetcmd"
"github.com/ava-labs/avalanche-cli/pkg/ansible"
"github.com/ava-labs/avalanche-cli/pkg/ssh"
Expand All @@ -31,6 +32,7 @@ import (

"github.com/ava-labs/avalanche-cli/pkg/ux"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"
)

const (
Expand All @@ -50,6 +52,7 @@ var (
numNodes []int
nodeType string
useLatestAvalanchegoVersion bool
useCustomAvalanchegoVersion string
useAvalanchegoVersionFromSubnet string
cmdLineGCPCredentialsPath string
cmdLineGCPProjectName string
Expand Down Expand Up @@ -86,6 +89,7 @@ will apply to all nodes in the cluster`,
cmd.Flags().IntSliceVar(&numNodes, "num-nodes", []int{}, "number of nodes to create per region(s). Use comma to separate multiple numbers for each region in the same order as --region flag")
cmd.Flags().StringVar(&nodeType, "node-type", "", "cloud instance type. Use 'default' to use recommended default instance type")
cmd.Flags().BoolVar(&useLatestAvalanchegoVersion, "latest-avalanchego-version", false, "install latest avalanchego version on node/s")
cmd.Flags().StringVar(&useCustomAvalanchegoVersion, "custom-avalanchego-version", "", "install given avalanchego version on node/s")
cmd.Flags().StringVar(&useAvalanchegoVersionFromSubnet, "avalanchego-version-from-subnet", "", "install latest avalanchego version, that is compatible with the given subnet, on node/s")
cmd.Flags().StringVar(&cmdLineGCPCredentialsPath, "gcp-credentials", "", "use given GCP credentials")
cmd.Flags().StringVar(&cmdLineGCPProjectName, "gcp-project", "", "use given GCP project")
Expand All @@ -97,8 +101,8 @@ will apply to all nodes in the cluster`,
}

func preCreateChecks() error {
if useLatestAvalanchegoVersion && useAvalanchegoVersionFromSubnet != "" {
return fmt.Errorf("could not use both latest avalanchego version and avalanchego version based on given subnet")
if !flags.EnsureMutuallyExclusive([]bool{useLatestAvalanchegoVersion, useAvalanchegoVersionFromSubnet != "", useCustomAvalanchegoVersion != ""}) {
return fmt.Errorf("latest avalanchego version, custom avalanchego version and avalanchego version based on given subnet, are mutually exclusive options")
}
if useAWS && useGCP {
return fmt.Errorf("could not use both AWS and GCP cloud options")
Expand Down Expand Up @@ -498,6 +502,11 @@ func getAvalancheGoVersion() (string, error) {
subnet := ""
if useLatestAvalanchegoVersion { //nolint: gocritic
version = "latest"
} else if useCustomAvalanchegoVersion != "" {
if !semver.IsValid(useCustomAvalanchegoVersion) {
return "", errors.New("custom avalanchego version must be a legal semantic version (ex: v1.1.1)")
}
version = useCustomAvalanchegoVersion
} else if useAvalanchegoVersionFromSubnet != "" {
subnet = useAvalanchegoVersionFromSubnet
} else {
Expand Down
2 changes: 1 addition & 1 deletion cmd/nodecmd/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func stopNodes(_ *cobra.Command, args []string) error {
if len(nodeErrors) > 0 {
ux.Logger.PrintToUser("Failed nodes: ")
for node, nodeErr := range nodeErrors {
if strings.Contains(err.Error(), constants.ErrReleasingGCPStaticIP) {
if strings.Contains(nodeErr.Error(), constants.ErrReleasingGCPStaticIP) {
ux.Logger.PrintToUser(fmt.Sprintf("Node is stopped, but failed to release static ip address for node %s due to %s", node, nodeErr))
} else {
ux.Logger.PrintToUser(fmt.Sprintf("Failed to stop node %s due to %s", node, nodeErr))
Expand Down
8 changes: 8 additions & 0 deletions cmd/nodecmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ You can check the subnet bootstrap status by calling avalanche node status <clus
RunE: syncSubnet,
}

cmd.Flags().StringSliceVar(&validators, "validators", []string{}, "sync subnet into given comma separated list of validators. defaults to all cluster nodes")

return cmd
}

Expand Down Expand Up @@ -129,6 +131,12 @@ func syncSubnet(_ *cobra.Command, args []string) error {
if err != nil {
return err
}
if len(validators) != 0 {
hosts, err = filterHosts(hosts, validators)
if err != nil {
return err
}
}
defer disconnectHosts(hosts)
notBootstrappedNodes, err := checkHostsAreBootstrapped(hosts)
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions cmd/nodecmd/validateSubnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ You can check the subnet sync status by calling avalanche node status <clusterNa
cmd.Flags().StringVar(&startTimeStr, "start-time", "", "UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format")
cmd.Flags().BoolVar(&defaultValidatorParams, "default-validator-params", false, "use default weight/start/duration params for subnet validator")

cmd.Flags().StringSliceVar(&validators, "validators", []string{}, "validate subnet for the given comma separated list of validators. defaults to all cluster nodes")

return cmd
}

Expand Down Expand Up @@ -159,6 +161,12 @@ func validateSubnet(_ *cobra.Command, args []string) error {
if err != nil {
return err
}
if len(validators) != 0 {
hosts, err = filterHosts(hosts, validators)
if err != nil {
return err
}
}
defer disconnectHosts(hosts)

nodeIDMap, failedNodesMap := getNodeIDs(hosts)
Expand Down
126 changes: 111 additions & 15 deletions cmd/nodecmd/wiz.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import (

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/utils/set"
"github.com/ava-labs/avalanchego/vms/platformvm/status"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
)

const (
Expand All @@ -43,6 +45,7 @@ var (
nodeConf string
subnetConf string
chainConf string
validators []string
)

func newWizCmd() *cobra.Command {
Expand All @@ -54,7 +57,7 @@ func newWizCmd() *cobra.Command {
The node wiz command creates a devnet and deploys, sync and validate a subnet into it. It creates the subnet if so needed.
`,
SilenceUsage: true,
Args: cobra.ExactArgs(2),
Args: cobra.RangeArgs(1, 2),
RunE: wiz,
}
cmd.Flags().BoolVar(&useStaticIP, "use-static-ip", true, "attach static Public IP on cloud servers")
Expand Down Expand Up @@ -82,20 +85,31 @@ The node wiz command creates a devnet and deploys, sync and validate a subnet in
cmd.Flags().StringVar(&nodeConf, "node-config", "", "path to avalanchego node configuration for subnet")
cmd.Flags().StringVar(&subnetConf, "subnet-config", "", "path to the subnet configuration for subnet")
cmd.Flags().StringVar(&chainConf, "chain-config", "", "path to the chain configuration for subnet")
cmd.Flags().BoolVar(&useLatestAvalanchegoVersion, "latest-avalanchego", false, "install latest avalanchego version on node/s")
cmd.Flags().StringVar(&useCustomAvalanchegoVersion, "avalanchego-version", "", "install given avalanchego version on node/s")
cmd.Flags().StringSliceVar(&validators, "validators", []string{}, "deploy subnet into given comma separated list of validators. defaults to all cluster nodes")
return cmd
}

func wiz(cmd *cobra.Command, args []string) error {
clusterName := args[0]
subnetName := args[1]
exists, err := clusterExists(clusterName)
subnetName := ""
if len(args) > 1 {
subnetName = args[1]
}
clusterAlreadyExists, err := clusterExists(clusterName)
if err != nil {
return err
}
if exists {
return fmt.Errorf("cluster %s already exists", clusterName)
if clusterAlreadyExists {
if err := checkClusterIsADevnet(clusterName); err != nil {
return err
}
}
if !app.SidecarExists(subnetName) || forceSubnetCreate {
if clusterAlreadyExists && subnetName == "" {
return fmt.Errorf("expecting to add subnet to existing cluster but no subnet-name was provided")
}
if subnetName != "" && (!app.SidecarExists(subnetName) || forceSubnetCreate) {
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Creating the subnet"))
ux.Logger.PrintToUser("")
Expand Down Expand Up @@ -126,18 +140,43 @@ func wiz(cmd *cobra.Command, args []string) error {
}
}
}
createDevnet = true
useAvalanchegoVersionFromSubnet = subnetName
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Creating the devnet"))
ux.Logger.PrintToUser("")
err = createNodes(cmd, []string{clusterName})
if err != nil {
return err

if !clusterAlreadyExists {
createDevnet = true
useAvalanchegoVersionFromSubnet = subnetName
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Creating the devnet..."))
ux.Logger.PrintToUser("")
if err := createNodes(cmd, []string{clusterName}); err != nil {
return err
}
} else {
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Adding subnet into existing devnet %s..."), clusterName)
ux.Logger.PrintToUser("")
}

// check all validators are found
if len(validators) != 0 {
hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName))
if err != nil {
return err
}
_, err = filterHosts(hosts, validators)
if err != nil {
return err
}
}

if err := waitForHealthyCluster(clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil {
return err
}
if subnetName == "" {
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Devnet %s has been created!"), clusterName)
return nil
}

ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Deploying the subnet"))
ux.Logger.PrintToUser("")
Expand Down Expand Up @@ -175,7 +214,11 @@ func wiz(cmd *cobra.Command, args []string) error {
return err
}
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(logging.Green.Wrap("Devnet %s has been created and is validating subnet %s!"), clusterName, subnetName)
if clusterAlreadyExists {
ux.Logger.PrintToUser(logging.Green.Wrap("Devnet %s is now validating subnet %s"), clusterName, subnetName)
} else {
ux.Logger.PrintToUser(logging.Green.Wrap("Devnet %s is successfully created and is now validating subnet %s!"), clusterName, subnetName)
}
return nil
}

Expand Down Expand Up @@ -228,6 +271,12 @@ func waitForClusterSubnetStatus(
if err != nil {
return err
}
if len(validators) != 0 {
hosts, err = filterHosts(hosts, validators)
if err != nil {
return err
}
}
defer disconnectHosts(hosts)
startTime := time.Now()
for {
Expand Down Expand Up @@ -275,3 +324,50 @@ func waitForClusterSubnetStatus(
time.Sleep(poolTime)
}
}

func checkClusterIsADevnet(clusterName string) error {
exists, err := clusterExists(clusterName)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("cluster %q does not exists", clusterName)
}
clustersConfig, err := app.LoadClustersConfig()
if err != nil {
return err
}
if clustersConfig.Clusters[clusterName].Network.Kind != models.Devnet {
return fmt.Errorf("cluster %q is not a Devnet", clusterName)
}
return nil
}

func filterHosts(hosts []*models.Host, nodes []string) ([]*models.Host, error) {
indices := set.Set[int]{}
for _, node := range nodes {
added := false
for i, host := range hosts {
cloudID := host.GetCloudID()
ip := host.IP
nodeID, err := getNodeID(app.GetNodeInstanceDirPath(cloudID))
if err != nil {
return nil, err
}
if slices.Contains([]string{cloudID, ip, nodeID.String()}, node) {
added = true
indices.Add(i)
}
}
if !added {
return nil, fmt.Errorf("node %q not found", node)
}
}
filteredHosts := []*models.Host{}
for i, host := range hosts {
if indices.Contains(i) {
filteredHosts = append(filteredHosts, host)
}
}
return filteredHosts, nil
}
2 changes: 1 addition & 1 deletion pkg/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func RunSSHCheckBootstrapped(host *models.Host) ([]byte, error) {
// RunSSHCheckHealthy checks if node is healthy
func RunSSHCheckHealthy(host *models.Host) ([]byte, error) {
// Craft and send the HTTP POST request
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\":\"health.health\"}"
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\":\"health.health\",\"params\": {\"tags\": [\"P\"]}}"
return PostOverSSH(host, "/ext/health", requestBody)
}

Expand Down

0 comments on commit cfd20b4

Please sign in to comment.