mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
1445 lines
55 KiB
Go
1445 lines
55 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package nodecmd
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/dependencies"
|
|
|
|
"github.com/luxfi/cli/v2/cmd/flags"
|
|
"github.com/luxfi/cli/v2/pkg/ansible"
|
|
awsAPI "github.com/luxfi/cli/v2/pkg/cloud/aws"
|
|
"github.com/luxfi/cli/v2/pkg/cobrautils"
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/docker"
|
|
"github.com/luxfi/cli/v2/pkg/metrics"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/networkoptions"
|
|
"github.com/luxfi/cli/v2/pkg/node"
|
|
"github.com/luxfi/cli/v2/pkg/ssh"
|
|
"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"
|
|
"github.com/luxfi/node/v2/v2/staking"
|
|
"github.com/luxfi/node/v2/v2/utils/logging"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
const (
|
|
enableMonitoringFlag = "enable-monitoring"
|
|
)
|
|
|
|
var (
|
|
globalNetworkFlags networkoptions.NetworkFlags
|
|
useAWS bool
|
|
useGCP bool
|
|
cmdLineRegion []string
|
|
authorizeAccess bool
|
|
numValidatorsNodes []int
|
|
nodeType string
|
|
existingSeparateInstance string
|
|
existingMonitoringInstance string
|
|
useLatestLuxgoReleaseVersion bool
|
|
useLatestLuxgoPreReleaseVersion bool
|
|
useCustomLuxgoVersion string
|
|
useLuxgoVersionFromSubnet string
|
|
cmdLineGCPCredentialsPath string
|
|
cmdLineGCPProjectName string
|
|
cmdLineAlternativeKeyPairName string
|
|
addMonitoring bool
|
|
useSSHAgent bool
|
|
sshIdentity string
|
|
numAPINodes []int
|
|
throughput int
|
|
iops int
|
|
volumeType string
|
|
volumeSize int
|
|
grafanaPkg string
|
|
wizSubnet string
|
|
publicHTTPPortAccess bool
|
|
)
|
|
|
|
func newCreateCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "create [clusterName]",
|
|
Short: "(ALPHA Warning) Create a new validator on cloud server",
|
|
Long: `(ALPHA Warning) This command is currently in experimental mode.
|
|
|
|
The node create command sets up a validator on a cloud server of your choice.
|
|
The validator will be validating the Lux Primary Network and Subnet
|
|
of your choice. By default, the command runs an interactive wizard. It
|
|
walks you through all the steps you need to set up a validator.
|
|
Once this command is completed, you will have to wait for the validator
|
|
to finish bootstrapping on the primary network before running further
|
|
commands on it, e.g. validating a Subnet. You can check the bootstrapping
|
|
status by running lux node status
|
|
|
|
The created node will be part of group of validators called <clusterName>
|
|
and users can call node commands with <clusterName> so that the command
|
|
will apply to all nodes in the cluster`,
|
|
Args: cobrautils.ExactArgs(1),
|
|
RunE: createNodes,
|
|
PersistentPostRun: handlePostRun,
|
|
}
|
|
networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, networkoptions.NonLocalSupportedNetworkOptions)
|
|
cmd.Flags().BoolVar(&useStaticIP, "use-static-ip", true, "attach static Public IP on cloud servers")
|
|
cmd.Flags().BoolVar(&useAWS, "aws", false, "create node/s in AWS cloud")
|
|
cmd.Flags().BoolVar(&useGCP, "gcp", false, "create node/s in GCP cloud")
|
|
cmd.Flags().StringSliceVar(&cmdLineRegion, "region", []string{}, "create node(s) in given region(s). Use comma to separate multiple regions")
|
|
cmd.Flags().BoolVar(&authorizeAccess, "authorize-access", false, "authorize CLI to create cloud resources")
|
|
cmd.Flags().IntSliceVar(&numValidatorsNodes, "num-validators", []int{}, "number of nodes to create per region(s). Use comma to separate multiple numbers for each region in the same order as --region flag")
|
|
cmd.Flags().StringVar(&nodeType, "node-type", "", "cloud instance type. Use 'default' to use recommended default instance type")
|
|
cmd.Flags().BoolVar(&useLatestLuxgoReleaseVersion, "latest-luxd-version", false, "install latest luxd release version on node/s")
|
|
cmd.Flags().BoolVar(&useLatestLuxgoPreReleaseVersion, "latest-luxd-pre-release-version", false, "install latest luxd pre-release version on node/s")
|
|
cmd.Flags().StringVar(&useCustomLuxgoVersion, "custom-luxd-version", "", "install given luxd version on node/s")
|
|
cmd.Flags().StringVar(&useLuxgoVersionFromSubnet, "luxd-version-from-subnet", "", "install latest luxd version, that is compatible with the given subnet, on node/s")
|
|
cmd.Flags().StringVar(&cmdLineGCPCredentialsPath, "gcp-credentials", "", "use given GCP credentials")
|
|
cmd.Flags().StringVar(&cmdLineGCPProjectName, "gcp-project", "", "use given GCP project")
|
|
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(&useSSHAgent, "use-ssh-agent", false, "use ssh agent(ex: Yubikey) for ssh auth")
|
|
cmd.Flags().StringVar(&sshIdentity, "ssh-agent-identity", "", "use given ssh identity(only for ssh agent). If not set, default will be used")
|
|
cmd.Flags().BoolVar(&addMonitoring, enableMonitoringFlag, false, "set up Prometheus monitoring for created nodes. This option creates a separate monitoring cloud instance and incures additional cost")
|
|
cmd.Flags().StringVar(&grafanaPkg, "grafana-pkg", "", "use grafana pkg instead of apt repo(by default), for example https://dl.grafana.com/oss/release/grafana_10.4.1_amd64.deb")
|
|
cmd.Flags().IntSliceVar(&numAPINodes, "num-apis", []int{}, "number of API nodes(nodes without stake) to create in the new Devnet")
|
|
cmd.Flags().StringVar(&customGrafanaDashboardPath, "add-grafana-dashboard", "", "path to additional grafana dashboard json file")
|
|
cmd.Flags().IntVar(&iops, "aws-volume-iops", constants.AWSGP3DefaultIOPS, "AWS iops (for gp3, io1, and io2 volume types only)")
|
|
cmd.Flags().IntVar(&throughput, "aws-volume-throughput", constants.AWSGP3DefaultThroughput, "AWS throughput in MiB/s (for gp3 volume type only)")
|
|
cmd.Flags().StringVar(&volumeType, "aws-volume-type", "gp3", "AWS volume type")
|
|
cmd.Flags().IntVar(&volumeSize, "aws-volume-size", constants.CloudServerStorageSize, "AWS volume size in GB")
|
|
cmd.Flags().BoolVar(&replaceKeyPair, "auto-replace-keypair", false, "automatically replaces key pair to access node if previous key pair is not found")
|
|
cmd.Flags().BoolVar(&publicHTTPPortAccess, "public-http-port", false, "allow public access to luxd HTTP port")
|
|
cmd.Flags().StringArrayVar(&bootstrapIDs, "bootstrap-ids", []string{}, "nodeIDs of bootstrap nodes")
|
|
cmd.Flags().StringArrayVar(&bootstrapIPs, "bootstrap-ips", []string{}, "IP:port pairs of bootstrap nodes")
|
|
cmd.Flags().StringVar(&genesisPath, "genesis", "", "path to genesis file")
|
|
cmd.Flags().StringVar(&upgradePath, "upgrade", "", "path to upgrade file")
|
|
cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "primary network partial sync")
|
|
return cmd
|
|
}
|
|
|
|
// override postrun function from root.go, so that we don't double send metrics for the same command
|
|
func handlePostRun(_ *cobra.Command, _ []string) {}
|
|
|
|
func preCreateChecks(clusterName string, network models.Network) error {
|
|
if useCustomLuxgoVersion != "" || useLuxgoVersionFromSubnet != "" {
|
|
if useCustomLuxgoVersion != "" {
|
|
if err := dependencies.CheckVersionIsOverMin(app, constants.LuxdRepoName, network, useCustomLuxgoVersion); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
useLatestLuxgoReleaseVersion = false
|
|
useLatestLuxgoPreReleaseVersion = false
|
|
}
|
|
if !flags.EnsureMutuallyExclusive([]bool{useLatestLuxgoReleaseVersion, useLatestLuxgoPreReleaseVersion}) {
|
|
return fmt.Errorf("latest luxd released version, latest luxd pre-released version are mutually exclusive options")
|
|
}
|
|
if !flags.EnsureMutuallyExclusive([]bool{useLuxgoVersionFromSubnet != "", useCustomLuxgoVersion != ""}) {
|
|
return fmt.Errorf("custom luxd version and luxd version based on given subnet, are mutually exclusive options")
|
|
}
|
|
if useAWS && useGCP {
|
|
return fmt.Errorf("could not use both AWS and GCP cloud options")
|
|
}
|
|
if !useAWS && awsProfile != constants.AWSDefaultCredential {
|
|
return fmt.Errorf("could not use AWS profile for non AWS cloud option")
|
|
}
|
|
if len(utils.Unique(cmdLineRegion)) != len(numValidatorsNodes) {
|
|
return fmt.Errorf("regions provided is not consistent with number of nodes provided. Please make sure list of regions is unique")
|
|
}
|
|
|
|
if len(numValidatorsNodes) > 0 {
|
|
for _, num := range numValidatorsNodes {
|
|
if num <= 0 {
|
|
return fmt.Errorf("number of nodes per region must be greater than 0")
|
|
}
|
|
}
|
|
}
|
|
if sshIdentity != "" && !useSSHAgent {
|
|
return fmt.Errorf("could not use ssh identity without using ssh agent")
|
|
}
|
|
if useSSHAgent && !utils.IsSSHAgentAvailable() {
|
|
return fmt.Errorf("ssh agent is not available")
|
|
}
|
|
if len(numAPINodes) > 0 && !(globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet) {
|
|
return fmt.Errorf("API nodes can only be created in Devnet/Testnet(Testnet)")
|
|
}
|
|
if (globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet) && len(numAPINodes) > 0 && len(numAPINodes) != len(numValidatorsNodes) {
|
|
return fmt.Errorf("API nodes and Validator nodes must be deployed to same number of regions")
|
|
}
|
|
if len(numAPINodes) > 0 {
|
|
for _, num := range numValidatorsNodes {
|
|
if num <= 0 {
|
|
return fmt.Errorf("number of API nodes per region must be greater than 0")
|
|
}
|
|
}
|
|
}
|
|
if customGrafanaDashboardPath != "" && !utils.FileExists(utils.ExpandHome(customGrafanaDashboardPath)) {
|
|
return fmt.Errorf("custom grafana dashboard file does not exist")
|
|
}
|
|
|
|
if useAWS {
|
|
if stringToAWSVolumeType(volumeType) == "" {
|
|
return fmt.Errorf("invalid AWS volume type provided")
|
|
}
|
|
if volumeType != constants.AWSVolumeTypeGP3 && throughput != constants.AWSGP3DefaultThroughput {
|
|
return fmt.Errorf("AWS throughput setting is only applicable AWS gp3 volume type")
|
|
}
|
|
if volumeType != constants.AWSVolumeTypeGP3 && volumeType != constants.AWSVolumeTypeIO1 && volumeType != constants.AWSVolumeTypeIO2 && iops != constants.AWSGP3DefaultIOPS {
|
|
return fmt.Errorf("AWS iops setting is only applicable AWS gp3, io1, and io2 volume types")
|
|
}
|
|
}
|
|
if grafanaPkg != "" && (!strings.HasSuffix(grafanaPkg, ".deb") || !utils.IsValidURL(grafanaPkg)) {
|
|
return fmt.Errorf("grafana package must be URL to a .deb file")
|
|
}
|
|
if grafanaPkg != "" && !addMonitoring {
|
|
return fmt.Errorf("grafana package can only be used with monitoring setup")
|
|
}
|
|
// check external cluster
|
|
if err := failForExternal(clusterName); err != nil {
|
|
return err
|
|
}
|
|
// check for local
|
|
clusterConfig := models.ClusterConfig{}
|
|
if ok, err := app.ClusterExists(clusterName); err != nil {
|
|
return err
|
|
} else if ok {
|
|
clusterConfig, err = app.GetClusterConfig(clusterName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if clusterConfig.Local {
|
|
return notImplementedForLocal("create")
|
|
}
|
|
// bootstrap checks
|
|
if len(bootstrapIDs) != len(bootstrapIPs) {
|
|
return fmt.Errorf("number of bootstrap ids and ip:port pairs must be equal")
|
|
}
|
|
if genesisPath != "" && !utils.FileExists(genesisPath) {
|
|
return fmt.Errorf("genesis file %s does not exist", genesisPath)
|
|
}
|
|
if upgradePath != "" && !utils.FileExists(upgradePath) {
|
|
return fmt.Errorf("upgrade file %s does not exist", upgradePath)
|
|
}
|
|
// check ip:port pairs
|
|
for _, ipPortPair := range bootstrapIPs {
|
|
if ok := utils.IsValidIPPort(ipPortPair); !ok {
|
|
return fmt.Errorf("invalid ip:port pair %s", ipPortPair)
|
|
}
|
|
}
|
|
if globalNetworkFlags.UseDevnet {
|
|
partialSync = false
|
|
ux.Logger.PrintToUser("disabling partial sync default for devnet")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkClusterExternal(clusterName string) (bool, error) {
|
|
clusterExists, err := node.CheckClusterExists(app, clusterName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error checking cluster: %w", err)
|
|
}
|
|
if clusterExists {
|
|
clusterConf, err := app.GetClusterConfig(clusterName)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if clusterConf.External {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func stringToAWSVolumeType(input string) types.VolumeType {
|
|
switch input {
|
|
case "gp3":
|
|
return types.VolumeTypeGp3
|
|
case "io1":
|
|
return types.VolumeTypeIo1
|
|
case "io2":
|
|
return types.VolumeTypeIo2
|
|
case "gp2":
|
|
return types.VolumeTypeGp2
|
|
case "sc1":
|
|
return types.VolumeTypeSc1
|
|
case "st1":
|
|
return types.VolumeTypeSt1
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func setGlobalNetworkFlags(network models.Network) {
|
|
switch network.Kind {
|
|
case models.Testnet:
|
|
globalNetworkFlags.UseTestnet = true
|
|
case models.Devnet:
|
|
globalNetworkFlags.UseDevnet = true
|
|
case models.Local:
|
|
globalNetworkFlags.UseLocal = true
|
|
case models.Mainnet:
|
|
globalNetworkFlags.UseMainnet = true
|
|
}
|
|
}
|
|
|
|
func createNodes(cmd *cobra.Command, args []string) error {
|
|
clusterName := args[0]
|
|
network, err := networkoptions.GetNetworkFromCmdLineFlags(
|
|
app,
|
|
"",
|
|
globalNetworkFlags,
|
|
false,
|
|
true,
|
|
networkoptions.NonLocalSupportedNetworkOptions,
|
|
"",
|
|
)
|
|
setGlobalNetworkFlags(network)
|
|
if err := preCreateChecks(clusterName, network); err != nil {
|
|
return err
|
|
}
|
|
network = models.NewNetworkFromCluster(network, clusterName)
|
|
globalNetworkFlags.UseDevnet = network.Kind == models.Devnet // set globalNetworkFlags.UseDevnet to true if network is devnet for further use
|
|
luxdVersionSetting := dependencies.LuxdVersionSettings{
|
|
UseLuxgoVersionFromSubnet: useLuxgoVersionFromSubnet,
|
|
UseLatestLuxgoReleaseVersion: useLatestLuxgoReleaseVersion,
|
|
UseLatestLuxgoPreReleaseVersion: useLatestLuxgoPreReleaseVersion,
|
|
UseCustomLuxgoVersion: useCustomLuxgoVersion,
|
|
}
|
|
luxdVersion, err := dependencies.GetLuxdVersion(app, luxdVersionSetting, network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cloudService, err := setCloudService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nodeType, err = setCloudInstanceType(cloudService)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cloudService != constants.GCPCloudService && cmdLineGCPCredentialsPath != "" {
|
|
return fmt.Errorf("set to use GCP credentials but cloud option is not GCP")
|
|
}
|
|
if cloudService != constants.GCPCloudService && cmdLineGCPProjectName != "" {
|
|
return fmt.Errorf("set to use GCP project but cloud option is not GCP")
|
|
}
|
|
// for devnet add nonstake api nodes for each region with stake
|
|
cloudConfigMap := models.CloudConfig{}
|
|
publicIPMap := map[string]string{}
|
|
apiNodeIPMap := map[string]string{}
|
|
numNodesMetricsMap := map[string]NumNodes{}
|
|
gcpProjectName := ""
|
|
gcpCredentialFilepath := ""
|
|
// set ssh-Key
|
|
if useSSHAgent && sshIdentity == "" {
|
|
sshIdentity, err = setSSHIdentity()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
monitoringHostRegion := ""
|
|
monitoringNodeConfig := models.RegionConfig{}
|
|
existingMonitoringInstance, err = getExistingMonitoringInstance(clusterName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingMonitoringInstance == "" && !cmd.Flags().Changed(enableMonitoringFlag) {
|
|
if addMonitoring, err = promptSetUpMonitoring(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if utils.IsE2E() {
|
|
usr, err := user.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// override cloudConfig for E2E testing
|
|
defaultLuxCLIPrefix := usr.Username + constants.LuxCLISuffix
|
|
keyPairName := fmt.Sprintf("%s-keypair", defaultLuxCLIPrefix)
|
|
certPath, err := app.GetSSHCertFilePath(keyPairName)
|
|
if globalNetworkFlags.UseDevnet {
|
|
for i, num := range numAPINodes {
|
|
numValidatorsNodes[i] += num
|
|
}
|
|
}
|
|
dockerNumNodes := utils.Sum(numValidatorsNodes)
|
|
var dockerNodesPublicIPs []string
|
|
var monitoringHostIP string
|
|
if addMonitoring {
|
|
generatedPublicIPs := utils.GenerateDockerHostIPs(dockerNumNodes + 1)
|
|
monitoringHostIP = generatedPublicIPs[len(generatedPublicIPs)-1]
|
|
dockerNodesPublicIPs = generatedPublicIPs[:len(generatedPublicIPs)-1]
|
|
} else {
|
|
dockerNodesPublicIPs = utils.GenerateDockerHostIPs(dockerNumNodes)
|
|
}
|
|
dockerHostIDs := utils.GenerateDockerHostIDs(dockerNumNodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cloudConfigMap = models.CloudConfig{
|
|
"docker": {
|
|
InstanceIDs: dockerHostIDs,
|
|
PublicIPs: dockerNodesPublicIPs,
|
|
KeyPair: keyPairName,
|
|
SecurityGroup: "docker",
|
|
CertFilePath: certPath,
|
|
ImageID: "docker",
|
|
Prefix: "docker",
|
|
CertName: "docker",
|
|
SecurityGroupName: "docker",
|
|
NumNodes: dockerNumNodes,
|
|
InstanceType: "docker",
|
|
},
|
|
}
|
|
currentRegionConfig := cloudConfigMap["docker"]
|
|
for i, ip := range currentRegionConfig.PublicIPs {
|
|
publicIPMap[dockerHostIDs[i]] = ip
|
|
}
|
|
apiNodeIDs := []string{}
|
|
if len(numAPINodes) > 0 {
|
|
_, apiNodeIDs = utils.SplitSliceAt(currentRegionConfig.InstanceIDs, len(currentRegionConfig.InstanceIDs)-numAPINodes[0])
|
|
}
|
|
currentRegionConfig.APIInstanceIDs = apiNodeIDs
|
|
for _, node := range currentRegionConfig.APIInstanceIDs {
|
|
apiNodeIPMap[node] = publicIPMap[node]
|
|
}
|
|
cloudConfigMap["docker"] = currentRegionConfig
|
|
if addMonitoring {
|
|
monitoringDockerHostID := utils.GenerateDockerHostIDs(1)
|
|
dockerHostIDs = append(dockerHostIDs, monitoringDockerHostID[0])
|
|
monitoringCloudConfig := models.CloudConfig{
|
|
"monitoringDocker": {
|
|
InstanceIDs: monitoringDockerHostID,
|
|
PublicIPs: []string{monitoringHostIP},
|
|
KeyPair: keyPairName,
|
|
SecurityGroup: "docker",
|
|
CertFilePath: certPath,
|
|
ImageID: "docker",
|
|
Prefix: "docker",
|
|
CertName: "docker",
|
|
SecurityGroupName: "docker",
|
|
NumNodes: 1,
|
|
InstanceType: "docker",
|
|
},
|
|
}
|
|
monitoringNodeConfig = monitoringCloudConfig["monitoringDocker"]
|
|
}
|
|
pubKeyString, err := os.ReadFile(fmt.Sprintf("%s.pub", certPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dockerComposeFile, err := utils.SaveDockerComposeFile(constants.E2EDockerComposeFile, len(dockerHostIDs), "focal", strings.TrimSuffix(string(pubKeyString), "\n"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := utils.StartDockerCompose(dockerComposeFile); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if cloudService == constants.AWSCloudService {
|
|
// Get AWS Credential, region and AMI
|
|
if !(authorizeAccess || node.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) {
|
|
return fmt.Errorf("cloud access is required")
|
|
}
|
|
ec2SvcMap, ami, numNodesMap, err := getAWSCloudConfig(awsProfile, false, nil, nodeType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
numNodesMetricsMap = numNodesMap
|
|
regions := maps.Keys(ec2SvcMap)
|
|
if existingMonitoringInstance == "" {
|
|
monitoringHostRegion = regions[0]
|
|
}
|
|
cloudConfigMap, err = createAWSInstances(ec2SvcMap, nodeType, numNodesMap, regions, ami, false, publicHTTPPortAccess)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
monitoringEc2SvcMap := make(map[string]*awsAPI.AwsCloud)
|
|
if addMonitoring && existingMonitoringInstance == "" {
|
|
monitoringEc2SvcMap[monitoringHostRegion] = ec2SvcMap[monitoringHostRegion]
|
|
monitoringCloudConfig, err := createAWSInstances(monitoringEc2SvcMap, nodeType, map[string]NumNodes{monitoringHostRegion: {1, 0}}, []string{monitoringHostRegion}, ami, true, publicHTTPPortAccess)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
monitoringNodeConfig = monitoringCloudConfig[regions[0]]
|
|
}
|
|
if existingMonitoringInstance != "" {
|
|
addMonitoring = true
|
|
monitoringNodeConfig, monitoringHostRegion, err = getNodeCloudConfig(existingMonitoringInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
monitoringEc2SvcMap, err = getAWSMonitoringEC2Svc(awsProfile, monitoringHostRegion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !useStaticIP && addMonitoring {
|
|
monitoringPublicIPMap, err := monitoringEc2SvcMap[monitoringHostRegion].GetInstancePublicIPs(monitoringNodeConfig.InstanceIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
monitoringNodeConfig.PublicIPs = []string{monitoringPublicIPMap[monitoringNodeConfig.InstanceIDs[0]]}
|
|
}
|
|
for region, numNodes := range numNodesMap {
|
|
currentRegionConfig := cloudConfigMap[region]
|
|
if !useStaticIP {
|
|
tmpIPMap, err := ec2SvcMap[region].GetInstancePublicIPs(currentRegionConfig.InstanceIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for node, ip := range tmpIPMap {
|
|
publicIPMap[node] = ip
|
|
}
|
|
} else {
|
|
for i, node := range currentRegionConfig.InstanceIDs {
|
|
publicIPMap[node] = currentRegionConfig.PublicIPs[i]
|
|
}
|
|
}
|
|
// split publicIPMap to between stake and non-stake(api) nodes
|
|
_, apiNodeIDs := utils.SplitSliceAt(currentRegionConfig.InstanceIDs, len(currentRegionConfig.InstanceIDs)-numNodes.numAPI)
|
|
currentRegionConfig.APIInstanceIDs = apiNodeIDs
|
|
for _, node := range currentRegionConfig.APIInstanceIDs {
|
|
apiNodeIPMap[node] = publicIPMap[node]
|
|
}
|
|
cloudConfigMap[region] = currentRegionConfig
|
|
if addMonitoring {
|
|
if err = AddMonitoringSecurityGroupRule(ec2SvcMap, monitoringNodeConfig.PublicIPs[0], currentRegionConfig.SecurityGroup, region); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if !(authorizeAccess || node.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) {
|
|
return fmt.Errorf("cloud access is required")
|
|
}
|
|
// Get GCP Credential, zone, Image ID, service account key file path, and GCP project name
|
|
gcpClient, numNodesMap, imageID, credentialFilepath, projectName, err := getGCPConfig(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
numNodesMetricsMap = numNodesMap
|
|
if existingMonitoringInstance == "" {
|
|
monitoringHostRegion = maps.Keys(numNodesMap)[0]
|
|
}
|
|
cloudConfigMap, err = createGCPInstance(gcpClient, nodeType, numNodesMap, imageID, clusterName, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if addMonitoring && existingMonitoringInstance == "" {
|
|
monitoringCloudConfig, err := createGCPInstance(gcpClient, nodeType, map[string]NumNodes{monitoringHostRegion: {1, 0}}, imageID, clusterName, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
monitoringNodeConfig = monitoringCloudConfig[monitoringHostRegion]
|
|
}
|
|
if existingMonitoringInstance != "" {
|
|
addMonitoring = true
|
|
monitoringNodeConfig, monitoringHostRegion, err = getNodeCloudConfig(existingMonitoringInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !useStaticIP && addMonitoring {
|
|
monitoringPublicIPMap, err := gcpClient.GetInstancePublicIPs(monitoringHostRegion, monitoringNodeConfig.InstanceIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
monitoringNodeConfig.PublicIPs = []string{monitoringPublicIPMap[monitoringNodeConfig.InstanceIDs[0]]}
|
|
}
|
|
for zone, numNodes := range numNodesMap {
|
|
currentRegionConfig := cloudConfigMap[zone]
|
|
if !useStaticIP {
|
|
tmpIPMap, err := gcpClient.GetInstancePublicIPs(zone, currentRegionConfig.InstanceIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for node, ip := range tmpIPMap {
|
|
publicIPMap[node] = ip
|
|
}
|
|
} else {
|
|
for i, node := range currentRegionConfig.InstanceIDs {
|
|
publicIPMap[node] = currentRegionConfig.PublicIPs[i]
|
|
}
|
|
}
|
|
// split publicIPMap to between stake and non-stake(api) nodes
|
|
_, apiNodeIDs := utils.SplitSliceAt(currentRegionConfig.InstanceIDs, len(currentRegionConfig.InstanceIDs)-numNodes.numAPI)
|
|
currentRegionConfig.APIInstanceIDs = apiNodeIDs
|
|
for _, node := range currentRegionConfig.APIInstanceIDs {
|
|
apiNodeIPMap[node] = publicIPMap[node]
|
|
}
|
|
cloudConfigMap[zone] = currentRegionConfig
|
|
if addMonitoring {
|
|
prefix, err := defaultLuxCLIPrefix("")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
networkName := fmt.Sprintf("%s-network", prefix)
|
|
firewallName := fmt.Sprintf("%s-%s-monitoring", networkName, strings.ReplaceAll(monitoringNodeConfig.PublicIPs[0], ".", ""))
|
|
ports := []string{
|
|
strconv.Itoa(constants.LuxdMachineMetricsPort), strconv.Itoa(constants.LuxdAPIPort),
|
|
strconv.Itoa(constants.LuxdMonitoringPort), strconv.Itoa(constants.LuxdGrafanaPort),
|
|
strconv.Itoa(constants.LuxdLokiPort),
|
|
}
|
|
if err = gcpClient.AddFirewall(
|
|
monitoringNodeConfig.PublicIPs[0],
|
|
networkName,
|
|
projectName,
|
|
firewallName,
|
|
ports,
|
|
true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
gcpProjectName = projectName
|
|
gcpCredentialFilepath = credentialFilepath
|
|
}
|
|
}
|
|
|
|
if err = CreateClusterNodeConfig(
|
|
network,
|
|
cloudConfigMap,
|
|
monitoringNodeConfig,
|
|
monitoringHostRegion,
|
|
clusterName,
|
|
cloudService,
|
|
addMonitoring,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if cloudService == constants.GCPCloudService {
|
|
if err = updateClustersConfigGCPKeyFilepath(gcpProjectName, gcpCredentialFilepath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
inventoryPath := app.GetAnsibleInventoryDirPath(clusterName)
|
|
if err = ansible.CreateAnsibleHostInventory(inventoryPath, "", cloudService, publicIPMap, cloudConfigMap); err != nil {
|
|
return err
|
|
}
|
|
monitoringInventoryPath := ""
|
|
var monitoringHosts []*models.Host
|
|
if addMonitoring {
|
|
monitoringInventoryPath = app.GetMonitoringInventoryDir(clusterName)
|
|
if existingMonitoringInstance == "" {
|
|
if err = ansible.CreateAnsibleHostInventory(monitoringInventoryPath, monitoringNodeConfig.CertFilePath, cloudService, map[string]string{monitoringNodeConfig.InstanceIDs[0]: monitoringNodeConfig.PublicIPs[0]}, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
monitoringHosts, err = ansible.GetInventoryFromAnsibleInventoryFile(monitoringInventoryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(inventoryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hosts := utils.Filter(allHosts, func(h *models.Host) bool { return slices.Contains(cloudConfigMap.GetAllInstanceIDs(), h.GetCloudID()) })
|
|
// waiting for all nodes to become accessible
|
|
checkHosts := hosts
|
|
if addMonitoring && len(monitoringHosts) > 0 {
|
|
checkHosts = append(checkHosts, monitoringHosts[0])
|
|
}
|
|
failedHosts := waitForHosts(checkHosts)
|
|
if failedHosts.Len() > 0 {
|
|
for _, result := range failedHosts.GetResults() {
|
|
ux.Logger.PrintToUser("Instance %s failed to provision with error %s. Please check instance logs for more information", result.NodeID, result.Err)
|
|
}
|
|
return fmt.Errorf("failed to provision node(s) %s", failedHosts.GetNodeList())
|
|
}
|
|
wg := sync.WaitGroup{}
|
|
wgResults := models.NodeResults{}
|
|
spinSession := ux.NewUserSpinner()
|
|
// setup monitoring in parallel with node setup
|
|
luxdPorts, machinePorts, ltPorts, err := getPrometheusTargets(clusterName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
startTime := time.Now()
|
|
if addMonitoring {
|
|
spinSession := ux.NewUserSpinner()
|
|
if len(monitoringHosts) != 1 {
|
|
return fmt.Errorf("expected only one monitoring host, found %d", len(monitoringHosts))
|
|
}
|
|
monitoringHost := monitoringHosts[0]
|
|
if existingMonitoringInstance == "" {
|
|
// setup new monitoring host
|
|
wg.Add(1)
|
|
go func(nodeResults *models.NodeResults, monitoringHost *models.Host) {
|
|
defer wg.Done()
|
|
if err := monitoringHost.Connect(0); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
return
|
|
}
|
|
spinner := spinSession.SpinToUser(utils.ScriptLog(monitoringHost.IP, "Setup Monitoring"))
|
|
if err = app.SetupMonitoringEnv(); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
if err = ssh.RunSSHSetupDockerService(monitoringHost); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.Logger.Info("SetupMonitoringEnv RunSSHSetupDockerService completed")
|
|
if err = ssh.RunSSHSetupMonitoringFolders(monitoringHost); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.Logger.Info("RunSSHSetupMonitoringFolders completed")
|
|
if err := ssh.RunSSHCopyMonitoringDashboards(monitoringHost, app.GetMonitoringDashboardDir()+"/"); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.Logger.Info("RunSSHCopyMonitoringDashboards completed")
|
|
if err := ssh.RunSSHSetupPrometheusConfig(monitoringHost, luxdPorts, machinePorts, ltPorts); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.Logger.Info("RunSSHSetupPrometheusConfig completed")
|
|
if err := ssh.RunSSHSetupLokiConfig(monitoringHost, constants.LuxdLokiPort); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.Logger.Info("RunSSHSetupLokiConfig completed")
|
|
if err := docker.ComposeSSHSetupMonitoring(monitoringHost); err != nil {
|
|
nodeResults.AddResult(monitoringHost.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.Logger.Info("ComposeSSHSetupMonitoring completed")
|
|
ux.SpinComplete(spinner)
|
|
}(&wgResults, monitoringHost)
|
|
}
|
|
wg.Wait()
|
|
spinSession.Stop()
|
|
}
|
|
for _, host := range hosts {
|
|
publicAccessToHTTPPort := slices.Contains(cloudConfigMap.GetAllAPIInstanceIDs(), host.GetCloudID()) || publicHTTPPortAccess
|
|
host.APINode = publicAccessToHTTPPort
|
|
}
|
|
if err = setup(hosts, luxdVersion, network); err != nil {
|
|
return err
|
|
}
|
|
if addMonitoring {
|
|
spinSession := ux.NewUserSpinner()
|
|
for _, host := range hosts {
|
|
wg.Add(1)
|
|
go func(nodeResults *models.NodeResults, host *models.Host) {
|
|
defer wg.Done()
|
|
spinner := spinSession.SpinToUser(utils.ScriptLog(host.IP, "Add Monitoring"))
|
|
if addMonitoring {
|
|
cloudID := host.GetCloudID()
|
|
nodeID, err := getNodeID(app.GetNodeInstanceDirPath(cloudID))
|
|
if err != nil {
|
|
nodeResults.AddResult(host.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
if err = ssh.RunSSHSetupPromtailConfig(host, monitoringNodeConfig.PublicIPs[0], constants.LuxdLokiPort, cloudID, nodeID.String(), ""); err != nil {
|
|
nodeResults.AddResult(host.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.SpinComplete(spinner)
|
|
}
|
|
}(&wgResults, host)
|
|
}
|
|
wg.Wait()
|
|
spinSession.Stop()
|
|
}
|
|
ux.Logger.Info("Create and setup nodes time took: %s", time.Since(startTime))
|
|
spinSession.Stop()
|
|
if network.Kind == models.Devnet {
|
|
if err := setupDevnet(clusterName, hosts, apiNodeIPMap); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, node := range hosts {
|
|
if wgResults.HasIDWithError(node.IP) {
|
|
ux.Logger.RedXToUser("Node %s is ERROR with error: %s", node.IP, wgResults.GetErrorHostMap()[node.IP])
|
|
}
|
|
}
|
|
|
|
if wgResults.HasErrors() {
|
|
return fmt.Errorf("failed to deploy node(s) %s", wgResults.GetErrorHostMap())
|
|
} else {
|
|
monitoringPublicIP := ""
|
|
if addMonitoring {
|
|
monitoringPublicIP = monitoringNodeConfig.PublicIPs[0]
|
|
}
|
|
printResults(cloudConfigMap, publicIPMap, monitoringPublicIP)
|
|
ux.Logger.PrintToUser(logging.Green.Wrap("Luxd and Lux-CLI installed and node(s) are bootstrapping!"))
|
|
}
|
|
sendNodeCreateMetrics(cloudService, network.Name(), numNodesMetricsMap)
|
|
return nil
|
|
}
|
|
|
|
func promptSetUpMonitoring() (bool, error) {
|
|
monitoringInstance, err := app.Prompt.CaptureYesNo("Do you want to set up monitoring? (This requires additional cloud instance and may incur additional cost)")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return monitoringInstance, nil
|
|
}
|
|
|
|
// CreateClusterNodeConfig creates node config and save it in .lux-cli/nodes/{instanceID}
|
|
// also creates cluster config in .lux-cli/nodes storing various key pair and security group info for all clusters
|
|
func CreateClusterNodeConfig(
|
|
network models.Network,
|
|
cloudConfigMap models.CloudConfig,
|
|
monitorCloudConfig models.RegionConfig,
|
|
monitoringHostRegion,
|
|
clusterName,
|
|
cloudService string,
|
|
addMonitoring bool,
|
|
) error {
|
|
for region, cloudConfig := range cloudConfigMap {
|
|
for i := range cloudConfig.InstanceIDs {
|
|
publicIP := ""
|
|
if len(cloudConfig.PublicIPs) > 0 {
|
|
publicIP = cloudConfig.PublicIPs[i]
|
|
}
|
|
nodeConfig := models.NodeConfig{
|
|
NodeID: cloudConfig.InstanceIDs[i],
|
|
Region: region,
|
|
AMI: cloudConfig.ImageID,
|
|
KeyPair: cloudConfig.KeyPair,
|
|
CertPath: cloudConfig.CertFilePath,
|
|
SecurityGroup: cloudConfig.SecurityGroup,
|
|
ElasticIP: publicIP,
|
|
CloudService: cloudService,
|
|
UseStaticIP: useStaticIP,
|
|
IsMonitor: false,
|
|
}
|
|
if err := app.CreateNodeCloudConfigFile(cloudConfig.InstanceIDs[i], &nodeConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := addNodeToClustersConfig(network, cloudConfig.InstanceIDs[i], clusterName, slices.Contains(cloudConfig.APIInstanceIDs, cloudConfig.InstanceIDs[i]), false, "", ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if addMonitoring {
|
|
if err := saveExternalHostConfig(monitorCloudConfig, monitoringHostRegion, cloudService, clusterName, constants.MonitorRole, ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// saveExternalHostConfig saves externally created instance (monitoring or load test instance)
|
|
// into existing cluster_config.json and creates new node_config.json file for the instance
|
|
// load test instances are given name of loadTestName in argument
|
|
func saveExternalHostConfig(externalHostConfig models.RegionConfig, hostRegion, cloudService, clusterName, externalHostRole, loadTestName string) error {
|
|
isLoadTest := externalHostRole == constants.LoadTestRole
|
|
isMonitoring := externalHostRole == constants.MonitorRole
|
|
nodeConfig := models.NodeConfig{
|
|
NodeID: externalHostConfig.InstanceIDs[0],
|
|
Region: hostRegion,
|
|
AMI: externalHostConfig.ImageID,
|
|
KeyPair: externalHostConfig.KeyPair,
|
|
CertPath: externalHostConfig.CertFilePath,
|
|
SecurityGroup: externalHostConfig.SecurityGroup,
|
|
ElasticIP: externalHostConfig.PublicIPs[0],
|
|
CloudService: cloudService,
|
|
UseStaticIP: useStaticIP,
|
|
IsMonitor: isMonitoring,
|
|
IsLoadTest: isLoadTest,
|
|
}
|
|
if err := app.CreateNodeCloudConfigFile(externalHostConfig.InstanceIDs[0], &nodeConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := addNodeToClustersConfig(models.UndefinedNetwork, externalHostConfig.InstanceIDs[0], clusterName, false, true, externalHostRole, loadTestName); err != nil {
|
|
return err
|
|
}
|
|
return updateKeyPairClustersConfig(nodeConfig)
|
|
}
|
|
|
|
func getExistingMonitoringInstance(clusterName string) (string, error) {
|
|
// check for local
|
|
clusterConfig := models.ClusterConfig{}
|
|
if ok, err := app.ClusterExists(clusterName); err != nil {
|
|
return "", err
|
|
} else if ok {
|
|
clusterConfig, err = app.GetClusterConfig(clusterName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if clusterConfig.MonitoringInstance != "" {
|
|
return clusterConfig.MonitoringInstance, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func updateKeyPairClustersConfig(cloudConfig models.NodeConfig) error {
|
|
clustersConfig, err := app.GetClustersConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if clustersConfig.KeyPair == nil {
|
|
clustersConfig.KeyPair = make(map[string]string)
|
|
}
|
|
if _, ok := clustersConfig.KeyPair[cloudConfig.KeyPair]; !ok {
|
|
clustersConfig.KeyPair[cloudConfig.KeyPair] = cloudConfig.CertPath
|
|
}
|
|
return app.WriteClustersConfigFile(&clustersConfig)
|
|
}
|
|
|
|
func getNodeCloudConfig(node string) (models.RegionConfig, string, error) {
|
|
config, err := app.LoadClusterNodeConfig(node)
|
|
if err != nil {
|
|
return models.RegionConfig{}, "", err
|
|
}
|
|
elasticIP := []string{}
|
|
if config.ElasticIP != "" {
|
|
elasticIP = append(elasticIP, config.ElasticIP)
|
|
}
|
|
instanceIDs := []string{}
|
|
instanceIDs = append(instanceIDs, config.NodeID)
|
|
return models.RegionConfig{
|
|
InstanceIDs: instanceIDs,
|
|
PublicIPs: elasticIP,
|
|
KeyPair: config.KeyPair,
|
|
SecurityGroupName: config.SecurityGroup,
|
|
CertFilePath: config.CertPath,
|
|
ImageID: config.AMI,
|
|
}, config.Region, nil
|
|
}
|
|
|
|
func addNodeToClustersConfig(network models.Network, nodeID, clusterName string, isAPIInstance bool, isExternalHost bool, nodeRole, loadTestName string) error {
|
|
clustersConfig, err := app.GetClustersConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if clustersConfig.Clusters == nil {
|
|
clustersConfig.Clusters = make(map[string]models.ClusterConfig)
|
|
}
|
|
clusterConfig := clustersConfig.Clusters[clusterName]
|
|
// if supplied network in argument is empty, don't change current cluster network in cluster_config.json
|
|
if !network.IsUndefined() {
|
|
clusterConfig.Network = network
|
|
}
|
|
clusterConfig.HTTPAccess = constants.HTTPAccess(publicHTTPPortAccess)
|
|
if clusterConfig.LoadTestInstance == nil {
|
|
clusterConfig.LoadTestInstance = make(map[string]string)
|
|
}
|
|
if isExternalHost {
|
|
switch nodeRole {
|
|
case constants.MonitorRole:
|
|
clusterConfig.MonitoringInstance = nodeID
|
|
case constants.LoadTestRole:
|
|
clusterConfig.LoadTestInstance[loadTestName] = nodeID
|
|
}
|
|
} else {
|
|
clusterConfig.Nodes = append(clusterConfig.Nodes, nodeID)
|
|
}
|
|
if isAPIInstance {
|
|
clusterConfig.APINodes = append(clusterConfig.APINodes, nodeID)
|
|
}
|
|
clustersConfig.Clusters[clusterName] = clusterConfig
|
|
return app.WriteClustersConfigFile(&clustersConfig)
|
|
}
|
|
|
|
func getNodeID(nodeDir string) (ids.NodeID, error) {
|
|
certBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.StakerCertFileName))
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
nodeID, err := utils.ToNodeID(certBytes)
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
return nodeID, nil
|
|
}
|
|
|
|
func generateNodeCertAndKeys(stakerCertFilePath, stakerKeyFilePath, blsKeyFilePath string) (ids.NodeID, error) {
|
|
certBytes, keyBytes, err := staking.NewCertAndKeyBytes()
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
nodeID, err := utils.ToNodeID(certBytes)
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(stakerCertFilePath), constants.DefaultPerms755); err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
if err := os.WriteFile(stakerCertFilePath, certBytes, constants.WriteReadUserOnlyPerms); err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(stakerKeyFilePath), constants.DefaultPerms755); err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
if err := os.WriteFile(stakerKeyFilePath, keyBytes, constants.WriteReadUserOnlyPerms); err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
blsSignerKeyBytes, err := utils.NewBlsSecretKeyBytes()
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(blsKeyFilePath), constants.DefaultPerms755); err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
if err := os.WriteFile(blsKeyFilePath, blsSignerKeyBytes, constants.WriteReadUserOnlyPerms); err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
return nodeID, nil
|
|
}
|
|
|
|
func provideStakingCertAndKey(host *models.Host) error {
|
|
keyPath := app.GetNodeStakingDir(host.IP)
|
|
if sdkutils.DirExists(keyPath) && !overrideExisting {
|
|
yes, err := app.Prompt.CaptureNoYes(fmt.Sprintf("Directory %s alreday exists. Do you want to override it?", keyPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !yes {
|
|
return nil
|
|
}
|
|
}
|
|
nodeID, err := generateNodeCertAndKeys(
|
|
filepath.Join(keyPath, constants.StakerCertFileName),
|
|
filepath.Join(keyPath, constants.StakerKeyFileName),
|
|
filepath.Join(keyPath, constants.BLSKeyFileName),
|
|
)
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("Failed to generate staking keys for host %s", host.IP)
|
|
return err
|
|
} else {
|
|
ux.Logger.GreenCheckmarkToUser("Generated staking keys for host %s[%s] ", host.IP, nodeID.String())
|
|
}
|
|
instanceID := host.GetCloudID()
|
|
if instanceID != "" {
|
|
if err := utils.FileCopy(filepath.Join(keyPath, constants.StakerCertFileName), filepath.Join(app.GetNodesDir(), instanceID, constants.StakerCertFileName)); err != nil {
|
|
return err
|
|
}
|
|
if err := utils.FileCopy(filepath.Join(keyPath, constants.StakerKeyFileName), filepath.Join(app.GetNodesDir(), instanceID, constants.StakerKeyFileName)); err != nil {
|
|
return err
|
|
}
|
|
if err := utils.FileCopy(filepath.Join(keyPath, constants.BLSKeyFileName), filepath.Join(app.GetNodesDir(), instanceID, constants.BLSKeyFileName)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ssh.RunSSHUploadStakingFiles(host, keyPath)
|
|
}
|
|
|
|
func setCloudService() (string, error) {
|
|
if utils.IsE2E() {
|
|
if !utils.E2EDocker() {
|
|
return "", fmt.Errorf("E2E is required but docker-compose is not available")
|
|
}
|
|
return constants.E2EDocker, nil
|
|
}
|
|
if useAWS {
|
|
return constants.AWSCloudService, nil
|
|
}
|
|
if useGCP {
|
|
return constants.GCPCloudService, nil
|
|
}
|
|
txt := "Which cloud service would you like to launch your Lux Node(s) in?"
|
|
cloudOptions := []string{constants.AWSCloudService, constants.GCPCloudService}
|
|
chosenCloudService, err := app.Prompt.CaptureList(txt, cloudOptions)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return chosenCloudService, nil
|
|
}
|
|
|
|
func setCloudInstanceType(cloudService string) (string, error) {
|
|
if utils.IsE2E() && utils.E2EDocker() {
|
|
return constants.E2EDocker, nil
|
|
}
|
|
switch { // backwards compatibility
|
|
case nodeType == constants.DefaultNodeType && cloudService == constants.AWSCloudService:
|
|
nodeType = constants.AWSDefaultInstanceType
|
|
return nodeType, nil
|
|
case nodeType == constants.DefaultNodeType && cloudService == constants.GCPCloudService:
|
|
nodeType = constants.GCPDefaultInstanceType
|
|
return nodeType, nil
|
|
}
|
|
defaultNodeType := ""
|
|
nodeTypeOption2 := ""
|
|
nodeTypeOption3 := ""
|
|
customNodeType := "Choose custom instance type"
|
|
switch {
|
|
case cloudService == constants.AWSCloudService:
|
|
defaultNodeType = constants.AWSDefaultInstanceType
|
|
nodeTypeOption2 = "t3a.2xlarge" // burst
|
|
nodeTypeOption3 = "c5n.2xlarge"
|
|
case cloudService == constants.GCPCloudService:
|
|
defaultNodeType = constants.GCPDefaultInstanceType
|
|
nodeTypeOption2 = "c3-highcpu-8"
|
|
nodeTypeOption3 = "n2-standard-8"
|
|
}
|
|
if nodeType == "" {
|
|
defaultStr := "[default] (recommended)"
|
|
nodeTypeStr, err := app.Prompt.CaptureList(
|
|
"Instance type to use",
|
|
[]string{fmt.Sprintf("%s %s", defaultNodeType, defaultStr), nodeTypeOption2, nodeTypeOption3, customNodeType},
|
|
)
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("Failed to capture node type with error: %s", err.Error())
|
|
return "", err
|
|
}
|
|
nodeTypeStr = strings.ReplaceAll(nodeTypeStr, defaultStr, "") // remove (default) if any
|
|
if nodeTypeStr == customNodeType {
|
|
nodeTypeStr, err = app.Prompt.CaptureString("What instance type would you like to use? Please refer to https://docs.lux.network/nodes/run/node-manually#hardware-and-os-requirements for minimum hardware requirements")
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("Failed to capture custom node type with error: %s", err.Error())
|
|
return "", err
|
|
}
|
|
}
|
|
return strings.Trim(nodeTypeStr, " "), nil
|
|
}
|
|
return nodeType, nil
|
|
}
|
|
|
|
func printResults(cloudConfigMap models.CloudConfig, publicIPMap map[string]string, monitoringHostIP string) {
|
|
ux.Logger.PrintToUser(" ")
|
|
ux.Logger.PrintLineSeparator()
|
|
ux.Logger.PrintToUser("LUX NODE(S) SUCCESSFULLY SET UP!")
|
|
ux.Logger.PrintLineSeparator()
|
|
ux.Logger.PrintToUser("Please wait until the node(s) are successfully bootstrapped to run further commands on the node(s)")
|
|
ux.Logger.PrintToUser("You can check status of the node(s) using %s command", logging.LightBlue.Wrap("lux node status"))
|
|
ux.Logger.PrintToUser("Please use %s to ssh into the node(s). More details: %s", logging.LightBlue.Wrap("lux node ssh"), "https://docs.lux.network/tooling/cli-create-nodes/node-ssh")
|
|
|
|
for region, cloudConfig := range cloudConfigMap {
|
|
ux.Logger.PrintToUser(" ")
|
|
ux.Logger.PrintToUser("Region: [%s] ", logging.LightBlue.Wrap(region))
|
|
ux.Logger.PrintToUser(" ")
|
|
if len(cloudConfig.APIInstanceIDs) > 0 {
|
|
ux.Logger.PrintLineSeparator()
|
|
ux.Logger.PrintToUser("API Endpoint(s) for region [%s]: ", logging.LightBlue.Wrap(region))
|
|
for _, apiNode := range cloudConfig.APIInstanceIDs {
|
|
ux.Logger.PrintToUser(logging.Green.Wrap(fmt.Sprintf(" http://%s:9650", publicIPMap[apiNode])))
|
|
}
|
|
ux.Logger.PrintLineSeparator()
|
|
ux.Logger.PrintToUser("")
|
|
}
|
|
ux.Logger.PrintToUser("Don't delete or replace your ssh private key file at %s as you won't be able to access your cloud server without it", cloudConfig.CertFilePath)
|
|
ux.Logger.PrintLineSeparator()
|
|
for _, instanceID := range cloudConfig.InstanceIDs {
|
|
nodeID, _ := getNodeID(app.GetNodeInstanceDirPath(instanceID))
|
|
publicIP := ""
|
|
publicIP = publicIPMap[instanceID]
|
|
if slices.Contains(cloudConfig.APIInstanceIDs, instanceID) {
|
|
ux.Logger.PrintToUser("%s [API] Cloud Instance ID: %s | Public IP: %s | %s", logging.Green.Wrap(">"), instanceID, publicIP, logging.Green.Wrap(nodeID.String()))
|
|
} else {
|
|
ux.Logger.PrintToUser("%s Cloud Instance ID: %s | Public IP: %s | %s ", logging.Green.Wrap(">"), instanceID, publicIP, logging.Green.Wrap(nodeID.String()))
|
|
}
|
|
ux.Logger.PrintToUser("staker.crt, staker.key and signer.key are stored at %s. Please keep them safe, as these files can be used to fully recreate your node.", app.GetNodeInstanceDirPath(instanceID))
|
|
ux.Logger.PrintLineSeparator()
|
|
}
|
|
}
|
|
if addMonitoring {
|
|
monitoringHost := models.Host{
|
|
IP: monitoringHostIP,
|
|
}
|
|
if err := waitForMonitoringEndpoint(&monitoringHost); err != nil {
|
|
ux.Logger.RedXToUser("Failed to wait for monitoring endpoint to be available with error: %w", err)
|
|
} else {
|
|
getMonitoringHint(monitoringHostIP)
|
|
}
|
|
}
|
|
}
|
|
|
|
// getMonitoringHint prints the monitoring help message including the link to the monitoring dashboard
|
|
func getMonitoringHint(monitoringHostIP string) {
|
|
ux.Logger.PrintToUser("")
|
|
ux.Logger.PrintLineSeparator()
|
|
ux.Logger.PrintToUser("To view unified node %s, visit the following link in your browser: ", logging.LightBlue.Wrap("monitoring dashboard"))
|
|
ux.Logger.PrintToUser(logging.Green.Wrap(fmt.Sprintf("http://%s:%d/dashboards", monitoringHostIP, constants.LuxdGrafanaPort)))
|
|
ux.Logger.PrintToUser("Log in with username: admin, password: admin")
|
|
ux.Logger.PrintLineSeparator()
|
|
ux.Logger.PrintToUser("")
|
|
}
|
|
|
|
func waitForMonitoringEndpoint(monitoringHost *models.Host) error {
|
|
spinSession := ux.NewUserSpinner()
|
|
spinner := spinSession.SpinToUser("Waiting for monitoring endpoint to be available")
|
|
if err := monitoringHost.WaitForPort(constants.LuxdGrafanaPort, constants.SSHLongRunningScriptTimeout); err != nil {
|
|
spinner.Error()
|
|
return err
|
|
}
|
|
spinner.Complete()
|
|
spinSession.Stop()
|
|
return nil
|
|
}
|
|
|
|
// waitForHosts waits for all hosts to become available via SSH.
|
|
func waitForHosts(hosts []*models.Host) *models.NodeResults {
|
|
hostErrors := models.NodeResults{}
|
|
createdWaitGroup := sync.WaitGroup{}
|
|
spinSession := ux.NewUserSpinner()
|
|
for _, host := range hosts {
|
|
createdWaitGroup.Add(1)
|
|
go func(nodeResults *models.NodeResults, host *models.Host) {
|
|
defer createdWaitGroup.Done()
|
|
spinner := spinSession.SpinToUser(utils.ScriptLog(host.IP, "Waiting for instance response"))
|
|
if err := host.WaitForSSHShell(constants.SSHServerStartTimeout); err != nil {
|
|
nodeResults.AddResult(host.IP, nil, err)
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return
|
|
}
|
|
ux.SpinComplete(spinner)
|
|
}(&hostErrors, host)
|
|
}
|
|
createdWaitGroup.Wait()
|
|
spinSession.Stop()
|
|
return &hostErrors
|
|
}
|
|
|
|
// requestCloudAuth makes sure user agree to
|
|
func requestCloudAuth(cloudName string) error {
|
|
ux.Logger.PrintToUser("Do you authorize Lux-CLI to access your %s account?", cloudName)
|
|
ux.Logger.PrintToUser("By clicking yes, you are authorizing Lux-CLI to:")
|
|
ux.Logger.PrintToUser("- Create Cloud instance(s) and other components (such as elastic IPs)")
|
|
ux.Logger.PrintToUser("- Start/Stop Cloud instance(s) and other components (such as elastic IPs) previously created by Lux-CLI")
|
|
ux.Logger.PrintToUser("- Delete Cloud instance(s) and other components (such as elastic IPs) previously created by Lux-CLI")
|
|
yes, err := app.Prompt.CaptureYesNo(fmt.Sprintf("I authorize Lux-CLI to access my %s account", cloudName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := app.Conf.SetConfigValue(constants.ConfigAuthorizeCloudAccessKey, yes); err != nil {
|
|
return err
|
|
}
|
|
if !yes {
|
|
return fmt.Errorf("user did not give authorization to Lux-CLI to access %s account", cloudName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getSeparateHostNodeParam(cloudName string) (
|
|
string,
|
|
error,
|
|
) {
|
|
type CloudPrompt struct {
|
|
defaultLocations []string
|
|
locationName string
|
|
locationsListURL string
|
|
}
|
|
|
|
supportedClouds := map[string]CloudPrompt{
|
|
constants.AWSCloudService: {
|
|
defaultLocations: []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2"},
|
|
locationName: "AWS Region",
|
|
locationsListURL: "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html",
|
|
},
|
|
constants.GCPCloudService: {
|
|
defaultLocations: []string{"us-east1", "us-central1", "us-west1"},
|
|
locationName: "Google Region",
|
|
locationsListURL: "https://cloud.google.com/compute/docs/regions-zones/",
|
|
},
|
|
}
|
|
|
|
if _, ok := supportedClouds[cloudName]; !ok {
|
|
return "", fmt.Errorf("cloud %s is not supported", cloudName)
|
|
}
|
|
|
|
awsCustomRegion := fmt.Sprintf("Choose custom %s (list of %ss available at %s)", supportedClouds[cloudName].locationName, supportedClouds[cloudName].locationName, supportedClouds[cloudName].locationsListURL)
|
|
userRegion, err := app.Prompt.CaptureList(
|
|
fmt.Sprintf("Which %s do you want to set up your separate node in?", supportedClouds[cloudName].locationName),
|
|
append(supportedClouds[cloudName].defaultLocations, awsCustomRegion),
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if userRegion == awsCustomRegion {
|
|
userRegion, err = app.Prompt.CaptureString(fmt.Sprintf("Which %s do you want to set up your node in?", supportedClouds[cloudName].locationName))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return userRegion, nil
|
|
}
|
|
|
|
func getRegionsNodeNum(cloudName string) (
|
|
map[string]NumNodes,
|
|
error,
|
|
) {
|
|
type CloudPrompt struct {
|
|
defaultLocations []string
|
|
locationName string
|
|
locationsListURL string
|
|
}
|
|
|
|
supportedClouds := map[string]CloudPrompt{
|
|
constants.AWSCloudService: {
|
|
defaultLocations: []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2"},
|
|
locationName: "AWS Region",
|
|
locationsListURL: "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html",
|
|
},
|
|
constants.GCPCloudService: {
|
|
defaultLocations: []string{"us-east1", "us-central1", "us-west1"},
|
|
locationName: "Google Region",
|
|
locationsListURL: "https://cloud.google.com/compute/docs/regions-zones/",
|
|
},
|
|
}
|
|
|
|
if _, ok := supportedClouds[cloudName]; !ok {
|
|
return nil, fmt.Errorf("cloud %s is not supported", cloudName)
|
|
}
|
|
|
|
nodes := map[string]NumNodes{}
|
|
awsCustomRegion := fmt.Sprintf("Choose custom %s (list of %ss available at %s)", supportedClouds[cloudName].locationName, supportedClouds[cloudName].locationName, supportedClouds[cloudName].locationsListURL)
|
|
additionalRegionPrompt := fmt.Sprintf("Would you like to add additional %s?", supportedClouds[cloudName].locationName)
|
|
for {
|
|
userRegion, err := app.Prompt.CaptureList(
|
|
fmt.Sprintf("Which %s do you want to set up your node(s) in?", supportedClouds[cloudName].locationName),
|
|
append(supportedClouds[cloudName].defaultLocations, awsCustomRegion),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if userRegion == awsCustomRegion {
|
|
userRegion, err = app.Prompt.CaptureString(fmt.Sprintf("Which %s do you want to set up your node in?", supportedClouds[cloudName].locationName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
numAPINodes := uint32(0)
|
|
numNodes, err := app.Prompt.CaptureUint32(fmt.Sprintf("How many nodes do you want to set up in %s %s?", userRegion, supportedClouds[cloudName].locationName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet {
|
|
numAPINodes, err = app.Prompt.CaptureUint32(fmt.Sprintf("How many API nodes (nodes without stake) do you want to set up in %s %s?", userRegion, supportedClouds[cloudName].locationName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if numNodes > uint32(math.MaxInt32) || numAPINodes > uint32(math.MaxInt32) {
|
|
return nil, fmt.Errorf("number of nodes exceeds the range of a signed 32-bit integer")
|
|
}
|
|
nodes[userRegion] = NumNodes{int(numNodes), int(numAPINodes)}
|
|
var currentInput []string
|
|
if globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet {
|
|
currentInput = sdkutils.Map(maps.Keys(nodes), func(region string) string {
|
|
return fmt.Sprintf("[%s]: %d validator(s) %d api(s)", region, nodes[region].numValidators, nodes[region].numAPI)
|
|
})
|
|
} else {
|
|
currentInput = sdkutils.Map(maps.Keys(nodes), func(region string) string {
|
|
return fmt.Sprintf("[%s]: %d validator(s)", region, nodes[region].numValidators)
|
|
})
|
|
}
|
|
ux.Logger.PrintToUser("Current selection: " + strings.Join(currentInput, " "))
|
|
yes, err := app.Prompt.CaptureNoYes(additionalRegionPrompt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !yes {
|
|
return nodes, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func setSSHIdentity() (string, error) {
|
|
const yubikeyMark = " [YubiKey] (recommended)"
|
|
const yubikeyPattern = `cardno:(\d+(_\d+)*)`
|
|
sshIdentities, err := utils.ListSSHAgentIdentities()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
yubikeyRegexp := regexp.MustCompile(yubikeyPattern)
|
|
sshIdentities = sdkutils.Map(sshIdentities, func(id string) string {
|
|
if len(yubikeyRegexp.FindStringSubmatch(id)) > 0 {
|
|
return fmt.Sprintf("%s%s", id, yubikeyMark)
|
|
}
|
|
return id
|
|
})
|
|
sshIdentity, err := app.Prompt.CaptureList(
|
|
"Which SSH identity do you want to use?", sshIdentities,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.ReplaceAll(sshIdentity, yubikeyMark, ""), nil
|
|
}
|
|
|
|
// defaultLuxCLIPrefix returns the default Lux CLI prefix.
|
|
func defaultLuxCLIPrefix(region string) (string, error) {
|
|
usr, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if region == "" {
|
|
return usr.Username + constants.LuxCLISuffix, nil
|
|
}
|
|
return usr.Username + "-" + region + constants.LuxCLISuffix, nil
|
|
}
|
|
|
|
func sendNodeCreateMetrics(cloudService, network string, nodes map[string]NumNodes) {
|
|
flags := make(map[string]string)
|
|
totalValidatorNodes := 0
|
|
totalAPINodes := 0
|
|
for region := range nodes {
|
|
totalValidatorNodes += nodes[region].numValidators
|
|
totalAPINodes += nodes[region].numAPI
|
|
flags["region-"+region] = strconv.Itoa(nodes[region].numValidators)
|
|
}
|
|
flags[constants.MetricsNumRegions] = strconv.Itoa(len(maps.Keys(nodes)))
|
|
flags[constants.MetricsCloudService] = cloudService
|
|
flags[constants.MetricsNodeType] = nodeType
|
|
flags[constants.MetricsUseStaticIP] = strconv.FormatBool(useStaticIP)
|
|
flags[constants.MetricsNetwork] = network
|
|
flags[constants.MetricsValidatorCount] = strconv.Itoa(totalValidatorNodes)
|
|
flags[constants.MetricsAPICount] = strconv.Itoa(totalAPINodes)
|
|
if cloudService == constants.AWSCloudService {
|
|
flags[constants.MetricsAWSVolumeType] = volumeType
|
|
flags[constants.MetricsAWSVolumeSize] = strconv.Itoa(volumeSize)
|
|
}
|
|
flags[constants.MetricsEnableMonitoring] = strconv.FormatBool(addMonitoring)
|
|
if wizSubnet != "" {
|
|
populateSubnetVMMetrics(flags, wizSubnet)
|
|
flags[constants.MetricsCalledFromWiz] = strconv.FormatBool(true)
|
|
}
|
|
metrics.HandleTracking(app, flags, nil)
|
|
}
|
|
|
|
func getPrometheusTargets(clusterName string) ([]string, []string, []string, error) {
|
|
const loadTestPort = 8082
|
|
luxdPorts := []string{}
|
|
machinePorts := []string{}
|
|
ltPorts := []string{}
|
|
inventoryHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName))
|
|
if err != nil {
|
|
return luxdPorts, machinePorts, ltPorts, err
|
|
}
|
|
for _, host := range inventoryHosts {
|
|
luxdPorts = append(luxdPorts, fmt.Sprintf("'%s:%s'", host.IP, strconv.Itoa(constants.LuxdAPIPort)))
|
|
machinePorts = append(machinePorts, fmt.Sprintf("'%s:%s'", host.IP, strconv.Itoa(constants.LuxdMachineMetricsPort)))
|
|
}
|
|
// no need to check error here as it's ok to have no load test instances
|
|
separateHosts, _ := ansible.GetInventoryFromAnsibleInventoryFile(app.GetLoadTestInventoryDir(clusterName))
|
|
for _, host := range separateHosts {
|
|
ltPorts = append(ltPorts, fmt.Sprintf("'%s:%s'", host.IP, strconv.Itoa(loadTestPort)))
|
|
}
|
|
return luxdPorts, machinePorts, ltPorts, nil
|
|
}
|