mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
976 lines
32 KiB
Go
976 lines
32 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/luxfi/node/v2/v2/config"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/application"
|
|
"github.com/luxfi/cli/v2/pkg/binutils"
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/docker"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/monitoring"
|
|
"github.com/luxfi/cli/v2/pkg/remoteconfig"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
type scriptInputs struct {
|
|
LuxdVersion string
|
|
SubnetExportFileName string
|
|
SubnetName string
|
|
ClusterName string
|
|
GoVersion string
|
|
IsDevNet bool
|
|
IsE2E bool
|
|
NetworkFlag string
|
|
VMBinaryPath string
|
|
SubnetEVMReleaseURL string
|
|
SubnetEVMArchive string
|
|
MonitoringDashboardPath string
|
|
LoadTestRepoDir string
|
|
LoadTestRepo string
|
|
LoadTestPath string
|
|
LoadTestCommand string
|
|
LoadTestBranch string
|
|
LoadTestGitCommit string
|
|
CheckoutCommit bool
|
|
LoadTestResultFile string
|
|
GrafanaPkg string
|
|
CustomVMRepoDir string
|
|
CustomVMRepoURL string
|
|
CustomVMBranch string
|
|
CustomVMBuildScript string
|
|
}
|
|
|
|
//go:embed shell/*.sh
|
|
var script embed.FS
|
|
|
|
// RunOverSSH runs provided script path over ssh.
|
|
// This script can be template as it will be rendered using scriptInputs vars
|
|
func RunOverSSH(
|
|
scriptDesc string,
|
|
host *models.Host,
|
|
timeout time.Duration,
|
|
scriptPath string,
|
|
templateVars scriptInputs,
|
|
) error {
|
|
startTime := time.Now()
|
|
shellScript, err := script.ReadFile(scriptPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var script bytes.Buffer
|
|
t, err := template.New(scriptDesc).Parse(string(shellScript))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = t.Execute(&script, templateVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if output, err := host.Command(script.String(), nil, timeout); err != nil {
|
|
return fmt.Errorf("%w: %s", err, string(output))
|
|
}
|
|
executionTime := time.Since(startTime)
|
|
ux.Logger.Info("RunOverSSH[%s]%s took %s with err: %v", host.NodeID, scriptDesc, executionTime, err)
|
|
return nil
|
|
}
|
|
|
|
func PostOverSSH(host *models.Host, path string, requestBody string) ([]byte, error) {
|
|
if path == "" {
|
|
path = "/ext/info"
|
|
}
|
|
localhost, err := url.Parse(constants.LocalAPIEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
requestHeaders := fmt.Sprintf("POST %s HTTP/1.1\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
|
|
return host.Forward(httpRequest, constants.SSHPOSTTimeout)
|
|
}
|
|
|
|
// RunSSHSetupNode runs script to setup node
|
|
func RunSSHSetupNode(host *models.Host, configPath string) error {
|
|
if err := RunOverSSH(
|
|
"Setup Node",
|
|
host,
|
|
constants.SSHLongRunningScriptTimeout,
|
|
"shell/setupNode.sh",
|
|
scriptInputs{IsE2E: utils.IsE2E()},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
// name: copy metrics config to cloud server
|
|
ux.Logger.Info("Uploading config %s to server %s: %s", configPath, host.NodeID, filepath.Join(constants.CloudNodeCLIConfigBasePath, filepath.Base(configPath)))
|
|
if err := host.Upload(
|
|
configPath,
|
|
filepath.Join(constants.CloudNodeCLIConfigBasePath, filepath.Base(configPath)),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunSSHSetupDockerService runs script to setup docker compose service for CLI
|
|
func RunSSHSetupDockerService(host *models.Host) error {
|
|
if host.IsSystemD() {
|
|
return RunOverSSH(
|
|
"Setup Docker Service",
|
|
host,
|
|
constants.SSHLongRunningScriptTimeout,
|
|
"shell/setupDockerService.sh",
|
|
scriptInputs{},
|
|
)
|
|
} else {
|
|
// no need to setup docker service
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RunSSHRestartNode runs script to restart luxd
|
|
func RunSSHRestartNode(host *models.Host) error {
|
|
remoteComposeFile := utils.GetRemoteComposeFile()
|
|
luxdService := "luxd"
|
|
if utils.IsE2E() {
|
|
luxdService += utils.E2ESuffix(host.IP)
|
|
}
|
|
return docker.RestartDockerComposeService(host, remoteComposeFile, luxdService, constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// ComposeSSHSetupWarpRelayer used docker compose to setup AWM Relayer
|
|
func ComposeSSHSetupWarpRelayer(host *models.Host, relayerVersion string) error {
|
|
if err := docker.ComposeSSHSetupWarpRelayer(host, relayerVersion); err != nil {
|
|
return err
|
|
}
|
|
return docker.StartDockerComposeService(host, utils.GetRemoteComposeFile(), "warp-relayer", constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// RunSSHStartWarpRelayerService runs script to start an AWM Relayer Service
|
|
func RunSSHStartWarpRelayerService(host *models.Host) error {
|
|
return docker.StartDockerComposeService(host, utils.GetRemoteComposeFile(), "warp-relayer", constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// RunSSHStopWarpRelayerService runs script to start an AWM Relayer Service
|
|
func RunSSHStopWarpRelayerService(host *models.Host) error {
|
|
return docker.StopDockerComposeService(host, utils.GetRemoteComposeFile(), "warp-relayer", constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// RunSSHUpgradeLuxgo runs script to upgrade luxd
|
|
func RunSSHUpgradeLuxgo(host *models.Host, luxdVersion string) error {
|
|
withMonitoring, err := docker.WasNodeSetupWithMonitoring(host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := docker.ComposeOverSSH("Compose Node",
|
|
host,
|
|
constants.SSHScriptTimeout,
|
|
"templates/luxd.docker-compose.yml",
|
|
docker.DockerComposeInputs{
|
|
LuxgoVersion: luxdVersion,
|
|
WithMonitoring: withMonitoring,
|
|
WithLuxgo: true,
|
|
E2E: utils.IsE2E(),
|
|
E2EIP: utils.E2EConvertIP(host.IP),
|
|
E2ESuffix: utils.E2ESuffix(host.IP),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return docker.RestartDockerCompose(host, constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// RunSSHStartNode runs script to start luxd
|
|
func RunSSHStartNode(host *models.Host) error {
|
|
if utils.IsE2E() && utils.E2EDocker() {
|
|
return RunOverSSH(
|
|
"E2E Start Luxgo",
|
|
host,
|
|
constants.SSHScriptTimeout,
|
|
"shell/e2e_startNode.sh",
|
|
scriptInputs{},
|
|
)
|
|
}
|
|
return docker.StartDockerComposeService(host, utils.GetRemoteComposeFile(), "luxd", constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// RunSSHStopNode runs script to stop luxd
|
|
func RunSSHStopNode(host *models.Host) error {
|
|
if utils.IsE2E() && utils.E2EDocker() {
|
|
return RunOverSSH(
|
|
"E2E Stop Luxgo",
|
|
host,
|
|
constants.SSHScriptTimeout,
|
|
"shell/e2e_stopNode.sh",
|
|
scriptInputs{},
|
|
)
|
|
}
|
|
return docker.StopDockerComposeService(host, utils.GetRemoteComposeFile(), "luxd", constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
func replaceCustomVarDashboardValues(customGrafanaDashboardFileName, chainID string) error {
|
|
content, err := os.ReadFile(customGrafanaDashboardFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
replacements := []struct {
|
|
old string
|
|
new string
|
|
}{
|
|
{"\"text\": \"CHAIN_ID_VAL\"", fmt.Sprintf("\"text\": \"%v\"", chainID)},
|
|
{"\"value\": \"CHAIN_ID_VAL\"", fmt.Sprintf("\"value\": \"%v\"", chainID)},
|
|
{"\"query\": \"CHAIN_ID_VAL\"", fmt.Sprintf("\"query\": \"%v\"", chainID)},
|
|
}
|
|
for _, r := range replacements {
|
|
content = []byte(strings.ReplaceAll(string(content), r.old, r.new))
|
|
}
|
|
err = os.WriteFile(customGrafanaDashboardFileName, content, constants.WriteReadUserOnlyPerms)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func RunSSHUpdateMonitoringDashboards(host *models.Host, monitoringDashboardPath, customGrafanaDashboardPath, chainID string) error {
|
|
remoteDashboardsPath := utils.GetRemoteComposeServicePath("grafana", "dashboards")
|
|
if !sdkutils.DirExists(monitoringDashboardPath) {
|
|
return fmt.Errorf("%s does not exist", monitoringDashboardPath)
|
|
}
|
|
if customGrafanaDashboardPath != "" && utils.FileExists(utils.ExpandHome(customGrafanaDashboardPath)) {
|
|
if err := utils.FileCopy(utils.ExpandHome(customGrafanaDashboardPath), filepath.Join(monitoringDashboardPath, constants.CustomGrafanaDashboardJSON)); err != nil {
|
|
return err
|
|
}
|
|
if err := replaceCustomVarDashboardValues(filepath.Join(monitoringDashboardPath, constants.CustomGrafanaDashboardJSON), chainID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := host.MkdirAll(remoteDashboardsPath, constants.SSHFileOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(
|
|
filepath.Join(monitoringDashboardPath, constants.CustomGrafanaDashboardJSON),
|
|
filepath.Join(remoteDashboardsPath, constants.CustomGrafanaDashboardJSON),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return docker.RestartDockerComposeService(host, utils.GetRemoteComposeFile(), "grafana", constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
func RunSSHSetupMonitoringFolders(host *models.Host) error {
|
|
for _, folder := range remoteconfig.RemoteFoldersToCreateMonitoring() {
|
|
if err := host.MkdirAll(folder, constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func RunSSHCopyMonitoringDashboards(host *models.Host, monitoringDashboardPath string) error {
|
|
// TODO: download dashboards from github instead
|
|
remoteDashboardsPath := utils.GetRemoteComposeServicePath("grafana", "dashboards")
|
|
if !sdkutils.DirExists(monitoringDashboardPath) {
|
|
return fmt.Errorf("%s does not exist", monitoringDashboardPath)
|
|
}
|
|
if err := host.MkdirAll(remoteDashboardsPath, constants.SSHFileOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
dashboards, err := os.ReadDir(monitoringDashboardPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, dashboard := range dashboards {
|
|
if err := host.Upload(
|
|
filepath.Join(monitoringDashboardPath, dashboard.Name()),
|
|
filepath.Join(remoteDashboardsPath, dashboard.Name()),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if composeFileExists(host) {
|
|
return docker.RestartDockerComposeService(host, utils.GetRemoteComposeFile(), "grafana", constants.SSHLongRunningScriptTimeout)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func RunSSHCopyYAMLFile(host *models.Host, yamlFilePath string) error {
|
|
if err := host.Upload(
|
|
yamlFilePath,
|
|
fmt.Sprintf("/home/ubuntu/%s", filepath.Base(yamlFilePath)),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func RunSSHSetupPrometheusConfig(host *models.Host, luxdPorts, machinePorts, loadTestPorts []string) error {
|
|
for _, folder := range remoteconfig.PrometheusFoldersToCreate() {
|
|
if err := host.MkdirAll(folder, constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cloudNodePrometheusConfigTemp := utils.GetRemoteComposeServicePath("prometheus", "prometheus.yml")
|
|
promConfig, err := os.CreateTemp("", "prometheus")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(promConfig.Name())
|
|
if err := monitoring.WritePrometheusConfig(promConfig.Name(), luxdPorts, machinePorts, loadTestPorts); err != nil {
|
|
return err
|
|
}
|
|
|
|
return host.Upload(
|
|
promConfig.Name(),
|
|
cloudNodePrometheusConfigTemp,
|
|
constants.SSHFileOpsTimeout,
|
|
)
|
|
}
|
|
|
|
func RunSSHSetupLokiConfig(host *models.Host, port int) error {
|
|
for _, folder := range remoteconfig.LokiFoldersToCreate() {
|
|
if err := host.MkdirAll(folder, constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cloudNodeLokiConfigTemp := utils.GetRemoteComposeServicePath("loki", "loki.yml")
|
|
lokiConfig, err := os.CreateTemp("", "loki")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(lokiConfig.Name())
|
|
if err := monitoring.WriteLokiConfig(lokiConfig.Name(), strconv.Itoa(port)); err != nil {
|
|
return err
|
|
}
|
|
return host.Upload(
|
|
lokiConfig.Name(),
|
|
cloudNodeLokiConfigTemp,
|
|
constants.SSHFileOpsTimeout,
|
|
)
|
|
}
|
|
|
|
func RunSSHSetupPromtailConfig(host *models.Host, lokiIP string, lokiPort int, cloudID string, nodeID string, chainID string) error {
|
|
for _, folder := range remoteconfig.PromtailFoldersToCreate() {
|
|
if err := host.MkdirAll(folder, constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cloudNodePromtailConfigTemp := utils.GetRemoteComposeServicePath("promtail", "promtail.yml")
|
|
promtailConfig, err := os.CreateTemp("", "promtail")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(promtailConfig.Name())
|
|
|
|
if err := monitoring.WritePromtailConfig(promtailConfig.Name(), lokiIP, strconv.Itoa(lokiPort), cloudID, nodeID, chainID); err != nil {
|
|
return err
|
|
}
|
|
return host.Upload(
|
|
promtailConfig.Name(),
|
|
cloudNodePromtailConfigTemp,
|
|
constants.SSHFileOpsTimeout,
|
|
)
|
|
}
|
|
|
|
func RunSSHDownloadNodePrometheusConfig(host *models.Host, nodeInstanceDirPath string) error {
|
|
return host.Download(
|
|
constants.CloudNodePrometheusConfigPath,
|
|
filepath.Join(nodeInstanceDirPath, constants.NodePrometheusConfigFileName),
|
|
constants.SSHFileOpsTimeout,
|
|
)
|
|
}
|
|
|
|
func RunSSHUploadNodeWarpRelayerConfig(host *models.Host, nodeInstanceDirPath string) error {
|
|
cloudWarpRelayerConfigDir := filepath.Join(constants.CloudNodeCLIConfigBasePath, constants.ServicesDir, constants.WarpRelayerInstallDir)
|
|
if err := host.MkdirAll(cloudWarpRelayerConfigDir, constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
return host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.ServicesDir, constants.WarpRelayerInstallDir, constants.WarpRelayerConfigFilename),
|
|
filepath.Join(cloudWarpRelayerConfigDir, constants.WarpRelayerConfigFilename),
|
|
constants.SSHFileOpsTimeout,
|
|
)
|
|
}
|
|
|
|
// RunSSHGetNewSubnetEVMRelease runs script to download new subnet evm
|
|
func RunSSHGetNewSubnetEVMRelease(host *models.Host, subnetEVMReleaseURL, subnetEVMArchive string) error {
|
|
return RunOverSSH(
|
|
"Get Subnet EVM Release",
|
|
host,
|
|
constants.SSHScriptTimeout,
|
|
"shell/getNewSubnetEVMRelease.sh",
|
|
scriptInputs{SubnetEVMReleaseURL: subnetEVMReleaseURL, SubnetEVMArchive: subnetEVMArchive},
|
|
)
|
|
}
|
|
|
|
// RunSSHSetupDevNet runs script to setup devnet
|
|
func RunSSHSetupDevNet(host *models.Host, nodeInstanceDirPath string) error {
|
|
if err := host.MkdirAll(
|
|
constants.CloudNodeConfigPath,
|
|
constants.SSHDirOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.GenesisFileName),
|
|
remoteconfig.GetRemoteLuxGenesis(),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.UpgradeFileName),
|
|
remoteconfig.GetRemoteLuxUpgrade(),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.NodeFileName),
|
|
remoteconfig.GetRemoteLuxNodeConfig(),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := docker.StopDockerCompose(host, constants.SSHLongRunningScriptTimeout); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Remove("/home/ubuntu/.luxd/db", true); err != nil {
|
|
return err
|
|
}
|
|
if err := host.MkdirAll("/home/ubuntu/.luxd/db", constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Remove("/home/ubuntu/.luxd/logs", true); err != nil {
|
|
return err
|
|
}
|
|
if err := host.MkdirAll("/home/ubuntu/.luxd/logs", constants.SSHDirOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
return docker.StartDockerCompose(host, constants.SSHLongRunningScriptTimeout)
|
|
}
|
|
|
|
// RunSSHUploadStakingFiles uploads staking files to a remote host via SSH.
|
|
func RunSSHUploadStakingFiles(host *models.Host, nodeInstanceDirPath string) error {
|
|
if err := host.MkdirAll(
|
|
constants.CloudNodeStakingPath,
|
|
constants.SSHDirOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.StakerCertFileName),
|
|
filepath.Join(constants.CloudNodeStakingPath, constants.StakerCertFileName),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.StakerKeyFileName),
|
|
filepath.Join(constants.CloudNodeStakingPath, constants.StakerKeyFileName),
|
|
constants.SSHFileOpsTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return host.Upload(
|
|
filepath.Join(nodeInstanceDirPath, constants.BLSKeyFileName),
|
|
filepath.Join(constants.CloudNodeStakingPath, constants.BLSKeyFileName),
|
|
constants.SSHFileOpsTimeout,
|
|
)
|
|
}
|
|
|
|
// RunSSHRenderLuxdAliasConfigFile renders lux alias config to a remote host via SSH.
|
|
func RunSSHRenderLuxdAliasConfigFile(
|
|
host *models.Host,
|
|
blockchainID string,
|
|
subnetAliases []string,
|
|
) error {
|
|
aliasToBlockchain := map[string]string{}
|
|
if aliasConfigFileExists(host) {
|
|
// load remote aliases
|
|
remoteAliases, err := getLuxdAliasData(host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for chainID, aliases := range remoteAliases {
|
|
for _, alias := range aliases {
|
|
aliasToBlockchain[alias] = chainID
|
|
}
|
|
}
|
|
}
|
|
for _, alias := range subnetAliases {
|
|
aliasToBlockchain[alias] = blockchainID
|
|
}
|
|
newAliases := map[string][]string{}
|
|
for alias, chainID := range aliasToBlockchain {
|
|
newAliases[chainID] = append(newAliases[chainID], alias)
|
|
}
|
|
aliasConf, err := json.MarshalIndent(newAliases, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
aliasConfFile, err := os.CreateTemp("", "luxcli-alias-*.yml")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(aliasConfFile.Name())
|
|
if err := os.WriteFile(aliasConfFile.Name(), aliasConf, constants.DefaultPerms755); err != nil {
|
|
return err
|
|
}
|
|
if err := host.Upload(aliasConfFile.Name(), remoteconfig.GetRemoteLuxAliasesConfig(), constants.SSHFileOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunSSHRenderLuxNodeConfig renders lux node config to a remote host via SSH.
|
|
func RunSSHRenderLuxNodeConfig(
|
|
app *application.Lux,
|
|
host *models.Host,
|
|
network models.Network,
|
|
trackSubnets []string,
|
|
isAPIHost bool,
|
|
) 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
|
|
}
|
|
|
|
luxdConf := remoteconfig.PrepareLuxConfig(host.IP, network.NetworkIDFlagValue(), subnetIDs)
|
|
// preserve remote configuration if it exists
|
|
if nodeConfigFileExists(host) {
|
|
// make sure that genesis and bootstrap data is preserved
|
|
if genesisFileExists(host) {
|
|
luxdConf.GenesisPath = filepath.Join(constants.DockerNodeConfigPath, constants.GenesisFileName)
|
|
}
|
|
if upgradeFileExists(host) {
|
|
luxdConf.UpgradePath = filepath.Join(constants.DockerNodeConfigPath, constants.UpgradeFileName)
|
|
}
|
|
if network.Kind == models.Local || network.Kind == models.Devnet || isAPIHost {
|
|
luxdConf.HTTPHost = "0.0.0.0"
|
|
}
|
|
remoteLuxdConf, err := getLuxdConfigData(host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// ignore errors if bootstrap configuration is not present - it's fine
|
|
bootstrapIDs, _ := utils.StringValue(remoteLuxdConf, "bootstrap-ids")
|
|
bootstrapIPs, _ := utils.StringValue(remoteLuxdConf, "bootstrap-ips")
|
|
luxdConf.BootstrapIDs = bootstrapIDs
|
|
luxdConf.BootstrapIPs = bootstrapIPs
|
|
partialSyncI, ok := remoteLuxdConf[config.PartialSyncPrimaryNetworkKey]
|
|
if !ok {
|
|
fmt.Println("Key not found in remoteLuxdConf:", config.PartialSyncPrimaryNetworkKey)
|
|
} else {
|
|
partialSync, _ := partialSyncI.(string)
|
|
if partialSync == "true" {
|
|
luxdConf.PartialSync = true
|
|
} else {
|
|
luxdConf.PartialSync = false
|
|
}
|
|
}
|
|
}
|
|
// ready to render node config
|
|
nodeConf, err := remoteconfig.RenderLuxNodeConfig(luxdConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return host.UploadBytes(nodeConf, remoteconfig.GetRemoteLuxNodeConfig(), constants.SSHFileOpsTimeout)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
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,
|
|
GoVersion: constants.BuildEnvGolangVersion,
|
|
},
|
|
); 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("node config path is empty")
|
|
}
|
|
remoteNodeConfigBytes, err := host.ReadFileBytes(remoteconfig.GetRemoteLuxNodeConfig(), constants.SSHFileOpsTimeout)
|
|
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 node config: %w", err)
|
|
}
|
|
var subnetNodeConfig map[string]interface{}
|
|
if err := json.Unmarshal(subnetNodeConfigBytes, &subnetNodeConfig); err != nil {
|
|
return fmt.Errorf("error unmarshalling node config: %w", err)
|
|
}
|
|
maps.Copy(remoteNodeConfig, subnetNodeConfig) // merge remote config into local subnet config. subnetNodeConfig takes precedence
|
|
mergedNodeConfigBytes, err := json.MarshalIndent(remoteNodeConfig, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("error creating merged node config: %w", err)
|
|
}
|
|
return host.UploadBytes(mergedNodeConfigBytes, remoteconfig.GetRemoteLuxNodeConfig(), constants.SSHFileOpsTimeout)
|
|
}
|
|
|
|
// RunSSHSyncSubnetData syncs subnet data required
|
|
func RunSSHSyncSubnetData(app *application.Lux, 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 utils.FileExists(genesisFilename) {
|
|
if err := host.Upload(genesisFilename, remoteconfig.GetRemoteLuxGenesis(), constants.SSHFileOpsTimeout); err != nil {
|
|
return fmt.Errorf("error uploading genesis config to %s: %w", remoteconfig.GetRemoteLuxGenesis(), err)
|
|
}
|
|
}
|
|
// end genesis config
|
|
// subnet node config
|
|
subnetNodeConfigPath := app.GetLuxdNodeConfigPath(subnetName)
|
|
if utils.FileExists(subnetNodeConfigPath) {
|
|
if err := mergeSubnetNodeConfig(host, subnetNodeConfigPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// subnet config
|
|
if app.LuxdSubnetConfigExists(subnetName) {
|
|
subnetConfig, err := app.LoadRawLuxdSubnetConfig(subnetName)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading blockchain config: %w", 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.UploadBytes(subnetConfig, subnetConfigPath, constants.SSHFileOpsTimeout); err != nil {
|
|
return fmt.Errorf("error uploading blockchain config to %s: %w", subnetConfigPath, err)
|
|
}
|
|
}
|
|
// end subnet config
|
|
|
|
// chain config
|
|
if blockchainID != ids.Empty && app.ChainConfigExists(subnetName) {
|
|
chainConfig, err := app.LoadRawChainConfig(subnetName)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading chain config: %w", 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.UploadBytes(chainConfig, 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) {
|
|
networkUpgrades, err := app.LoadRawNetworkUpgrades(subnetName)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading network upgrades: %w", 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.UploadBytes(networkUpgrades, 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 {
|
|
return StreamOverSSH(
|
|
"Build Load Test",
|
|
host,
|
|
constants.SSHLongRunningScriptTimeout,
|
|
"shell/buildLoadTest.sh",
|
|
scriptInputs{
|
|
LoadTestRepoDir: repoDirName,
|
|
LoadTestRepo: loadTestRepo, LoadTestPath: loadTestPath, LoadTestGitCommit: loadTestGitCommit,
|
|
CheckoutCommit: checkoutCommit, LoadTestBranch: loadTestBranch,
|
|
},
|
|
)
|
|
}
|
|
|
|
func RunSSHBuildLoadTestDependencies(host *models.Host) error {
|
|
return RunOverSSH(
|
|
"Build Load Test",
|
|
host,
|
|
constants.SSHLongRunningScriptTimeout,
|
|
"shell/buildLoadTestDeps.sh",
|
|
scriptInputs{GoVersion: constants.BuildEnvGolangVersion},
|
|
)
|
|
}
|
|
|
|
func RunSSHRunLoadTest(host *models.Host, loadTestCommand, loadTestName string) error {
|
|
return RunOverSSH(
|
|
"Run Load Test",
|
|
host,
|
|
constants.SSHLongRunningScriptTimeout,
|
|
"shell/runLoadTest.sh",
|
|
scriptInputs{
|
|
GoVersion: constants.BuildEnvGolangVersion,
|
|
LoadTestCommand: loadTestCommand,
|
|
LoadTestResultFile: fmt.Sprintf("/home/ubuntu/.luxd/logs/loadtest_%s.txt", loadTestName),
|
|
},
|
|
)
|
|
}
|
|
|
|
// RunSSHCheckLuxdVersion checks node luxd version
|
|
func RunSSHCheckLuxdVersion(host *models.Host) ([]byte, error) {
|
|
// Craft and send the HTTP POST request
|
|
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\" :\"info.getNodeVersion\"}"
|
|
return PostOverSSH(host, "", requestBody)
|
|
}
|
|
|
|
// RunSSHCheckBootstrapped checks if node is bootstrapped to primary network
|
|
func RunSSHCheckBootstrapped(host *models.Host) ([]byte, error) {
|
|
// Craft and send the HTTP POST request
|
|
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\" :\"info.isBootstrapped\", \"params\": {\"chain\":\"P\"}}"
|
|
return PostOverSSH(host, "", requestBody)
|
|
}
|
|
|
|
// 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\",\"params\": {\"tags\": [\"P\"]}}"
|
|
return PostOverSSH(host, "/ext/health", requestBody)
|
|
}
|
|
|
|
// RunSSHGetNodeID reads nodeID from luxd
|
|
func RunSSHGetNodeID(host *models.Host) ([]byte, error) {
|
|
// Craft and send the HTTP POST request
|
|
requestBody := "{\"jsonrpc\":\"2.0\", \"id\":1,\"method\" :\"info.getNodeID\"}"
|
|
return PostOverSSH(host, "", requestBody)
|
|
}
|
|
|
|
// SubnetSyncStatus checks if node is synced to subnet
|
|
func RunSSHSubnetSyncStatus(host *models.Host, blockchainID string) ([]byte, error) {
|
|
// Craft and send the HTTP POST request
|
|
requestBody := fmt.Sprintf("{\"jsonrpc\":\"2.0\", \"id\":1,\"method\" :\"platform.getBlockchainStatus\", \"params\": {\"blockchainID\":\"%s\"}}", blockchainID)
|
|
return PostOverSSH(host, "/ext/bc/P", requestBody)
|
|
}
|
|
|
|
// StreamOverSSH runs provided script path over ssh.
|
|
// This script can be template as it will be rendered using scriptInputs vars
|
|
func StreamOverSSH(
|
|
scriptDesc string,
|
|
host *models.Host,
|
|
timeout time.Duration,
|
|
scriptPath string,
|
|
templateVars scriptInputs,
|
|
) error {
|
|
shellScript, err := script.ReadFile(scriptPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var script bytes.Buffer
|
|
t, err := template.New(scriptDesc).Parse(string(shellScript))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = t.Execute(&script, templateVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := host.StreamSSHCommand(script.String(), nil, timeout); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunSSHWhitelistPubKey downloads the authorized_keys file from the specified host, appends the provided sshPubKey to it, and uploads the file back to the host.
|
|
func RunSSHWhitelistPubKey(host *models.Host, sshPubKey string) error {
|
|
const sshAuthFile = "/home/ubuntu/.ssh/authorized_keys"
|
|
tmpName := filepath.Join(os.TempDir(), utils.RandomString(10))
|
|
defer os.Remove(tmpName)
|
|
if err := host.Download(sshAuthFile, tmpName, constants.SSHFileOpsTimeout); err != nil {
|
|
return err
|
|
}
|
|
// write ssh public key
|
|
tmpFile, err := os.OpenFile(tmpName, os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := tmpFile.WriteString(sshPubKey + "\n"); err != nil {
|
|
return err
|
|
}
|
|
if err := tmpFile.Close(); err != nil {
|
|
return err
|
|
}
|
|
return host.Upload(tmpFile.Name(), sshAuthFile, constants.SSHFileOpsTimeout)
|
|
}
|
|
|
|
// RunSSHDownloadFile downloads specified file from the specified host
|
|
func RunSSHDownloadFile(host *models.Host, filePath string, localFilePath string) error {
|
|
return host.Download(filePath, localFilePath, constants.SSHFileOpsTimeout)
|
|
}
|
|
|
|
func RunSSHUpsizeRootDisk(host *models.Host) error {
|
|
return RunOverSSH(
|
|
"Upsize Disk",
|
|
host,
|
|
constants.SSHScriptTimeout,
|
|
"shell/upsizeRootDisk.sh",
|
|
scriptInputs{},
|
|
)
|
|
}
|
|
|
|
// composeFileExists checks if the docker-compose file exists on the host
|
|
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 upgradeFileExists(host *models.Host) bool {
|
|
upgradeFileExists, _ := host.FileExists(filepath.Join(constants.CloudNodeConfigPath, constants.UpgradeFileName))
|
|
return upgradeFileExists
|
|
}
|
|
|
|
func nodeConfigFileExists(host *models.Host) bool {
|
|
nodeConfigFileExists, _ := host.FileExists(remoteconfig.GetRemoteLuxNodeConfig())
|
|
return nodeConfigFileExists
|
|
}
|
|
|
|
func aliasConfigFileExists(host *models.Host) bool {
|
|
aliasConfigFileExists, _ := host.FileExists(remoteconfig.GetRemoteLuxAliasesConfig())
|
|
return aliasConfigFileExists
|
|
}
|
|
|
|
func getLuxdConfigData(host *models.Host) (map[string]interface{}, error) {
|
|
// get remote node.json file
|
|
nodeJSONPath := filepath.Join(constants.CloudNodeConfigPath, constants.NodeConfigJSONFile)
|
|
// parse node.json file
|
|
nodeJSON, err := host.ReadFileBytes(nodeJSONPath, constants.SSHFileOpsTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var luxdConfig map[string]interface{}
|
|
if err := json.Unmarshal(nodeJSON, &luxdConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
return luxdConfig, nil
|
|
}
|
|
|
|
func getLuxdAliasData(host *models.Host) (map[string][]string, error) {
|
|
// parse aliases.json file
|
|
aliasesJSON, err := host.ReadFileBytes(remoteconfig.GetRemoteLuxAliasesConfig(), constants.SSHFileOpsTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var aliases map[string][]string
|
|
if err := json.Unmarshal(aliasesJSON, &aliases); err != nil {
|
|
return nil, err
|
|
}
|
|
return aliases, nil
|
|
}
|