mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
585 lines
24 KiB
Go
585 lines
24 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package nodecmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
|
"golang.org/x/exp/slices"
|
|
|
|
awsAPI "github.com/luxfi/cli/v2/pkg/cloud/aws"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
)
|
|
|
|
func getNewKeyPairName(ec2Svc *awsAPI.AwsCloud) (string, error) {
|
|
newKeyPairName := cmdLineAlternativeKeyPairName
|
|
for {
|
|
if newKeyPairName != "" {
|
|
keyPairExists, err := ec2Svc.CheckKeyPairExists(newKeyPairName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !keyPairExists {
|
|
return newKeyPairName, nil
|
|
}
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Key Pair named %s already exists", newKeyPairName))
|
|
}
|
|
ux.Logger.PrintToUser("What do you want to name your key pair?")
|
|
var err error
|
|
newKeyPairName, err = app.Prompt.CaptureString("Key Pair Name")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
|
|
func printNoCredentialsOutput(awsProfile string) {
|
|
ux.Logger.PrintToUser("No AWS credentials found in file ~/.aws/credentials ")
|
|
ux.Logger.PrintToUser("Or in environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY")
|
|
ux.Logger.PrintToUser("Please make sure correspoding keys are set in [%s] section in ~/.aws/credentials", awsProfile)
|
|
ux.Logger.PrintToUser("Or create a file called 'credentials' with the contents below, and add the file to ~/.aws/ directory if it's not already there")
|
|
ux.Logger.PrintToUser("===========BEGINNING OF FILE===========")
|
|
ux.Logger.PrintToUser("[%s]\naws_access_key_id=<AWS_ACCESS_KEY>\naws_secret_access_key=<AWS_SECRET_ACCESS_KEY>", awsProfile)
|
|
ux.Logger.PrintToUser("===========END OF FILE===========")
|
|
ux.Logger.PrintToUser("More info can be found at https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds")
|
|
ux.Logger.PrintToUser("Also you can set environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY")
|
|
ux.Logger.PrintToUser("Please use https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html#envvars-set for more details")
|
|
}
|
|
|
|
func isExpiredCredentialError(err error) bool {
|
|
return strings.Contains(err.Error(), "RequestExpired: Request has expired")
|
|
}
|
|
|
|
func printExpiredCredentialsOutput(awsProfile string) {
|
|
ux.Logger.PrintToUser("AWS credentials expired")
|
|
ux.Logger.PrintToUser("Please update your environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY")
|
|
ux.Logger.PrintToUser("Following https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html#envvars-set")
|
|
ux.Logger.PrintToUser("Or fill in ~/.aws/credentials with updated contents following the format below")
|
|
ux.Logger.PrintToUser("===========BEGINNING OF FILE===========")
|
|
ux.Logger.PrintToUser("[%s]\naws_access_key_id=<AWS_ACCESS_KEY>\naws_secret_access_key=<AWS_SECRET_ACCESS_KEY>", awsProfile)
|
|
ux.Logger.PrintToUser("===========END OF FILE===========")
|
|
ux.Logger.PrintToUser("More info can be found at https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds")
|
|
ux.Logger.PrintToUser("")
|
|
}
|
|
|
|
// getAWSCloudCredentials gets AWS account credentials defined in .aws dir in user home dir
|
|
func getAWSCloudCredentials(awsProfile, region string) (*awsAPI.AwsCloud, error) {
|
|
return awsAPI.NewAwsCloud(awsProfile, region)
|
|
}
|
|
|
|
// promptKeyPairName get custom name for key pair if the default key pair name that we use cannot be used for this EC2 instance
|
|
func promptKeyPairName(ec2 *awsAPI.AwsCloud) (string, error) {
|
|
newKeyPairName, err := getNewKeyPairName(ec2)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return newKeyPairName, nil
|
|
}
|
|
|
|
func getAWSMonitoringEC2Svc(awsProfile, monitoringRegion string) (map[string]*awsAPI.AwsCloud, error) {
|
|
ec2SvcMap := map[string]*awsAPI.AwsCloud{}
|
|
var err error
|
|
ec2SvcMap[monitoringRegion], err = getAWSCloudCredentials(awsProfile, monitoringRegion)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "cloud access is required") {
|
|
printNoCredentialsOutput(awsProfile)
|
|
}
|
|
return nil, err
|
|
}
|
|
return ec2SvcMap, nil
|
|
}
|
|
|
|
func getAWSCloudConfig(awsProfile string, singleNode bool, clusterSgRegions []string, instanceType string) (map[string]*awsAPI.AwsCloud, map[string]string, map[string]NumNodes, error) {
|
|
finalRegions := map[string]NumNodes{}
|
|
switch {
|
|
case len(numValidatorsNodes) != len(utils.Unique(cmdLineRegion)):
|
|
return nil, nil, nil, fmt.Errorf("number of nodes and regions should be the same")
|
|
case (globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet) && len(numAPINodes) != 0 && len(numAPINodes) != len(utils.Unique(cmdLineRegion)):
|
|
return nil, nil, nil, fmt.Errorf("number of api nodes and regions should be the same")
|
|
case (globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet) && len(numAPINodes) != 0 && len(numAPINodes) != len(numValidatorsNodes):
|
|
return nil, nil, nil, fmt.Errorf("number of api nodes and validator nodes should be the same")
|
|
case len(cmdLineRegion) == 0 && len(numValidatorsNodes) == 0 && len(numAPINodes) == 0:
|
|
var err error
|
|
if singleNode {
|
|
selectedRegion := ""
|
|
if loadTestHostRegion != "" {
|
|
selectedRegion = loadTestHostRegion
|
|
} else {
|
|
selectedRegion, err = getSeparateHostNodeParam(constants.AWSCloudService)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
finalRegions = map[string]NumNodes{selectedRegion: {1, 0}}
|
|
} else {
|
|
finalRegions, err = getRegionsNodeNum(constants.AWSCloudService)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
default:
|
|
for i, region := range cmdLineRegion {
|
|
numAPINodesInRegion := 0
|
|
if len(numAPINodes) > 0 {
|
|
numAPINodesInRegion = numAPINodes[i]
|
|
}
|
|
if globalNetworkFlags.UseDevnet || globalNetworkFlags.UseTestnet {
|
|
finalRegions[region] = NumNodes{numValidatorsNodes[i], numAPINodesInRegion}
|
|
} else {
|
|
finalRegions[region] = NumNodes{numValidatorsNodes[i], 0}
|
|
}
|
|
}
|
|
}
|
|
ec2SvcMap := map[string]*awsAPI.AwsCloud{}
|
|
amiMap := map[string]string{}
|
|
numNodesMap := map[string]NumNodes{}
|
|
// verify regions are valid
|
|
if invalidRegions, err := checkRegions(maps.Keys(finalRegions)); err != nil {
|
|
return nil, nil, nil, err
|
|
} else if len(invalidRegions) > 0 {
|
|
return nil, nil, nil, fmt.Errorf("invalid regions %s provided for %s", invalidRegions, constants.AWSCloudService)
|
|
}
|
|
for region := range finalRegions {
|
|
var err error
|
|
if singleNode {
|
|
for _, clusterRegion := range clusterSgRegions {
|
|
ec2SvcMap[clusterRegion], err = getAWSCloudCredentials(awsProfile, clusterRegion)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "cloud access is required") {
|
|
printNoCredentialsOutput(awsProfile)
|
|
}
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
} else {
|
|
ec2SvcMap[region], err = getAWSCloudCredentials(awsProfile, region)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "cloud access is required") {
|
|
printNoCredentialsOutput(awsProfile)
|
|
}
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
arch, err := ec2SvcMap[region].GetInstanceTypeArch(instanceType)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
amiMap[region], err = ec2SvcMap[region].GetUbuntuAMIID(arch, constants.UbuntuVersionLTS)
|
|
if err != nil {
|
|
if isExpiredCredentialError(err) {
|
|
printExpiredCredentialsOutput(awsProfile)
|
|
}
|
|
return nil, nil, nil, err
|
|
}
|
|
isSupported, err := ec2SvcMap[region].IsInstanceTypeSupported(instanceType)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
} else if !isSupported {
|
|
return nil, nil, nil, fmt.Errorf("instance type %s is not supported in region %s", instanceType, region)
|
|
}
|
|
|
|
numNodesMap[region] = finalRegions[region]
|
|
}
|
|
return ec2SvcMap, amiMap, numNodesMap, nil
|
|
}
|
|
|
|
// createEC2Instances creates ec2 instances
|
|
func createEC2Instances(ec2Svc map[string]*awsAPI.AwsCloud,
|
|
regions []string,
|
|
regionConf map[string]models.RegionConfig,
|
|
forMonitoring bool,
|
|
publicHTTPPortAccess bool,
|
|
) (map[string][]string, map[string][]string, map[string]string, map[string]string, error) {
|
|
if !forMonitoring {
|
|
ux.Logger.PrintToUser("Creating new EC2 instance(s) on AWS...")
|
|
} else {
|
|
ux.Logger.PrintToUser("Creating separate monitoring EC2 instance(s) on AWS...")
|
|
}
|
|
userIPAddress, err := utils.GetUserIPAddress()
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
keyPairName := map[string]string{}
|
|
instanceIDs := map[string][]string{}
|
|
elasticIPs := map[string][]string{}
|
|
sshCertPath := map[string]string{}
|
|
for _, region := range regions {
|
|
keyPairExists, err := ec2Svc[region].CheckKeyPairExists(regionConf[region].Prefix)
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
certInSSHDir, err := app.CheckCertInSSHDir(regionConf[region].CertName)
|
|
if useSSHAgent {
|
|
certInSSHDir = true // if using ssh agent, we consider that we have a cert on hand
|
|
}
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
sgID := ""
|
|
keyPairName[region] = regionConf[region].Prefix
|
|
securityGroupName := regionConf[region].SecurityGroupName
|
|
privKey, err := app.GetSSHCertFilePath(regionConf[region].CertName)
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
if replaceKeyPair && !forMonitoring {
|
|
// delete existing key pair on AWS console and download the newly created key pair file
|
|
// in .ssh dir (will overwrite existing file in .ssh dir)
|
|
if keyPairExists {
|
|
if err := ec2Svc[region].DeleteKeyPair(regionConf[region].Prefix); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, fmt.Errorf("unable to delete existing key pair %s in AWS console due to %w", regionConf[region].Prefix, err)
|
|
}
|
|
}
|
|
if err = os.RemoveAll(privKey); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, fmt.Errorf("unable to delete existing key pair file %s in .ssh dir due to %w", privKey, err)
|
|
}
|
|
if err := ec2Svc[region].CreateAndDownloadKeyPair(regionConf[region].Prefix, privKey); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
} else {
|
|
if !keyPairExists {
|
|
switch {
|
|
case useSSHAgent:
|
|
ux.Logger.PrintToUser("Using ssh agent identity %s to create key pair %s in AWS[%s]", sshIdentity, keyPairName[region], region)
|
|
if err := ec2Svc[region].UploadSSHIdentityKeyPair(regionConf[region].Prefix, sshIdentity); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
case !useSSHAgent && certInSSHDir:
|
|
ux.Logger.PrintToUser("Default Key Pair named %s already exists on your .ssh directory but not on AWS", regionConf[region].Prefix)
|
|
ux.Logger.PrintToUser("We need to create a new Key Pair in AWS as we can't find Key Pair named %s in AWS[%s]", regionConf[region].Prefix, region)
|
|
keyPairName[region], err = promptKeyPairName(ec2Svc[region])
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
if err := ec2Svc[region].CreateAndDownloadKeyPair(regionConf[region].Prefix, privKey); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
case !useSSHAgent && !certInSSHDir:
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Creating new key pair %s in AWS[%s]", keyPairName, region))
|
|
if err := ec2Svc[region].CreateAndDownloadKeyPair(regionConf[region].Prefix, privKey); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
} else {
|
|
// keypair exists
|
|
switch {
|
|
case useSSHAgent:
|
|
ux.Logger.PrintToUser("Using existing key pair %s in AWS[%s] via ssh-agent", keyPairName[region], region)
|
|
case !useSSHAgent && certInSSHDir:
|
|
ux.Logger.PrintToUser("Using existing key pair %s in AWS[%s]", keyPairName[region], region)
|
|
case !useSSHAgent && !certInSSHDir:
|
|
ux.Logger.PrintToUser("Default Key Pair named %s already exists in AWS[%s]", keyPairName[region], region)
|
|
ux.Logger.PrintToUser("We need to create a new Key Pair in AWS as we can't find Key Pair named %s in your .ssh directory", keyPairName[region])
|
|
keyPairName[region], err = promptKeyPairName(ec2Svc[region])
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
privKey, err = app.GetSSHCertFilePath(keyPairName[region] + constants.CertSuffix)
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
if err := ec2Svc[region].CreateAndDownloadKeyPair(keyPairName[region], privKey); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
securityGroupExists, sg, err := ec2Svc[region].CheckSecurityGroupExists(regionConf[region].SecurityGroupName)
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
if !securityGroupExists {
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Creating new security group %s in AWS[%s]", securityGroupName, region))
|
|
if newSGID, err := ec2Svc[region].SetupSecurityGroup(userIPAddress, regionConf[region].SecurityGroupName); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
} else {
|
|
sgID = newSGID
|
|
}
|
|
// allow public access to API luxd port
|
|
if publicHTTPPortAccess {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", "0.0.0.0/0", constants.LuxdAPIPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
} else {
|
|
sgID = *sg.GroupId
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Using existing security group %s in AWS[%s]", securityGroupName, region))
|
|
ipInTCP := awsAPI.CheckIPInSg(&sg, userIPAddress, constants.SSHTCPPort)
|
|
ipInHTTP := awsAPI.CheckIPInSg(&sg, userIPAddress, constants.LuxdAPIPort)
|
|
ipInMonitoring := awsAPI.CheckIPInSg(&sg, userIPAddress, constants.LuxdMonitoringPort)
|
|
ipInGrafana := awsAPI.CheckIPInSg(&sg, userIPAddress, constants.LuxdGrafanaPort)
|
|
ipInLoki := awsAPI.CheckIPInSg(&sg, "0.0.0.0/0", constants.LuxdLokiPort)
|
|
|
|
if !ipInTCP {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", userIPAddress, constants.SSHTCPPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
if !ipInHTTP {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", userIPAddress, constants.LuxdAPIPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
if !ipInMonitoring {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", userIPAddress, constants.LuxdMonitoringPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
if !ipInGrafana {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", userIPAddress, constants.LuxdGrafanaPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
if !ipInLoki {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", "0.0.0.0/0", constants.LuxdLokiPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
// check for public access to API port if flag is set
|
|
if publicHTTPPortAccess {
|
|
ipInPublicAPI := awsAPI.CheckIPInSg(&sg, "0.0.0.0/0", constants.LuxdAPIPort)
|
|
if !ipInPublicAPI {
|
|
if err := ec2Svc[region].AddSecurityGroupRule(sgID, "ingress", "tcp", "0.0.0.0/0", constants.LuxdAPIPort); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sshCertPath[region] = privKey
|
|
if instanceIDs[region], err = ec2Svc[region].CreateEC2Instances(
|
|
regionConf[region].Prefix,
|
|
regionConf[region].NumNodes,
|
|
regionConf[region].ImageID,
|
|
regionConf[region].InstanceType,
|
|
keyPairName[region],
|
|
sgID,
|
|
forMonitoring,
|
|
iops,
|
|
throughput,
|
|
stringToAWSVolumeType(volumeType),
|
|
volumeSize,
|
|
); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
spinSession := ux.NewUserSpinner()
|
|
spinner := spinSession.SpinToUser("Waiting for EC2 instance(s) in AWS[%s] to be provisioned...", region)
|
|
if err := ec2Svc[region].WaitForEC2Instances(instanceIDs[region], types.InstanceStateNameRunning); err != nil {
|
|
ux.SpinFailWithError(spinner, "", err)
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
ux.SpinComplete(spinner)
|
|
spinSession.Stop()
|
|
if useStaticIP {
|
|
publicIPs := []string{}
|
|
for count := 0; count < regionConf[region].NumNodes; count++ {
|
|
allocationID, publicIP, err := ec2Svc[region].CreateEIP(regionConf[region].Prefix)
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
if err := ec2Svc[region].AssociateEIP(instanceIDs[region][count], allocationID); err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
publicIPs = append(publicIPs, publicIP)
|
|
}
|
|
elasticIPs[region] = publicIPs
|
|
} else {
|
|
instanceEIPMap, err := ec2Svc[region].GetInstancePublicIPs(instanceIDs[region])
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
regionElasticIPs := []string{}
|
|
for _, instanceID := range instanceIDs[region] {
|
|
regionElasticIPs = append(regionElasticIPs, instanceEIPMap[instanceID])
|
|
}
|
|
elasticIPs[region] = regionElasticIPs
|
|
}
|
|
}
|
|
ux.Logger.GreenCheckmarkToUser("New EC2 instance(s) successfully created in AWS!")
|
|
for _, region := range regions {
|
|
if useSSHAgent {
|
|
sshCertPath[region] = ""
|
|
} else {
|
|
// don't overwrite existing sshCertPath for a particular region
|
|
if _, ok := sshCertPath[region]; !ok {
|
|
sshCertPath[region], err = app.GetSSHCertFilePath(regionConf[region].CertName)
|
|
if err != nil {
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// instanceIDs, elasticIPs, certFilePath, keyPairName, err
|
|
return instanceIDs, elasticIPs, sshCertPath, keyPairName, nil
|
|
}
|
|
|
|
func AddMonitoringSecurityGroupRule(ec2Svc map[string]*awsAPI.AwsCloud, monitoringHostPublicIP, securityGroupName, region string) error {
|
|
securityGroupExists, sg, err := ec2Svc[region].CheckSecurityGroupExists(securityGroupName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !securityGroupExists {
|
|
return fmt.Errorf("security group %s doesn't exist in region %s", securityGroupName, region)
|
|
}
|
|
metricsPortInSG := awsAPI.CheckIPInSg(&sg, monitoringHostPublicIP, constants.LuxdMachineMetricsPort)
|
|
apiPortInSG := awsAPI.CheckIPInSg(&sg, monitoringHostPublicIP, constants.LuxdAPIPort)
|
|
if !metricsPortInSG {
|
|
if err = ec2Svc[region].AddSecurityGroupRule(*sg.GroupId, "ingress", "tcp", monitoringHostPublicIP+constants.IPAddressSuffix, constants.LuxdMachineMetricsPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !apiPortInSG {
|
|
if err = ec2Svc[region].AddSecurityGroupRule(*sg.GroupId, "ingress", "tcp", monitoringHostPublicIP+constants.IPAddressSuffix, constants.LuxdAPIPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deleteHostSecurityGroupRule(ec2Svc *awsAPI.AwsCloud, hostPublicIP, securityGroupName string) error {
|
|
securityGroupExists, sg, err := ec2Svc.CheckSecurityGroupExists(securityGroupName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// exit early if security group doesn't exist
|
|
if !securityGroupExists {
|
|
return nil
|
|
}
|
|
metricsPortInSG := awsAPI.CheckIPInSg(&sg, hostPublicIP, constants.LuxdMachineMetricsPort)
|
|
apiPortInSG := awsAPI.CheckIPInSg(&sg, hostPublicIP, constants.LuxdAPIPort)
|
|
if metricsPortInSG {
|
|
if err = ec2Svc.DeleteSecurityGroupRule(*sg.GroupId, "ingress", "tcp", hostPublicIP+constants.IPAddressSuffix, constants.LuxdMachineMetricsPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if apiPortInSG {
|
|
if err = ec2Svc.DeleteSecurityGroupRule(*sg.GroupId, "ingress", "tcp", hostPublicIP+constants.IPAddressSuffix, constants.LuxdAPIPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func grantAccessToPublicIPViaSecurityGroup(ec2Svc *awsAPI.AwsCloud, publicIP, securityGroupName, region string) error {
|
|
securityGroupExists, sg, err := ec2Svc.CheckSecurityGroupExists(securityGroupName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !securityGroupExists {
|
|
return fmt.Errorf("security group %s doesn't exist in region %s", securityGroupName, region)
|
|
}
|
|
metricsPortInSG := awsAPI.CheckIPInSg(&sg, publicIP, constants.LuxdMachineMetricsPort)
|
|
apiPortInSG := awsAPI.CheckIPInSg(&sg, publicIP, constants.LuxdAPIPort)
|
|
if !metricsPortInSG {
|
|
if err = ec2Svc.AddSecurityGroupRule(*sg.GroupId, "ingress", "tcp", publicIP+constants.IPAddressSuffix, constants.LuxdMachineMetricsPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !apiPortInSG {
|
|
if err = ec2Svc.AddSecurityGroupRule(*sg.GroupId, "ingress", "tcp", publicIP+constants.IPAddressSuffix, constants.LuxdAPIPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createAWSInstances(
|
|
ec2Svc map[string]*awsAPI.AwsCloud,
|
|
nodeType string,
|
|
numNodes map[string]NumNodes,
|
|
regions []string,
|
|
ami map[string]string,
|
|
forMonitoring bool,
|
|
publicHTTPPortAccess bool) (
|
|
models.CloudConfig, error,
|
|
) {
|
|
regionConf := map[string]models.RegionConfig{}
|
|
for _, region := range regions {
|
|
prefix, err := defaultLuxCLIPrefix(region)
|
|
if err != nil {
|
|
return models.CloudConfig{}, err
|
|
}
|
|
regionConf[region] = models.RegionConfig{
|
|
Prefix: prefix,
|
|
ImageID: ami[region],
|
|
CertName: prefix + "-" + region + constants.CertSuffix,
|
|
SecurityGroupName: prefix + "-" + region + constants.AWSSecurityGroupSuffix,
|
|
NumNodes: numNodes[region].All(),
|
|
InstanceType: nodeType,
|
|
}
|
|
}
|
|
// Create new EC2 instances
|
|
instanceIDs, elasticIPs, certFilePath, keyPairName, err := createEC2Instances(ec2Svc, regions, regionConf, forMonitoring, publicHTTPPortAccess)
|
|
if err != nil {
|
|
if err.Error() == constants.EIPLimitErr {
|
|
ux.Logger.PrintToUser("Failed to create AWS cloud server(s), please try creating again in a different region")
|
|
} else {
|
|
ux.Logger.PrintToUser("Failed to create AWS cloud server(s) with error: %s", err.Error())
|
|
}
|
|
// we destroy created instances so that user doesn't pay for unused EC2 instances
|
|
ux.Logger.PrintToUser("Destroying all created AWS instances due to error to prevent charge for unused AWS instances...")
|
|
failedNodes := map[string]error{}
|
|
for region, regionInstanceID := range instanceIDs {
|
|
for _, instanceID := range regionInstanceID {
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Destroying AWS cloud server %s...", instanceID))
|
|
if destroyErr := ec2Svc[region].DestroyInstance(instanceID, "", true); destroyErr != nil {
|
|
failedNodes[instanceID] = destroyErr
|
|
}
|
|
ux.Logger.PrintToUser(fmt.Sprintf("AWS cloud server instance %s destroyed", instanceID))
|
|
}
|
|
}
|
|
if len(failedNodes) > 0 {
|
|
ux.Logger.PrintToUser("Failed nodes: ")
|
|
for node, err := range failedNodes {
|
|
ux.Logger.PrintToUser(fmt.Sprintf("Failed to destroy node %s due to %s", node, err))
|
|
}
|
|
ux.Logger.PrintToUser("Destroy the above instance(s) on AWS console to prevent charges")
|
|
return models.CloudConfig{}, fmt.Errorf("failed to destroy node(s) %s", failedNodes)
|
|
}
|
|
return models.CloudConfig{}, err
|
|
}
|
|
awsCloudConfig := models.CloudConfig{}
|
|
for _, region := range regions {
|
|
awsCloudConfig[region] = models.RegionConfig{
|
|
InstanceIDs: instanceIDs[region],
|
|
PublicIPs: elasticIPs[region],
|
|
KeyPair: keyPairName[region],
|
|
SecurityGroup: regionConf[region].SecurityGroupName,
|
|
CertFilePath: certFilePath[region],
|
|
ImageID: ami[region],
|
|
}
|
|
}
|
|
return awsCloudConfig, nil
|
|
}
|
|
|
|
// checkRegions checks if the given regions are available in AWS.
|
|
// It returns list of invalid regions and error if any
|
|
func checkRegions(regions []string) ([]string, error) {
|
|
const regionCheckerRegion = "us-east-1"
|
|
invalidRegions := []string{}
|
|
awsCloudRegionChecker, err := getAWSCloudCredentials(awsProfile, regionCheckerRegion)
|
|
if err != nil {
|
|
return invalidRegions, err
|
|
}
|
|
availableRegions, err := awsCloudRegionChecker.ListRegions()
|
|
if err != nil {
|
|
if isExpiredCredentialError(err) {
|
|
printExpiredCredentialsOutput(awsProfile)
|
|
}
|
|
return invalidRegions, err
|
|
}
|
|
for _, region := range regions {
|
|
if !slices.Contains(availableRegions, region) {
|
|
invalidRegions = append(invalidRegions, region)
|
|
}
|
|
}
|
|
return invalidRegions, nil
|
|
}
|