diff --git a/README.md b/README.md index d9b6f9c..19d9422 100644 --- a/README.md +++ b/README.md @@ -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 @@ -220,7 +219,6 @@ func CreateNodes() { Roles: []node.SupportedRole{node.Validator}, Network: avalanche.FujiNetwork(), AvalancheGoVersion: avalancheGoVersion, - AvalancheCliVersion: avalancheCliVersion, UseStaticIP: false, SSHPrivateKeyPath: sshPrivateKeyPath, }) @@ -280,4 +278,4 @@ func CreateNodes() { panic(err) } } -``` \ No newline at end of file +``` diff --git a/constants/constants.go b/constants/constants.go index 62cbf88..9b7f88d 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -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" diff --git a/examples/node.go b/examples/node.go index 2826091..c500619 100644 --- a/examples/node.go +++ b/examples/node.go @@ -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 @@ -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) @@ -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 diff --git a/examples/subnet_ multisig.go b/examples/subnet_ multisig.go index 2003e77..21151b4 100644 --- a/examples/subnet_ multisig.go +++ b/examples/subnet_ multisig.go @@ -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" @@ -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() { diff --git a/node/avalanchego.go b/node/avalanchego.go new file mode 100644 index 0000000..17b26f8 --- /dev/null +++ b/node/avalanchego.go @@ -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 + } + } +} diff --git a/node/config/avalanche.go b/node/config/avalanche.go index 8c4bf3a..d3a6482 100644 --- a/node/config/avalanche.go +++ b/node/config/avalanche.go @@ -6,6 +6,7 @@ package services import ( "bytes" "path/filepath" + "strings" "text/template" "github.com/ava-labs/avalanche-tooling-sdk-go/constants" @@ -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, @@ -32,6 +37,7 @@ func DefaultCliAvalancheConfig(publicIP string, networkID string) AvalancheConfi PublicIP: publicIP, StateSyncEnabled: true, PruningEnabled: false, + TrackSubnets: strings.Join(subnetsToTrack, ","), } } @@ -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", diff --git a/node/config/templates/avalanche-node.tmpl b/node/config/templates/avalanche-node.tmpl index ef0ffbd..c5704fd 100644 --- a/node/config/templates/avalanche-node.tmpl +++ b/node/config/templates/avalanche-node.tmpl @@ -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}}" } diff --git a/node/create.go b/node/create.go index d209da0..e02b13c 100644 --- a/node/create.go +++ b/node/create.go @@ -32,6 +32,10 @@ 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 @@ -39,9 +43,6 @@ type NodeParams struct { // 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. @@ -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 { @@ -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 { diff --git a/node/create_test.go b/node/create_test.go index 8e9e6d4..7fc9eaf 100644 --- a/node/create_test.go +++ b/node/create_test.go @@ -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 @@ -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) diff --git a/node/dockerCompose.go b/node/dockerCompose.go index 1c6f4c0..7a2ad34 100644 --- a/node/dockerCompose.go +++ b/node/dockerCompose.go @@ -6,6 +6,7 @@ package node import ( "bytes" "embed" + "encoding/json" "fmt" "os" "path/filepath" @@ -288,3 +289,34 @@ func (h *Node) HasRemoteComposeService(composeFile string, service string, timeo } return found, nil } + +func (h *Node) ListDockerComposeImages(composeFile string, timeout time.Duration) (map[string]string, error) { + output, err := h.Commandf(nil, timeout, "docker compose -f %s images --format json", composeFile) + if err != nil { + return nil, err + } + type dockerImages struct { + ID string `json:"ID"` + Name string `json:"ContainerName"` + Repository string `json:"Repository"` + Tag string `json:"Tag"` + Size uint `json:"Size"` + } + var images []dockerImages + if err := json.Unmarshal(output, &images); err != nil { + return nil, err + } + imageMap := make(map[string]string) + for _, image := range images { + imageMap[image.Repository] = image.Tag + } + return imageMap, nil +} + +func (h *Node) GetDockerImageVersion(image string, timeout time.Duration) (string, error) { + imageMap, err := h.ListDockerComposeImages(utils.GetRemoteComposeFile(), timeout) + if err != nil { + return "", err + } + return imageMap[image], nil +} diff --git a/node/dockerConfig.go b/node/dockerConfig.go index b565042..d327820 100644 --- a/node/dockerConfig.go +++ b/node/dockerConfig.go @@ -7,38 +7,53 @@ import ( "os" "github.com/ava-labs/avalanche-tooling-sdk-go/constants" - config "github.com/ava-labs/avalanche-tooling-sdk-go/node/config" + remoteconfig "github.com/ava-labs/avalanche-tooling-sdk-go/node/config" + "github.com/ava-labs/avalanche-tooling-sdk-go/utils" ) -func (h *Node) prepareAvalanchegoConfig(networkID string) (string, string, error) { - avagoConf := config.DefaultCliAvalancheConfig(h.IP, networkID) - nodeConf, err := config.RenderAvalancheNodeConfig(avagoConf) - if err != nil { - return "", "", err - } - nodeConfFile, err := os.CreateTemp("", "avalanchecli-node-*.yml") +// PrepareAvalanchegoConfig creates the config files for the AvalancheGo +// networkID is the ID of the network to be used +// trackSubnets is the list of subnets to track +func (h *Node) RunSSHRenderAvalancheNodeConfig(networkID string, trackSubnets []string) error { + avagoConf := remoteconfig.PrepareAvalancheConfig(h.IP, networkID, trackSubnets) + + nodeConf, err := remoteconfig.RenderAvalancheNodeConfig(avagoConf) if err != nil { - return "", "", err - } - if err := os.WriteFile(nodeConfFile.Name(), nodeConf, constants.WriteReadUserOnlyPerms); err != nil { - return "", "", err + return err + } + // preserve remote configuration if it exists + if nodeConfigFileExists(*h) { + // make sure that bootsrap configuration is preserved + if genesisFileExists(*h) { + avagoConf.GenesisPath = remoteconfig.GetRemoteAvalancheGenesis() + } + remoteAvagoConf, err := h.GetAvalancheGoConfigData() + if err != nil { + return err + } + // ignore errors if bootstrap configuration is not present - it's fine + bootstrapIDs, _ := utils.StringValue(remoteAvagoConf, "bootstrap-ids") + bootstrapIPs, _ := utils.StringValue(remoteAvagoConf, "bootstrap-ips") + + avagoConf.BootstrapIDs = bootstrapIDs + avagoConf.BootstrapIPs = bootstrapIPs } - cChainConf, err := config.RenderAvalancheCChainConfig(avagoConf) - if err != nil { - return "", "", err + // configuration is ready to be uploaded + if err := h.UploadBytes(nodeConf, remoteconfig.GetRemoteAvalancheNodeConfig(), constants.SSHFileOpsTimeout); err != nil { + return err } - cChainConfFile, err := os.CreateTemp("", "avalanchecli-cchain-*.yml") + cChainConf, err := remoteconfig.RenderAvalancheCChainConfig(avagoConf) if err != nil { - return "", "", err + return err } - if err := os.WriteFile(cChainConfFile.Name(), cChainConf, constants.WriteReadUserOnlyPerms); err != nil { - return "", "", err + if err := h.UploadBytes(cChainConf, remoteconfig.GetRemoteAvalancheCChainConfig(), constants.SSHFileOpsTimeout); err != nil { + return err } - return nodeConfFile.Name(), cChainConfFile.Name(), nil + return nil } func prepareGrafanaConfig() (string, string, string, string, error) { - grafanaDataSource, err := config.RenderGrafanaLokiDataSourceConfig() + grafanaDataSource, err := remoteconfig.RenderGrafanaLokiDataSourceConfig() if err != nil { return "", "", "", "", err } @@ -50,7 +65,7 @@ func prepareGrafanaConfig() (string, string, string, string, error) { return "", "", "", "", err } - grafanaPromDataSource, err := config.RenderGrafanaPrometheusDataSourceConfigg() + grafanaPromDataSource, err := remoteconfig.RenderGrafanaPrometheusDataSourceConfigg() if err != nil { return "", "", "", "", err } @@ -62,7 +77,7 @@ func prepareGrafanaConfig() (string, string, string, string, error) { return "", "", "", "", err } - grafanaDashboards, err := config.RenderGrafanaDashboardConfig() + grafanaDashboards, err := remoteconfig.RenderGrafanaDashboardConfig() if err != nil { return "", "", "", "", err } @@ -74,7 +89,7 @@ func prepareGrafanaConfig() (string, string, string, string, error) { return "", "", "", "", err } - grafanaConfig, err := config.RenderGrafanaConfig() + grafanaConfig, err := remoteconfig.RenderGrafanaConfig() if err != nil { return "", "", "", "", err } diff --git a/node/dockerSsh.go b/node/dockerSsh.go index bea6f10..f219657 100644 --- a/node/dockerSsh.go +++ b/node/dockerSsh.go @@ -10,7 +10,7 @@ import ( "time" "github.com/ava-labs/avalanche-tooling-sdk-go/constants" - config "github.com/ava-labs/avalanche-tooling-sdk-go/node/config" + remoteconfig "github.com/ava-labs/avalanche-tooling-sdk-go/node/config" "github.com/ava-labs/avalanche-tooling-sdk-go/utils" ) @@ -23,9 +23,9 @@ func (h *Node) ValidateComposeFile(composeFile string, timeout time.Duration) er } // ComposeSSHSetupNode sets up an AvalancheGo node and dependencies on a remote node over SSH. -func (h *Node) ComposeSSHSetupNode(networkID string, avalancheGoVersion string, withMonitoring bool) error { +func (h *Node) ComposeSSHSetupNode(networkID string, subnetsToTrack []string, avalancheGoVersion string, withMonitoring bool) error { startTime := time.Now() - folderStructure := config.RemoteFoldersToCreateAvalanchego() + folderStructure := remoteconfig.RemoteFoldersToCreateAvalanchego() for _, dir := range folderStructure { if err := h.MkdirAll(dir, constants.SSHFileOpsTimeout); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) @@ -38,23 +38,7 @@ func (h *Node) ComposeSSHSetupNode(networkID string, avalancheGoVersion string, return err } h.Logger.Infof("AvalancheGo Docker image %s ready on %s[%s] after %s", avagoDockerImage, h.NodeID, h.IP, time.Since(startTime)) - nodeConfFile, cChainConfFile, err := h.prepareAvalanchegoConfig(networkID) - if err != nil { - return err - } - defer func() { - if err := os.Remove(nodeConfFile); err != nil { - h.Logger.Errorf("Error removing temporary file %s: %s", nodeConfFile, err) - } - if err := os.Remove(cChainConfFile); err != nil { - h.Logger.Errorf("Error removing temporary file %s: %s", cChainConfFile, err) - } - }() - - if err := h.Upload(nodeConfFile, config.GetRemoteAvalancheNodeConfig(), constants.SSHFileOpsTimeout); err != nil { - return err - } - if err := h.Upload(cChainConfFile, config.GetRemoteAvalancheCChainConfig(), constants.SSHFileOpsTimeout); err != nil { + if err := h.RunSSHRenderAvalancheNodeConfig(networkID, subnetsToTrack); err != nil { return err } h.Logger.Infof("AvalancheGo configs uploaded to %s[%s] after %s", h.NodeID, h.IP, time.Since(startTime)) diff --git a/node/installer/installer.go b/node/installer/installer.go new file mode 100644 index 0000000..eb86a4d --- /dev/null +++ b/node/installer/installer.go @@ -0,0 +1,30 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package installer + +import ( + "strings" + + "github.com/ava-labs/avalanche-tooling-sdk-go/constants" + "github.com/ava-labs/avalanche-tooling-sdk-go/node" +) + +type NodeInstaller struct { + Node *node.Node +} + +func NewHostInstaller(node *node.Node) *NodeInstaller { + return &NodeInstaller{Node: node} +} + +func (i *NodeInstaller) GetArch() (string, string) { + goArhBytes, err := i.Node.Command(nil, constants.SSHScriptTimeout, "dpkg --print-architecture") + if err != nil { + return "", "" + } + goOSBytes, err := i.Node.Command(nil, constants.SSHScriptTimeout, "uname -s") + if err != nil { + return "", "" + } + return strings.TrimSpace(string(goArhBytes)), strings.TrimSpace(strings.ToLower(string(goOSBytes))) +} diff --git a/node/net.go b/node/net.go index ce728d7..369202c 100644 --- a/node/net.go +++ b/node/net.go @@ -53,7 +53,7 @@ func (h *Node) Post(path string, requestBody string) ([]byte, error) { return nil, err } requestHeaders := fmt.Sprintf("POST %s HTTP/1.1\r\n"+ - "Node: %s\r\n"+ + "Host: %s\r\n"+ "Content-Length: %d\r\n"+ "Content-Type: application/json\r\n\r\n", path, localhost.Host, len(requestBody)) httpRequest := requestHeaders + requestBody diff --git a/node/node.go b/node/node.go index 939cd9d..ab67085 100644 --- a/node/node.go +++ b/node/node.go @@ -184,6 +184,22 @@ func (h *Node) Upload(localFile string, remoteFile string, timeout time.Duration return err } +// UploadBytes uploads a byte array to a remote file on the host. +func (h *Node) UploadBytes(data []byte, remoteFile string, timeout time.Duration) error { + tmpFile, err := os.CreateTemp("", "NodeUploadBytes-*.tmp") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + if _, err := tmpFile.Write(data); err != nil { + return err + } + if err := tmpFile.Close(); err != nil { + return err + } + return h.Upload(tmpFile.Name(), remoteFile, timeout) +} + // Download downloads a file from the remote server to the local machine. func (h *Node) Download(remoteFile string, localFile string, timeout time.Duration) error { if !h.Connected() { @@ -207,6 +223,19 @@ func (h *Node) Download(remoteFile string, localFile string, timeout time.Durati return err } +// ReadFileBytes downloads a file from the remote server to a byte array +func (h *Node) ReadFileBytes(remoteFile string, timeout time.Duration) ([]byte, error) { + tmpFile, err := os.CreateTemp("", "NodeDownloadBytes-*.tmp") + if err != nil { + return nil, err + } + defer os.Remove(tmpFile.Name()) + if err := h.Download(remoteFile, tmpFile.Name(), timeout); err != nil { + return nil, err + } + return os.ReadFile(tmpFile.Name()) +} + // ExpandHome expands the ~ symbol to the home directory. func (h *Node) ExpandHome(path string) string { userHome := filepath.Join("/home", h.SSHConfig.User) diff --git a/node/shell/buildCustomVM.sh b/node/shell/buildCustomVM.sh new file mode 100644 index 0000000..eb03e9c --- /dev/null +++ b/node/shell/buildCustomVM.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +if [ -d {{ .CustomVMRepoDir }} ]; then + rm -rf {{ .CustomVMRepoDir }} +fi + +cd {{ .CustomVMRepoDir }} +git init -q +git remote add origin {{ .CustomVMRepoURL }} +git fetch --depth 1 origin {{ .CustomVMBranch }} -q +git checkout {{ .CustomVMBranch }} +chmod +x {{ .CustomVMBuildScript }} +./{{ .CustomVMBuildScript }} {{ .SubnetVMBinaryPath }} +echo {{ .SubnetVMBinaryPath }} [ok] diff --git a/node/shell/getNewSubnetEVMRelease.sh b/node/shell/getNewSubnetEVMRelease.sh index 76072e3..d67251e 100644 --- a/node/shell/getNewSubnetEVMRelease.sh +++ b/node/shell/getNewSubnetEVMRelease.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -e #name:TASK [download new subnet EVM release] -wget -N "{{ .SubnetEVMReleaseURL }}" +busybox wget "{{ .SubnetEVMReleaseURL }}" #name:TASK [unpack new subnet EVM release] tar xvf "{{ .SubnetEVMArchive}}" diff --git a/node/shell/setupCLIFromSource.sh b/node/shell/setupCLIFromSource.sh deleted file mode 100644 index c6055f5..0000000 --- a/node/shell/setupCLIFromSource.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -e - -#name:TASK [install gcc] -export DEBIAN_FRONTEND=noninteractive -while ! gcc --version >/dev/null 2>&1; do - echo "GCC is not installed. Trying to install..." - sudo apt-get -y -o DPkg::Lock::Timeout=120 update - sudo apt-get -y -o DPkg::Lock::Timeout=120 install gcc - if [ $? -ne 0 ]; then - echo "Failed to install GCC. Retrying in 10 seconds..." - sleep 10 - fi -done -#name:TASK [install go] -install_go() { - ARCH=amd64 - [[ "$(uname -m)" == "aarch64" ]] && ARCH=arm64 - GOFILE="go{{ .GoVersion }}.linux-$ARCH.tar.gz" - cd - sudo rm -rf $GOFILE go - wget -q -nv https://go.dev/dl/$GOFILE - tar xfz $GOFILE - echo >>~/.bashrc - echo export PATH=\$PATH:~/go/bin:~/bin >>~/.bashrc - echo export CGO_ENABLED=1 >>~/.bashrc -} -go version || install_go -export PATH=$PATH:~/go/bin -#name:TASK [build avalanche-cli] -cd ~ -rm -rf avalanche-cli -git clone --single-branch -b {{ .CliBranch }} https://github.com/ava-labs/avalanche-cli -cd avalanche-cli -./scripts/build.sh -cp bin/avalanche ~/bin/avalanche diff --git a/node/shell/setupNode.sh b/node/shell/setupNode.sh index a4a177c..abd02e0 100644 --- a/node/shell/setupNode.sh +++ b/node/shell/setupNode.sh @@ -19,10 +19,3 @@ fi sudo usermod -aG docker ubuntu sudo chgrp ubuntu /var/run/docker.sock sudo chmod +rw /var/run/docker.sock -export PATH=$PATH:~/go/bin -mkdir -p ~/.avalanche-cli -rm -vf install.sh && busybox wget -q -nd https://raw.githubusercontent.com/ava-labs/avalanche-cli/main/scripts/install.sh -#name:TASK [modify permissions] -chmod 755 install.sh -#name:TASK [run install script] -./install.sh {{ .CLIVersion }} diff --git a/node/ssh.go b/node/ssh.go index 2b3952b..fca99ea 100644 --- a/node/ssh.go +++ b/node/ssh.go @@ -26,15 +26,13 @@ import ( type scriptInputs struct { AvalancheGoVersion string - CLIVersion string SubnetExportFileName string SubnetName string ClusterName string GoVersion string - CliBranch string IsDevNet bool NetworkFlag string - SubnetEVMBinaryPath string + SubnetVMBinaryPath string SubnetEVMReleaseURL string SubnetEVMArchive string LoadTestRepoDir string @@ -83,12 +81,12 @@ func (h *Node) RunOverSSH( } // RunSSHSetupNode runs script to setup sdk dependencies on a remote host over SSH. -func (h *Node) RunSSHSetupNode(cliVersion string) error { +func (h *Node) RunSSHSetupNode() error { if err := h.RunOverSSH( "Setup Node", constants.SSHLongRunningScriptTimeout, "shell/setupNode.sh", - scriptInputs{CLIVersion: cliVersion}, + scriptInputs{}, ); err != nil { return err } @@ -127,13 +125,23 @@ func (h *Node) RunSSHStopAWMRelayerService() error { } // RunSSHUpgradeAvalanchego runs script to upgrade avalanchego -func (h *Node) RunSSHUpgradeAvalanchego(networkID string, avalancheGoVersion string) error { +func (h *Node) RunSSHUpgradeAvalanchego(avalancheGoVersion string) error { withMonitoring, err := h.WasNodeSetupWithMonitoring() if err != nil { return err } - if err := h.ComposeSSHSetupNode(networkID, avalancheGoVersion, withMonitoring); err != nil { + if err := h.ComposeOverSSH("Compose Node", + constants.SSHScriptTimeout, + "templates/avalanchego.docker-compose.yml", + dockerComposeInputs{ + AvalanchegoVersion: avalancheGoVersion, + WithMonitoring: withMonitoring, + WithAvalanchego: true, + E2E: utils.IsE2E(), + E2EIP: utils.E2EConvertIP(h.IP), + E2ESuffix: utils.E2ESuffix(h.IP), + }); err != nil { return err } return h.RestartDockerCompose(constants.SSHLongRunningScriptTimeout) @@ -151,12 +159,10 @@ func (h *Node) RunSSHStopAvalanchego() error { // RunSSHUpgradeSubnetEVM runs script to upgrade subnet evm func (h *Node) RunSSHUpgradeSubnetEVM(subnetEVMBinaryPath string) error { - return h.RunOverSSH( - "Upgrade Subnet EVM", - constants.SSHScriptTimeout, - "shell/upgradeSubnetEVM.sh", - scriptInputs{SubnetEVMBinaryPath: subnetEVMBinaryPath}, - ) + if _, err := h.Commandf(nil, constants.SSHScriptTimeout, "cp -f subnet-evm %s", subnetEVMBinaryPath); err != nil { + return err + } + return nil } func (h *Node) RunSSHSetupPrometheusConfig(avalancheGoPorts, machinePorts, loadTestPorts []string) error { @@ -373,6 +379,37 @@ func (h *Node) MonitorNodes(ctx context.Context, targets []Node, chainID string) return nil } +// SyncSubnets reconfigures avalanchego to sync subnets +func (h *Node) SyncSubnets(subnetsToTrack []string) error { + // necessary checks + if !isAvalancheGoNode(*h) { + return fmt.Errorf("%s is not a avalanchego node", h.NodeID) + } + withMonitoring, err := h.WasNodeSetupWithMonitoring() + if err != nil { + return err + } + if err := h.WaitForSSHShell(constants.SSHScriptTimeout); err != nil { + return err + } + avagoVersion, err := h.GetDockerImageVersion(constants.AvalancheGoDockerImage, constants.SSHScriptTimeout) + if err != nil { + return err + } + networkName, err := h.GetAvalancheGoNetworkName() + if err != nil { + return err + } + if err := h.ComposeSSHSetupNode(networkName, subnetsToTrack, avagoVersion, withMonitoring); err != nil { + return err + } + if err := h.RestartDockerCompose(constants.SSHScriptTimeout); err != nil { + return err + } + + return nil +} + func (h *Node) RunSSHCopyMonitoringDashboards(monitoringDashboardPath string) error { // TODO: download dashboards from github instead remoteDashboardsPath := utils.GetRemoteComposeServicePath("grafana", "dashboards") @@ -396,7 +433,7 @@ func (h *Node) RunSSHCopyMonitoringDashboards(monitoringDashboardPath string) er return err } } - if composeFileExists, err := h.FileExists(utils.GetRemoteComposeFile()); err == nil && composeFileExists { + if composeFileExists(*h) { return h.RestartDockerComposeService(utils.GetRemoteComposeFile(), constants.ServiceGrafana, constants.SSHScriptTimeout) } return nil diff --git a/node/utils.go b/node/utils.go index 43a7d11..3c85f04 100644 --- a/node/utils.go +++ b/node/utils.go @@ -14,6 +14,7 @@ import ( "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" ) @@ -109,3 +110,18 @@ func getPrometheusTargets(nodes []Node) ([]string, []string, []string) { } return avalancheGoPorts, machinePorts, ltPorts } + +func composeFileExists(node Node) bool { + composeFileExists, _ := node.FileExists(utils.GetRemoteComposeFile()) + return composeFileExists +} + +func genesisFileExists(node Node) bool { + genesisFileExists, _ := node.FileExists(remoteconfig.GetRemoteAvalancheGenesis()) + return genesisFileExists +} + +func nodeConfigFileExists(node Node) bool { + nodeConfigFileExists, _ := node.FileExists(remoteconfig.GetRemoteAvalancheNodeConfig()) + return nodeConfigFileExists +} diff --git a/utils/strings.go b/utils/strings.go index c58d0ce..2a12cf8 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -52,3 +52,11 @@ func AddSingleQuotes(s []string) []string { return item }) } + +// StringValue returns the value of a key in a map as a string. +func StringValue(data map[string]interface{}, key string) (string, error) { + if value, ok := data[key]; ok { + return fmt.Sprintf("%v", value), nil + } + return "", fmt.Errorf("key %s not found", key) +}