Skip to content

Commit

Permalink
add subnetID tracking to configuration (#113)
Browse files Browse the repository at this point in the history
* wip

* add subnetid tracking

* lint

* composefileexist is back

* rm replaced shell script

* if nodeConfigFileExists

* lint

* add ValidateSubnet func

* added avagoversion detection and network name

* detect network and avalanchego version

* fix

* fix example

* fix example

* fix host header

* added waitForAvago Health

* rm debug println

* rewrite get avalanchego image version

* fix command

* lint

* lint

* rename to SyncSubnets
  • Loading branch information
arturrez authored Aug 2, 2024
1 parent bc4e432 commit d7c448e
Show file tree
Hide file tree
Showing 22 changed files with 417 additions and 146 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ func CreateNodes() {
// of Avalanche Tooling SDK
const (
avalancheGoVersion = "v1.11.8"
avalancheCliVersion = "v1.6.2"
)

// Create two new Avalanche Validator nodes on Fuji Network on AWS without Elastic IPs
Expand All @@ -220,7 +219,6 @@ func CreateNodes() {
Roles: []node.SupportedRole{node.Validator},
Network: avalanche.FujiNetwork(),
AvalancheGoVersion: avalancheGoVersion,
AvalancheCliVersion: avalancheCliVersion,
UseStaticIP: false,
SSHPrivateKeyPath: sshPrivateKeyPath,
})
Expand Down Expand Up @@ -280,4 +278,4 @@ func CreateNodes() {
panic(err)
}
}
```
```
14 changes: 7 additions & 7 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ const (
AnsibleSSHUser = "ubuntu"

// node
CloudNodeCLIConfigBasePath = "/home/ubuntu/.avalanche-cli/"
CloudNodeConfigBasePath = "/home/ubuntu/.avalanchego/"
CloudNodeSubnetEvmBinaryPath = "/home/ubuntu/.avalanchego/plugins/%s"
CloudNodeStakingPath = "/home/ubuntu/.avalanchego/staking/"
CloudNodeConfigPath = "/home/ubuntu/.avalanchego/configs/"
ServicesDir = "services"
DashboardsDir = "dashboards"
CloudNodeCLIConfigBasePath = "/home/ubuntu/.avalanche-cli/"
CloudNodeConfigBasePath = "/home/ubuntu/.avalanchego/"
CloudNodeSubnetVMBinaryPath = "/home/ubuntu/.avalanchego/plugins/%s"
CloudNodeStakingPath = "/home/ubuntu/.avalanchego/staking/"
CloudNodeConfigPath = "/home/ubuntu/.avalanchego/configs/"
ServicesDir = "services"
DashboardsDir = "dashboards"

// services
ServiceAvalanchego = "avalanchego"
Expand Down
27 changes: 17 additions & 10 deletions examples/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ func CreateNodes() {
// Avalanche-CLI dependency by Avalanche nodes will be deprecated in the next release
// of Avalanche Tooling SDK
const (
avalancheGoVersion = "v1.11.8"
avalancheCliVersion = "v1.6.2"
avalancheGoVersion = "v1.11.8"
)

// Create two new Avalanche Validator nodes on Fuji Network on AWS without Elastic IPs
Expand All @@ -62,14 +61,13 @@ func CreateNodes() {
// in the next Avalanche Tooling SDK release.
hosts, err := node.CreateNodes(ctx,
&node.NodeParams{
CloudParams: cp,
Count: 2,
Roles: []node.SupportedRole{node.Validator},
Network: avalanche.FujiNetwork(),
AvalancheGoVersion: avalancheGoVersion,
AvalancheCliVersion: avalancheCliVersion,
UseStaticIP: false,
SSHPrivateKeyPath: sshPrivateKeyPath,
CloudParams: cp,
Count: 2,
Roles: []node.SupportedRole{node.Validator},
Network: avalanche.FujiNetwork(),
AvalancheGoVersion: avalancheGoVersion,
UseStaticIP: false,
SSHPrivateKeyPath: sshPrivateKeyPath,
})
if err != nil {
panic(err)
Expand Down Expand Up @@ -104,6 +102,15 @@ func CreateNodes() {
}
}

// examle of how to reconfigure the created nodes to track a subnet
subnetIDsToValidate := []string{"xxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyzzzzzzzzzzzzzzz"}
for _, h := range hosts {
fmt.Println("Reconfiguring node %s to track subnet %s", h.NodeID, subnetIDsToValidate)
if err := h.SyncSubnets(subnetIDsToValidate); err != nil {
panic(err)
}
}

// Create a monitoring node.
// Monitoring node enables you to have a centralized Grafana Dashboard where you can view
// metrics relevant to any Validator & API nodes that the monitoring node is linked to as well
Expand Down
3 changes: 2 additions & 1 deletion examples/subnet_ multisig.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package examples
import (
"context"
"fmt"
"time"

"github.com/ava-labs/avalanche-tooling-sdk-go/avalanche"
"github.com/ava-labs/avalanche-tooling-sdk-go/keychain"
"github.com/ava-labs/avalanche-tooling-sdk-go/subnet"
Expand All @@ -14,7 +16,6 @@ import (
"github.com/ava-labs/avalanchego/utils/set"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
"github.com/ava-labs/avalanchego/wallet/subnet/primary"
"time"
)

func DeploySubnetMultiSig() {
Expand Down
121 changes: 121 additions & 0 deletions node/avalanchego.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package node

import (
"encoding/json"
"fmt"
"time"

"github.com/ava-labs/avalanche-tooling-sdk-go/constants"
remoteconfig "github.com/ava-labs/avalanche-tooling-sdk-go/node/config"
"github.com/ava-labs/avalanche-tooling-sdk-go/utils"
"github.com/ava-labs/avalanchego/api/info"
)

func (h *Node) GetAvalancheGoVersion() (string, error) {
// Craft and send the HTTP POST request
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\" :\"info.getNodeVersion\"}"
resp, err := h.Post("", requestBody)
if err != nil {
return "", err
}
if avalancheGoVersion, _, err := parseAvalancheGoOutput(resp); err != nil {
return "", err
} else {
return avalancheGoVersion, nil
}
}

func (h *Node) GetAvalancheGoHealth() (bool, error) {
// Craft and send the HTTP POST request
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\":\"health.health\",\"params\": {\"tags\": [\"P\"]}}"
resp, err := h.Post("/ext/health", requestBody)
if err != nil {
return false, err
}
return parseHealthyOutput(resp)
}

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 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 (h *Node) GetAvalancheGoNetworkName() (string, error) {
if nodeConfigFileExists(*h) {
avagoConfig, err := h.GetAvalancheGoConfigData()
if err != nil {
return "", err
}
return utils.StringValue(avagoConfig, "network-id")
} else {
return "", fmt.Errorf("node config file does not exist")
}
}

func (h *Node) GetAvalancheGoConfigData() (map[string]interface{}, error) {
// get remote node.json file
nodeJSON, err := h.ReadFileBytes(remoteconfig.GetRemoteAvalancheNodeConfig(), constants.SSHFileOpsTimeout)
if err != nil {
return nil, err
}
var avagoConfig map[string]interface{}
if err := json.Unmarshal(nodeJSON, &avagoConfig); err != nil {
return nil, err
}
return avagoConfig, nil
}

// WaitForSSHShell waits for the SSH shell to be available on the node within the specified timeout.
func (h *Node) WaitForAvalancheGoHealth(timeout time.Duration) error {
if h.IP == "" {
return fmt.Errorf("node IP is empty")
}
start := time.Now()
if err := h.WaitForPort(constants.AvalanchegoAPIPort, timeout); err != nil {
return err
}

deadline := start.Add(timeout)
for {
if time.Now().After(deadline) {
return fmt.Errorf("timeout: AvalancheGo health on node %s is not available after %ds", h.IP, int(timeout.Seconds()))
}
if isHealthy, err := h.GetAvalancheGoHealth(); err != nil || !isHealthy {
time.Sleep(constants.SSHSleepBetweenChecks)
continue
} else {
return nil
}
}
}
13 changes: 12 additions & 1 deletion node/config/avalanche.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package services
import (
"bytes"
"path/filepath"
"strings"
"text/template"

"github.com/ava-labs/avalanche-tooling-sdk-go/constants"
Expand All @@ -21,9 +22,13 @@ type AvalancheConfigInputs struct {
PublicIP string
StateSyncEnabled bool
PruningEnabled bool
TrackSubnets string
BootstrapIDs string
BootstrapIPs string
GenesisPath string
}

func DefaultCliAvalancheConfig(publicIP string, networkID string) AvalancheConfigInputs {
func PrepareAvalancheConfig(publicIP string, networkID string, subnetsToTrack []string) AvalancheConfigInputs {
return AvalancheConfigInputs{
HTTPHost: "0.0.0.0",
NetworkID: networkID,
Expand All @@ -32,6 +37,7 @@ func DefaultCliAvalancheConfig(publicIP string, networkID string) AvalancheConfi
PublicIP: publicIP,
StateSyncEnabled: true,
PruningEnabled: false,
TrackSubnets: strings.Join(subnetsToTrack, ","),
}
}

Expand Down Expand Up @@ -77,11 +83,16 @@ func GetRemoteAvalancheCChainConfig() string {
return filepath.Join(constants.CloudNodeConfigPath, "chains", "C", "config.json")
}

func GetRemoteAvalancheGenesis() string {
return filepath.Join(constants.CloudNodeConfigPath, "genesis.json")
}

func AvalancheFolderToCreate() []string {
return []string{
"/home/ubuntu/.avalanchego/db",
"/home/ubuntu/.avalanchego/logs",
"/home/ubuntu/.avalanchego/configs",
"/home/ubuntu/.avalanchego/configs/subnets/",
"/home/ubuntu/.avalanchego/configs/chains/C",
"/home/ubuntu/.avalanchego/staking",
"/home/ubuntu/.avalanchego/plugins",
Expand Down
24 changes: 18 additions & 6 deletions node/config/templates/avalanche-node.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@
"api-admin-enabled": {{.APIAdminEnabled}},
"index-enabled": {{.IndexEnabled}},
"network-id": "{{if .NetworkID}}{{.NetworkID}}{{else}}fuji{{end}}",
{{- if .BootstrapIDs }}
"bootstrap-ids": "{{ .BootstrapIDs }}",
{{- end }}
{{- if .BootstrapIPs }}
"bootstrap-ips": "{{ .BootstrapIPs }}",
{{- end }}
{{- if .GenesisPath }}
"genesis-file": "{{ .GenesisPath }}",
{{- end }}
{{- if .PublicIP }}
"public-ip": "{{.PublicIP}}",
{{- else }}
"public-ip-resolution-service": "opendns",
{{- end }}
{{- if .TrackSubnets }}
"track-subnets": "{{ .TrackSubnets }}",
{{- end }}
"db-dir": "{{.DBDir}}",
"log-dir": "{{.LogDir}}",
{{- if .PublicIP -}}
"public-ip": "{{.PublicIP}}"
{{- else -}}
"public-ip-resolution-service": "opendns"
{{- end -}}
"log-dir": "{{.LogDir}}"
}
11 changes: 6 additions & 5 deletions node/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ type NodeParams struct {
// in Fuji / Mainnet / Devnet
Network avalanche.Network

// SubnetIDs is the list of subnet IDs that the created nodes will be tracking
// For primary network, it should be empty
SubnetIDs []string

// SSHPrivateKeyPath is the file path to the private key of the SSH key pair that is used
// to gain access to the created nodes
SSHPrivateKeyPath string

// AvalancheGoVersion is the version of Avalanche Go to install in the created node
AvalancheGoVersion string

// AvalancheCliVersion is the version of Avalanche CLI to install in the created node
AvalancheCliVersion string

// UseStaticIP is whether the created node should have static IP attached to it. Note that
// assigning Static IP to a node may incur additional charges on AWS / GCP. There could also be
// a limit to how many Static IPs you can have in a region in AWS & GCP.
Expand Down Expand Up @@ -283,7 +284,7 @@ func provisionHost(node Node, nodeParams *NodeParams) error {

func provisionAvagoHost(node Node, nodeParams *NodeParams) error {
const withMonitoring = true
if err := node.RunSSHSetupNode(nodeParams.AvalancheCliVersion); err != nil {
if err := node.RunSSHSetupNode(); err != nil {
return err
}
if err := node.RunSSHSetupDockerService(); err != nil {
Expand All @@ -293,7 +294,7 @@ func provisionAvagoHost(node Node, nodeParams *NodeParams) error {
if err := node.RunSSHSetupPromtailConfig("127.0.0.1", constants.AvalanchegoLokiPort, node.NodeID, "", ""); err != nil {
return err
}
if err := node.ComposeSSHSetupNode(nodeParams.Network.HRP(), nodeParams.AvalancheGoVersion, withMonitoring); err != nil {
if err := node.ComposeSSHSetupNode(nodeParams.Network.HRP(), nodeParams.SubnetIDs, nodeParams.AvalancheGoVersion, withMonitoring); err != nil {
return err
}
if err := node.StartDockerCompose(constants.SSHScriptTimeout); err != nil {
Expand Down
18 changes: 8 additions & 10 deletions node/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ func TestCreateNodes(_ *testing.T) {
// Avalanche-CLI dependency by Avalanche nodes will be deprecated in the next release
// of Avalanche Tooling SDK
const (
avalancheGoVersion = "v1.11.8"
avalancheCliVersion = "v1.6.2"
avalancheGoVersion = "v1.11.8"
)

// Create two new Avalanche Validator nodes on Fuji Network on AWS without Elastic IPs
Expand All @@ -57,14 +56,13 @@ func TestCreateNodes(_ *testing.T) {
// in the next Avalanche Tooling SDK release.
hosts, err := CreateNodes(ctx,
&NodeParams{
CloudParams: cp,
Count: 2,
Roles: []SupportedRole{Validator},
Network: avalanche.FujiNetwork(),
AvalancheGoVersion: avalancheGoVersion,
AvalancheCliVersion: avalancheCliVersion,
UseStaticIP: false,
SSHPrivateKeyPath: sshPrivateKeyPath,
CloudParams: cp,
Count: 2,
Roles: []SupportedRole{Validator},
Network: avalanche.FujiNetwork(),
AvalancheGoVersion: avalancheGoVersion,
UseStaticIP: false,
SSHPrivateKeyPath: sshPrivateKeyPath,
})
if err != nil {
panic(err)
Expand Down
Loading

0 comments on commit d7c448e

Please sign in to comment.