diff --git a/cmd/nodecmd/create.go b/cmd/nodecmd/create.go index 83183eb7b..5e46a5dd0 100644 --- a/cmd/nodecmd/create.go +++ b/cmd/nodecmd/create.go @@ -38,7 +38,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "golang.org/x/mod/semver" ) const ( @@ -60,7 +59,6 @@ var ( useLatestAvalanchegoPreReleaseVersion bool useCustomAvalanchegoVersion string useAvalanchegoVersionFromSubnet string - remoteCLIVersion string cmdLineGCPCredentialsPath string cmdLineGCPProjectName string cmdLineAlternativeKeyPairName string @@ -113,7 +111,6 @@ will apply to all nodes in the cluster`, cmd.Flags().BoolVar(&useLatestAvalanchegoPreReleaseVersion, "latest-avalanchego-pre-release-version", false, "install latest avalanchego pre-release 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(&remoteCLIVersion, "remote-cli-version", "", "install given CLI version on remote nodes. defaults to latest CLI release") cmd.Flags().StringVar(&cmdLineGCPCredentialsPath, "gcp-credentials", "", "use given GCP credentials") cmd.Flags().StringVar(&cmdLineGCPProjectName, "gcp-project", "", "use given GCP project") cmd.Flags().StringVar(&cmdLineAlternativeKeyPairName, "alternative-key-pair-name", "", "key pair name to use if default one generates conflicts") @@ -175,11 +172,6 @@ func preCreateChecks(clusterName string) error { } } } - if remoteCLIVersion != "" { - if !semver.IsValid(remoteCLIVersion) { - return fmt.Errorf("invalid semantic version for CLI on hosts") - } - } if customGrafanaDashboardPath != "" && !utils.FileExists(utils.ExpandHome(customGrafanaDashboardPath)) { return fmt.Errorf("custom grafana dashboard file does not exist") } @@ -693,7 +685,7 @@ func createNodes(cmd *cobra.Command, args []string) error { return } spinner := spinSession.SpinToUser(utils.ScriptLog(host.NodeID, "Setup Node")) - if err := ssh.RunSSHSetupNode(host, app.Conf.GetConfigPath(), remoteCLIVersion); err != nil { + if err := ssh.RunSSHSetupNode(host, app.Conf.GetConfigPath()); err != nil { nodeResults.AddResult(host.NodeID, nil, err) ux.SpinFailWithError(spinner, "", err) return diff --git a/cmd/nodecmd/create_devnet.go b/cmd/nodecmd/create_devnet.go index fb7360dc6..1582867a9 100644 --- a/cmd/nodecmd/create_devnet.go +++ b/cmd/nodecmd/create_devnet.go @@ -204,6 +204,10 @@ func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[stri if err != nil { return err } + // make sure that custom genesis is saved to the subnet dir + if err := os.WriteFile(app.GetGenesisPath(subnetName), genesisBytes, constants.WriteReadReadPerms); err != nil { + return err + } // create avalanchego conf node.json at each node dir bootstrapIPs := []string{} diff --git a/cmd/nodecmd/sync.go b/cmd/nodecmd/sync.go index 84af07e2b..a5928ae60 100644 --- a/cmd/nodecmd/sync.go +++ b/cmd/nodecmd/sync.go @@ -4,15 +4,14 @@ package nodecmd import ( "fmt" - "path/filepath" "sync" "github.com/ava-labs/avalanche-cli/cmd/subnetcmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" - "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/spf13/cobra" ) @@ -41,6 +40,10 @@ func syncSubnet(_ *cobra.Command, args []string) error { if err := checkCluster(clusterName); err != nil { return err } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } if _, err := subnetcmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { return err } @@ -66,7 +69,10 @@ func syncSubnet(_ *cobra.Command, args []string) error { return err } } - untrackedNodes, err := trackSubnet(hosts, clusterName, subnetName) + if err := prepareSubnetPlugin(hosts, subnetName); err != nil { + return err + } + untrackedNodes, err := trackSubnet(hosts, clusterName, clusterConfig.Network, subnetName) if err != nil { return err } @@ -78,34 +84,62 @@ func syncSubnet(_ *cobra.Command, args []string) error { return nil } +// prepareSubnetPlugin creates subnet plugin to all nodes in the cluster +func prepareSubnetPlugin(hosts []*models.Host, subnetName string) error { + sc, err := app.LoadSidecar(subnetName) + 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, subnetName string, ) ([]string, error) { - subnetPath := "/tmp/" + subnetName + constants.ExportSubnetSuffix - networkFlag := "--cluster " + clusterName - if err := subnetcmd.CallExportSubnet(subnetName, subnetPath); err != nil { + // load cluster config + clusterConf, err := app.GetClusterConfig(clusterName) + if err != nil { return nil, err } + // and get list of subnets + allSubnets := utils.Unique(append(clusterConf.Subnets, subnetName)) + wg := sync.WaitGroup{} wgResults := models.NodeResults{} for _, host := range hosts { wg.Add(1) go func(nodeResults *models.NodeResults, host *models.Host) { defer wg.Done() - subnetExportPath := filepath.Join("/tmp", filepath.Base(subnetPath)) - if err := ssh.RunSSHExportSubnet(host, subnetPath, subnetExportPath); err != nil { + if err := ssh.RunSSHStopNode(host); err != nil { nodeResults.AddResult(host.NodeID, nil, err) - return } - if err := ssh.RunSSHUploadClustersConfig(host, app.GetClustersConfigPath()); err != nil { + if err := ssh.RunSSHRenderAvalancheNodeConfig(app, host, network, allSubnets); err != nil { nodeResults.AddResult(host.NodeID, nil, err) } - - if err := ssh.RunSSHTrackSubnet(host, subnetName, subnetExportPath, networkFlag); err != nil { + if err := ssh.RunSSHSyncSubnetData(app, host, network, subnetName); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHStartNode(host); err != nil { nodeResults.AddResult(host.NodeID, nil, err) return } diff --git a/cmd/nodecmd/updateSubnet.go b/cmd/nodecmd/updateSubnet.go index 3823212ad..d51f06538 100644 --- a/cmd/nodecmd/updateSubnet.go +++ b/cmd/nodecmd/updateSubnet.go @@ -4,15 +4,14 @@ package nodecmd import ( "fmt" - "path/filepath" "sync" "github.com/ava-labs/avalanche-cli/cmd/subnetcmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" - "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/spf13/cobra" ) @@ -38,6 +37,10 @@ func updateSubnet(_ *cobra.Command, args []string) error { if err := checkCluster(clusterName); err != nil { return err } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } if _, err := subnetcmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { return err } @@ -55,7 +58,7 @@ func updateSubnet(_ *cobra.Command, args []string) error { if err := checkHostsAreRPCCompatible(hosts, subnetName); err != nil { return err } - nonUpdatedNodes, err := doUpdateSubnet(hosts, subnetName) + nonUpdatedNodes, err := doUpdateSubnet(hosts, clusterName, clusterConfig.Network, subnetName) if err != nil { return err } @@ -71,27 +74,34 @@ func updateSubnet(_ *cobra.Command, args []string) error { // restart tracking the specified subnet (similar to avalanche subnet join command) func doUpdateSubnet( hosts []*models.Host, + clusterName string, + network models.Network, subnetName string, ) ([]string, error) { - subnetPath := "/tmp/" + subnetName + constants.ExportSubnetSuffix - if err := subnetcmd.CallExportSubnet(subnetName, subnetPath); err != nil { + // load cluster config + clusterConf, err := app.GetClusterConfig(clusterName) + if err != nil { return nil, err } + // and get list of subnets + allSubnets := utils.Unique(append(clusterConf.Subnets, subnetName)) + wg := sync.WaitGroup{} wgResults := models.NodeResults{} for _, host := range hosts { wg.Add(1) go func(nodeResults *models.NodeResults, host *models.Host) { defer wg.Done() - subnetExportPath := filepath.Join("/tmp", filepath.Base(subnetPath)) - if err := ssh.RunSSHExportSubnet(host, subnetPath, subnetExportPath); err != nil { + if err := ssh.RunSSHStopNode(host); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHRenderAvalancheNodeConfig(app, host, network, allSubnets); err != nil { nodeResults.AddResult(host.NodeID, nil, err) - return } - if err := ssh.RunSSHUploadClustersConfig(host, app.GetClustersConfigPath()); err != nil { + if err := ssh.RunSSHSyncSubnetData(app, host, network, subnetName); err != nil { nodeResults.AddResult(host.NodeID, nil, err) } - if err := ssh.RunSSHUpdateSubnet(host, subnetName, subnetExportPath); err != nil { + if err := ssh.RunSSHStartNode(host); err != nil { nodeResults.AddResult(host.NodeID, nil, err) return } diff --git a/cmd/nodecmd/wiz.go b/cmd/nodecmd/wiz.go index ada42c911..40747ca65 100644 --- a/cmd/nodecmd/wiz.go +++ b/cmd/nodecmd/wiz.go @@ -99,7 +99,6 @@ The node wiz command creates a devnet and deploys, sync and validate a subnet in cmd.Flags().StringVar(&cmdLineAlternativeKeyPairName, "alternative-key-pair-name", "", "key pair name to use if default one generates conflicts") cmd.Flags().StringVar(&awsProfile, "aws-profile", constants.AWSDefaultCredential, "aws profile to use") cmd.Flags().BoolVar(&defaultValidatorParams, "default-validator-params", false, "use default weight/start/duration params for subnet validator") - cmd.Flags().BoolVar(&forceSubnetCreate, "force-subnet-create", false, "overwrite the existing subnet configuration if one exists") cmd.Flags().StringVar(&subnetGenesisFile, "subnet-genesis", "", "file path of the subnet genesis") cmd.Flags().BoolVar(&teleporterReady, "teleporter", false, "generate a teleporter-ready vm") @@ -124,7 +123,6 @@ The node wiz command creates a devnet and deploys, sync and validate a subnet in cmd.Flags().BoolVar(&useLatestAvalanchegoReleaseVersion, "latest-avalanchego-version", false, "install latest avalanchego release version on node/s") cmd.Flags().BoolVar(&useLatestAvalanchegoPreReleaseVersion, "latest-avalanchego-pre-release-version", false, "install latest avalanchego pre-release version on node/s") cmd.Flags().StringVar(&useCustomAvalanchegoVersion, "custom-avalanchego-version", "", "install given avalanchego version on node/s") - cmd.Flags().StringVar(&remoteCLIVersion, "remote-cli-version", "", "install given CLI version on remote nodes. defaults to latest CLI release") cmd.Flags().StringSliceVar(&validators, "validators", []string{}, "deploy subnet into given comma separated list of validators. defaults to all cluster nodes") cmd.Flags().BoolVar(&addMonitoring, enableMonitoringFlag, false, " set up Prometheus monitoring for created nodes. Please note that this option creates a separate monitoring instance and incures additional cost") cmd.Flags().IntSliceVar(&numAPINodes, "num-apis", []int{}, "number of API nodes(nodes without stake) to create in the new Devnet") diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 1df0996e9..220c6e9d7 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -216,6 +216,7 @@ const ( CloudNodeSubnetEvmBinaryPath = "/home/ubuntu/.avalanchego/plugins/%s" CloudNodeStakingPath = "/home/ubuntu/.avalanchego/staking/" CloudNodeConfigPath = "/home/ubuntu/.avalanchego/configs/" + CloudNodePluginsPath = "/home/ubuntu/.avalanchego/plugins/" DockerNodeConfigPath = "/.avalanchego/configs/" CloudNodePrometheusConfigPath = "/etc/prometheus/prometheus.yml" CloudNodeCLIConfigBasePath = "/home/ubuntu/.avalanche-cli/" diff --git a/pkg/docker/config.go b/pkg/docker/config.go index e5b5412c5..125e43b2f 100644 --- a/pkg/docker/config.go +++ b/pkg/docker/config.go @@ -12,7 +12,7 @@ import ( ) func prepareAvalanchegoConfig(host *models.Host, networkID string) (string, string, error) { - avagoConf := remoteconfig.DefaultCliAvalancheConfig(host.IP, networkID) + avagoConf := remoteconfig.PrepareAvalancheConfig(host.IP, networkID, nil) nodeConf, err := remoteconfig.RenderAvalancheNodeConfig(avagoConf) if err != nil { return "", "", err diff --git a/pkg/remoteconfig/avalanche.go b/pkg/remoteconfig/avalanche.go index e0aec807b..d4e1c12d6 100644 --- a/pkg/remoteconfig/avalanche.go +++ b/pkg/remoteconfig/avalanche.go @@ -7,6 +7,7 @@ import ( "bytes" "html/template" "path/filepath" + "strings" "github.com/ava-labs/avalanche-cli/pkg/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, subnets []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(subnets, ","), } } @@ -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/pkg/remoteconfig/templates/avalanche-node.tmpl b/pkg/remoteconfig/templates/avalanche-node.tmpl index ef0ffbd24..c5704fd1f 100644 --- a/pkg/remoteconfig/templates/avalanche-node.tmpl +++ b/pkg/remoteconfig/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/pkg/ssh/installer.go b/pkg/ssh/installer.go new file mode 100644 index 000000000..9c4b10b65 --- /dev/null +++ b/pkg/ssh/installer.go @@ -0,0 +1,30 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package ssh + +import ( + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/models" +) + +type HostInstaller struct { + Host *models.Host +} + +func NewHostInstaller(host *models.Host) *HostInstaller { + return &HostInstaller{Host: host} +} + +func (h *HostInstaller) GetArch() (string, string) { + goArhBytes, err := h.Host.Command("go env GOARCH", nil, constants.SSHScriptTimeout) + if err != nil { + return "", "" + } + goOSBytes, err := h.Host.Command("go env GOOS", nil, constants.SSHScriptTimeout) + if err != nil { + return "", "" + } + return strings.TrimSpace(string(goArhBytes)), strings.TrimSpace(string(goOSBytes)) +} diff --git a/pkg/ssh/shell/buildCustomVM.sh b/pkg/ssh/shell/buildCustomVM.sh new file mode 100644 index 000000000..ad813e6f1 --- /dev/null +++ b/pkg/ssh/shell/buildCustomVM.sh @@ -0,0 +1,16 @@ +#!/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 }} {{ .VMBinaryPath }} +echo {{ .VMBinaryPath }} [ok] + + diff --git a/pkg/ssh/shell/buildLoadTest.sh b/pkg/ssh/shell/buildLoadTest.sh index 5569c25ac..ed610a73b 100644 --- a/pkg/ssh/shell/buildLoadTest.sh +++ b/pkg/ssh/shell/buildLoadTest.sh @@ -2,7 +2,7 @@ # delete existing repo directory if it exists # choosing to delete and re-clone to avoid merge conflicts if [ -d {{ .LoadTestRepoDir }} ]; then - rm -r {{ .LoadTestRepoDir }} + rm -rf {{ .LoadTestRepoDir }} fi git clone {{ .LoadTestRepo }} echo "getting load test repo ..." diff --git a/pkg/ssh/shell/getNewSubnetEVMRelease.sh b/pkg/ssh/shell/getNewSubnetEVMRelease.sh index 76072e39a..caecedb7b 100644 --- a/pkg/ssh/shell/getNewSubnetEVMRelease.sh +++ b/pkg/ssh/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 }}" -O "{{ .SubnetEVMArchive }}" #name:TASK [unpack new subnet EVM release] tar xvf "{{ .SubnetEVMArchive}}" diff --git a/pkg/ssh/shell/setupNode.sh b/pkg/ssh/shell/setupNode.sh index 25d2e5ed6..5ee84424b 100644 --- a/pkg/ssh/shell/setupNode.sh +++ b/pkg/ssh/shell/setupNode.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash {{ if .IsE2E }} echo "E2E detected" -echo "CLI Version: {{ .CLIVersion }}" export DEBIAN_FRONTEND=noninteractive sudo apt-get -y update && sudo apt-get -y install busybox-static software-properties-common sudo add-apt-repository -y ppa:longsleep/golang-backports @@ -13,10 +12,4 @@ sudo usermod -aG docker ubuntu sudo chgrp ubuntu /var/run/docker.sock sudo chmod +rw /var/run/docker.sock {{ end }} -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/pkg/ssh/shell/upgradeSubnetEVM.sh b/pkg/ssh/shell/upgradeSubnetEVM.sh new file mode 100644 index 000000000..05ba3634e --- /dev/null +++ b/pkg/ssh/shell/upgradeSubnetEVM.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +#name:TASK [upgrade avalanchego version] +cp -f subnet-evm {{ .VMBinaryPath }} diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 072746272..212fd0414 100644 --- a/pkg/ssh/ssh.go +++ b/pkg/ssh/ssh.go @@ -5,6 +5,8 @@ package ssh import ( "bytes" "embed" + "encoding/json" + "errors" "fmt" "net/url" "os" @@ -14,11 +16,14 @@ import ( "text/template" "time" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/docker" "github.com/ava-labs/avalanche-cli/pkg/monitoring" "github.com/ava-labs/avalanche-cli/pkg/remoteconfig" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -26,7 +31,6 @@ import ( type scriptInputs struct { AvalancheGoVersion string - CLIVersion string SubnetExportFileName string SubnetName string ClusterName string @@ -35,7 +39,7 @@ type scriptInputs struct { IsDevNet bool IsE2E bool NetworkFlag string - SubnetEVMBinaryPath string + VMBinaryPath string SubnetEVMReleaseURL string SubnetEVMArchive string MonitoringDashboardPath string @@ -48,6 +52,10 @@ type scriptInputs struct { CheckoutCommit bool LoadTestResultFile string GrafanaPkg string + CustomVMRepoDir string + CustomVMRepoURL string + CustomVMBranch string + CustomVMBuildScript string } //go:embed shell/*.sh @@ -102,13 +110,13 @@ func PostOverSSH(host *models.Host, path string, requestBody string) ([]byte, er } // RunSSHSetupNode runs script to setup node -func RunSSHSetupNode(host *models.Host, configPath, cliVersion string) error { +func RunSSHSetupNode(host *models.Host, configPath string) error { if err := RunOverSSH( "Setup Node", host, constants.SSHLongRunningScriptTimeout, "shell/setupNode.sh", - scriptInputs{CLIVersion: cliVersion, IsE2E: utils.IsE2E()}, + scriptInputs{IsE2E: utils.IsE2E()}, ); err != nil { return err } @@ -216,7 +224,7 @@ func RunSSHUpgradeSubnetEVM(host *models.Host, subnetEVMBinaryPath string) error host, constants.SSHScriptTimeout, "shell/upgradeSubnetEVM.sh", - scriptInputs{SubnetEVMBinaryPath: subnetEVMBinaryPath}, + scriptInputs{VMBinaryPath: subnetEVMBinaryPath}, ) } @@ -457,22 +465,6 @@ func RunSSHSetupDevNet(host *models.Host, nodeInstanceDirPath string) error { return docker.StartDockerCompose(host, constants.SSHLongRunningScriptTimeout) } -func RunSSHUploadClustersConfig(host *models.Host, localClustersConfigPath string) error { - remoteNodesDir := filepath.Join(constants.CloudNodeCLIConfigBasePath, constants.NodesDir) - if err := host.MkdirAll( - remoteNodesDir, - constants.SSHDirOpsTimeout, - ); err != nil { - return err - } - remoteClustersConfigPath := filepath.Join(remoteNodesDir, constants.ClustersConfigFileName) - return host.Upload( - localClustersConfigPath, - remoteClustersConfigPath, - constants.SSHFileOpsTimeout, - ) -} - // RunSSHUploadStakingFiles uploads staking files to a remote host via SSH. func RunSSHUploadStakingFiles(host *models.Host, nodeInstanceDirPath string) error { if err := host.MkdirAll( @@ -502,42 +494,253 @@ func RunSSHUploadStakingFiles(host *models.Host, nodeInstanceDirPath string) err ) } -// RunSSHExportSubnet exports deployed Subnet from local machine to cloud server -func RunSSHExportSubnet(host *models.Host, exportPath, cloudServerSubnetPath string) error { - // name: copy exported subnet VM spec to cloud server - return host.Upload( - exportPath, - cloudServerSubnetPath, - constants.SSHFileOpsTimeout, - ) -} +// RunSSHRenderAvalancheNodeConfig renders avalanche node config to a remote host via SSH. +func RunSSHRenderAvalancheNodeConfig(app *application.Avalanche, host *models.Host, network models.Network, trackSubnets []string) error { + // get subnet ids + subnetIDs, err := utils.MapWithError(trackSubnets, func(subnetName string) (string, error) { + sc, err := app.LoadSidecar(subnetName) + if err != nil { + return "", err + } else { + return sc.Networks[network.Name()].SubnetID.String(), nil + } + }) + if err != nil { + return err + } -// RunSSHTrackSubnet enables tracking of specified subnet -func RunSSHTrackSubnet(host *models.Host, subnetName, importPath, networkFlag string) error { - if _, err := host.Command(fmt.Sprintf("/home/ubuntu/bin/avalanche subnet import file %s --force", importPath), nil, constants.SSHScriptTimeout); err != nil { + nodeConfFile, err := os.CreateTemp("", "avalanchecli-node-*.yml") + if err != nil { return err } - if err := docker.StopDockerComposeService(host, utils.GetRemoteComposeFile(), "avalanchego", constants.SSHLongRunningScriptTimeout); err != nil { + defer os.Remove(nodeConfFile.Name()) + + avagoConf := remoteconfig.PrepareAvalancheConfig(host.IP, network.NetworkIDFlagValue(), subnetIDs) + // make sure that genesis and bootstrap data is preserved + if genesisFileExists(host) { + avagoConf.GenesisPath = filepath.Join(constants.DockerNodeConfigPath, constants.GenesisFileName) + } + remoteAvagoConfFile, err := getAvalancheGoConfigData(host) + if err != nil { return err } - if _, err := host.Command(fmt.Sprintf("/home/ubuntu/bin/avalanche subnet join %s %s --avalanchego-config /home/ubuntu/.avalanchego/configs/node.json --plugin-dir /home/ubuntu/.avalanchego/plugins --force-write", subnetName, networkFlag), nil, constants.SSHScriptTimeout); err != nil { + bootstrapIDs, _ := utils.GetValueString(remoteAvagoConfFile, "bootstrap-ids") + bootstrapIPs, _ := utils.GetValueString(remoteAvagoConfFile, "bootstrap-ips") + avagoConf.BootstrapIDs = bootstrapIDs + avagoConf.BootstrapIPs = bootstrapIPs + // ready to render node config + nodeConf, err := remoteconfig.RenderAvalancheNodeConfig(avagoConf) + if err != nil { return err } - return docker.StartDockerComposeService(host, utils.GetRemoteComposeFile(), "avalanchego", constants.SSHLongRunningScriptTimeout) + if err := os.WriteFile(nodeConfFile.Name(), nodeConf, constants.WriteReadUserOnlyPerms); err != nil { + return err + } + return host.Upload(nodeConfFile.Name(), remoteconfig.GetRemoteAvalancheNodeConfig(), constants.SSHFileOpsTimeout) } -// RunSSHUpdateSubnet runs avalanche subnet join in cloud server using update subnet info -func RunSSHUpdateSubnet(host *models.Host, subnetName, importPath string) error { - if err := docker.StopDockerComposeService(host, utils.GetRemoteComposeFile(), "avalanchego", constants.SSHLongRunningScriptTimeout); err != nil { +// RunSSHCreatePlugin runs script to create plugin +func RunSSHCreatePlugin(host *models.Host, sc models.Sidecar) error { + vmID, err := sc.GetVMID() + if err != nil { + return err + } + subnetVMBinaryPath := fmt.Sprintf(constants.CloudNodeSubnetEvmBinaryPath, vmID) + hostInstaller := NewHostInstaller(host) + tmpDir, err := host.CreateTempDir() + if err != nil { return err } - if _, err := host.Command(fmt.Sprintf("/home/ubuntu/bin/avalanche subnet import file %s --force", importPath), nil, constants.SSHScriptTimeout); err != nil { + defer func(h *models.Host) { + _ = h.Remove(tmpDir, true) + }(host) + switch { + case sc.VM == models.CustomVM: + ux.Logger.Info("Building Custom VM for %s to %s", host.NodeID, subnetVMBinaryPath) + ux.Logger.Info("Custom VM Params: repo %s branch %s via %s", sc.CustomVMRepoURL, sc.CustomVMBranch, sc.CustomVMBuildScript) + if err := RunOverSSH( + "Build CustomVM", + host, + constants.SSHLongRunningScriptTimeout, + "shell/buildCustomVM.sh", + scriptInputs{ + CustomVMRepoDir: tmpDir, + CustomVMRepoURL: sc.CustomVMRepoURL, + CustomVMBranch: sc.CustomVMBranch, + CustomVMBuildScript: sc.CustomVMBuildScript, + VMBinaryPath: subnetVMBinaryPath, + }, + ); err != nil { + return err + } + + case sc.VM == models.SubnetEvm: + ux.Logger.Info("Installing Subnet EVM for %s", host.NodeID) + dl := binutils.NewSubnetEVMDownloader() + installURL, _, err := dl.GetDownloadURL(sc.VMVersion, hostInstaller) // extension is tar.gz + if err != nil { + return err + } + + archiveName := "subnet-evm.tar.gz" + archiveFullPath := filepath.Join(tmpDir, archiveName) + + // download and install subnet evm + if _, err := host.Command(fmt.Sprintf("%s %s -O %s", "busybox wget", installURL, archiveFullPath), nil, constants.SSHLongRunningScriptTimeout); err != nil { + return err + } + if _, err := host.Command(fmt.Sprintf("tar -xzf %s -C %s", archiveFullPath, tmpDir), nil, constants.SSHLongRunningScriptTimeout); err != nil { + return err + } + + if _, err := host.Command(fmt.Sprintf("mv -f %s/subnet-evm %s", tmpDir, subnetVMBinaryPath), nil, constants.SSHLongRunningScriptTimeout); err != nil { + return err + } + default: + return fmt.Errorf("unexpected error: unsupported VM type: %s", sc.VM) + } + + return nil +} + +// RunSSHMergeSubnetNodeConfig merges subnet node config to the node config on the remote host +func mergeSubnetNodeConfig(host *models.Host, subnetNodeConfigPath string) error { + if subnetNodeConfigPath == "" { + return fmt.Errorf("subnet node config path is empty") + } + tmpFile, err := os.CreateTemp("", "avalanchecli-subnet-node-*.yml") + if err != nil { return err } - if _, err := host.Command(fmt.Sprintf("/home/ubuntu/bin/avalanche subnet join %s --fuji --avalanchego-config /home/ubuntu/.avalanchego/configs/node.json --plugin-dir /home/ubuntu/.avalanchego/plugins --force-write", subnetName), nil, constants.SSHScriptTimeout); err != nil { + defer os.Remove(tmpFile.Name()) + if err := host.Download(remoteconfig.GetRemoteAvalancheNodeConfig(), tmpFile.Name(), constants.SSHFileOpsTimeout); err != nil { return err } - return docker.StartDockerComposeService(host, utils.GetRemoteComposeFile(), "avalanchego", constants.SSHLongRunningScriptTimeout) + remoteNodeConfigBytes, err := os.ReadFile(tmpFile.Name()) + if err != nil { + return fmt.Errorf("error reading remote node config: %w", err) + } + var remoteNodeConfig map[string]interface{} + if err := json.Unmarshal(remoteNodeConfigBytes, &remoteNodeConfig); err != nil { + return fmt.Errorf("error unmarshalling remote node config: %w", err) + } + subnetNodeConfigBytes, err := os.ReadFile(subnetNodeConfigPath) + if err != nil { + return fmt.Errorf("error reading subnet node config: %w", err) + } + var subnetNodeConfig map[string]interface{} + if err := json.Unmarshal(subnetNodeConfigBytes, &subnetNodeConfig); err != nil { + return fmt.Errorf("error unmarshalling subnet node config: %w", err) + } + mergedNodeConfig := utils.MergeJSONMaps(remoteNodeConfig, subnetNodeConfig) + mergedNodeConfigBytes, err := json.MarshalIndent(mergedNodeConfig, "", " ") + if err != nil { + return fmt.Errorf("error creating merged node config: %w", err) + } + if err := os.WriteFile(tmpFile.Name(), mergedNodeConfigBytes, constants.WriteReadUserOnlyPerms); err != nil { + return err + } + return host.Upload(tmpFile.Name(), remoteconfig.GetRemoteAvalancheNodeConfig(), constants.SSHFileOpsTimeout) +} + +// RunSSHSyncSubnetData syncs subnet data required +func RunSSHSyncSubnetData(app *application.Avalanche, host *models.Host, network models.Network, subnetName string) error { + sc, err := app.LoadSidecar(subnetName) + if err != nil { + return err + } + subnetID := sc.Networks[network.Name()].SubnetID + if subnetID == ids.Empty { + return errors.New("subnet id is empty") + } + subnetIDStr := subnetID.String() + blockchainID := sc.Networks[network.Name()].BlockchainID + // genesis config + genesisFilename := filepath.Join(app.GetNodesDir(), host.GetCloudID(), constants.GenesisFileName) + if err := host.Upload(genesisFilename, remoteconfig.GetRemoteAvalancheGenesis(), constants.SSHFileOpsTimeout); err != nil { + return fmt.Errorf("error uploading genesis config to %s: %w", remoteconfig.GetRemoteAvalancheGenesis(), err) + } + // end genesis config + // subnet node config + subnetNodeConfigPath := app.GetAvagoNodeConfigPath(subnetName) + if utils.FileExists(subnetNodeConfigPath) { + if err := mergeSubnetNodeConfig(host, subnetNodeConfigPath); err != nil { + return err + } + } + // subnet config + if app.AvagoSubnetConfigExists(subnetName) { + subnetConfig, err := app.LoadRawAvagoSubnetConfig(subnetName) + if err != nil { + return fmt.Errorf("error loading subnet config: %w", err) + } + subnetConfigFile, err := os.CreateTemp("", "avalanchecli-subnet-*.json") + if err != nil { + return err + } + defer os.Remove(subnetConfigFile.Name()) + if err := os.WriteFile(subnetConfigFile.Name(), subnetConfig, constants.WriteReadUserOnlyPerms); err != nil { + return err + } + subnetConfigPath := filepath.Join(constants.CloudNodeConfigPath, "subnets", subnetIDStr+".json") + if err := host.MkdirAll(filepath.Dir(subnetConfigPath), constants.SSHDirOpsTimeout); err != nil { + return err + } + if err := host.Upload(subnetConfigFile.Name(), subnetConfigPath, constants.SSHFileOpsTimeout); err != nil { + return fmt.Errorf("error uploading subnet config to %s: %w", subnetConfigPath, err) + } + } + // end subnet config + + // chain config + if blockchainID != ids.Empty && app.ChainConfigExists(subnetName) { + chainConfigFile, err := os.CreateTemp("", "avalanchecli-chain-*.json") + if err != nil { + return err + } + defer os.Remove(chainConfigFile.Name()) + chainConfig, err := app.LoadRawChainConfig(subnetName) + if err != nil { + return fmt.Errorf("error loading chain config: %w", err) + } + if err := os.WriteFile(chainConfigFile.Name(), chainConfig, constants.WriteReadUserOnlyPerms); err != nil { + return err + } + chainConfigPath := filepath.Join(constants.CloudNodeConfigPath, "chains", blockchainID.String(), "config.json") + if err := host.MkdirAll(filepath.Dir(chainConfigPath), constants.SSHDirOpsTimeout); err != nil { + return err + } + if err := host.Upload(chainConfigFile.Name(), chainConfigPath, constants.SSHFileOpsTimeout); err != nil { + return fmt.Errorf("error uploading chain config to %s: %w", chainConfigPath, err) + } + } + // end chain config + + // network upgrade + if app.NetworkUpgradeExists(subnetName) { + networkUpgradesFile, err := os.CreateTemp("", "avalanchecli-network-*.json") + if err != nil { + return err + } + defer os.Remove(networkUpgradesFile.Name()) + networkUpgrades, err := app.LoadRawNetworkUpgrades(subnetName) + if err != nil { + return fmt.Errorf("error loading network upgrades: %w", err) + } + if err := os.WriteFile(networkUpgradesFile.Name(), networkUpgrades, constants.WriteReadUserOnlyPerms); err != nil { + return err + } + networkUpgradesPath := filepath.Join(constants.CloudNodeConfigPath, "subnets", "chains", blockchainID.String(), "upgrade.json") + if err := host.MkdirAll(filepath.Dir(networkUpgradesPath), constants.SSHDirOpsTimeout); err != nil { + return err + } + if err := host.Upload(networkUpgradesFile.Name(), networkUpgradesPath, constants.SSHFileOpsTimeout); err != nil { + return fmt.Errorf("error uploading network upgrades to %s: %w", networkUpgradesPath, err) + } + } + // end network upgrade + + return nil } func RunSSHBuildLoadTestCode(host *models.Host, loadTestRepo, loadTestPath, loadTestGitCommit, repoDirName, loadTestBranch string, checkoutCommit bool) error { @@ -702,3 +905,31 @@ func composeFileExists(host *models.Host) bool { composeFileExists, _ := host.FileExists(utils.GetRemoteComposeFile()) return composeFileExists } + +func genesisFileExists(host *models.Host) bool { + genesisFileExists, _ := host.FileExists(filepath.Join(constants.CloudNodeConfigPath, constants.GenesisFileName)) + return genesisFileExists +} + +func getAvalancheGoConfigData(host *models.Host) (map[string]interface{}, error) { + // get remote node.json file + nodeJSONPath := filepath.Join(constants.CloudNodeConfigPath, constants.NodeFileName) + tmpFile, err := os.CreateTemp("", "avalanchecli-node-*.json") + if err != nil { + return nil, err + } + defer os.Remove(tmpFile.Name()) + if err := host.Download(nodeJSONPath, tmpFile.Name(), constants.SSHFileOpsTimeout); err != nil { + return nil, err + } + // parse node.json file + nodeJSON, err := os.ReadFile(tmpFile.Name()) + 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 +} diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 0380056aa..d4ba63b6b 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -559,3 +559,11 @@ func Command(cmdLine string, params ...string) *exec.Cmd { c.Env = os.Environ() return c } + +// GetValueString returns the value of a key in a map as a string. +func GetValueString(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) +} diff --git a/pkg/utils/json.go b/pkg/utils/json.go index d35b7eb30..cad2a773e 100644 --- a/pkg/utils/json.go +++ b/pkg/utils/json.go @@ -25,3 +25,14 @@ func ValidateJSON(path string) ([]byte, error) { return contentBytes, nil } + +// MergeJsonMaps merges two maps of type map[string]interface{} +func MergeJSONMaps(a, b map[string]interface{}) map[string]interface{} { + for k, v := range b { + if _, ok := a[k]; !ok { + a[k] = v + } + // skip if key already exists in a + } + return a +} diff --git a/pkg/utils/json_test.go b/pkg/utils/json_test.go new file mode 100644 index 000000000..419295c1a --- /dev/null +++ b/pkg/utils/json_test.go @@ -0,0 +1,67 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package utils + +import ( + "reflect" + "testing" +) + +func TestMergeJsonMaps(t *testing.T) { + tests := []struct { + name string + a map[string]interface{} + b map[string]interface{} + expected map[string]interface{} + }{ + { + name: "no conflict", + a: map[string]interface{}{"key1": "value1"}, + b: map[string]interface{}{"key2": "value2"}, + expected: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "with conflict", + a: map[string]interface{}{"key1": "value1"}, + b: map[string]interface{}{"key1": "new_value1", "key2": "value2"}, + expected: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "empty map a", + a: map[string]interface{}{}, + b: map[string]interface{}{"key1": "value1"}, + expected: map[string]interface{}{ + "key1": "value1", + }, + }, + { + name: "empty map b", + a: map[string]interface{}{"key1": "value1"}, + b: map[string]interface{}{}, + expected: map[string]interface{}{ + "key1": "value1", + }, + }, + { + name: "both maps empty", + a: map[string]interface{}{}, + b: map[string]interface{}{}, + expected: map[string]interface{}{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MergeJSONMaps(tt.a, tt.b) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("MergeJsonMaps(%v, %v) = %v; expected %v", tt.a, tt.b, result, tt.expected) + } + }) + } +}