package com.sequenceiq.cloudbreak.cloud.aws; import static com.amazonaws.services.cloudformation.model.StackStatus.CREATE_COMPLETE; import static com.amazonaws.services.cloudformation.model.StackStatus.CREATE_FAILED; import static com.amazonaws.services.cloudformation.model.StackStatus.DELETE_COMPLETE; import static com.amazonaws.services.cloudformation.model.StackStatus.DELETE_FAILED; import static com.amazonaws.services.cloudformation.model.StackStatus.ROLLBACK_COMPLETE; import static com.amazonaws.services.cloudformation.model.StackStatus.ROLLBACK_FAILED; import static com.amazonaws.services.cloudformation.model.StackStatus.ROLLBACK_IN_PROGRESS; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.apache.commons.net.util.SubnetUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.autoscaling.AmazonAutoScalingClient; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsRequest; import com.amazonaws.services.autoscaling.model.DetachInstancesRequest; import com.amazonaws.services.autoscaling.model.ResumeProcessesRequest; import com.amazonaws.services.autoscaling.model.SuspendProcessesRequest; import com.amazonaws.services.autoscaling.model.UpdateAutoScalingGroupRequest; import com.amazonaws.services.cloudformation.AmazonCloudFormationClient; import com.amazonaws.services.cloudformation.model.CreateStackRequest; import com.amazonaws.services.cloudformation.model.DeleteStackRequest; import com.amazonaws.services.cloudformation.model.DescribeStacksRequest; import com.amazonaws.services.cloudformation.model.OnFailure; import com.amazonaws.services.cloudformation.model.Output; import com.amazonaws.services.cloudformation.model.Parameter; import com.amazonaws.services.cloudformation.model.StackStatus; import com.amazonaws.services.ec2.AmazonEC2Client; import com.amazonaws.services.ec2.model.Address; import com.amazonaws.services.ec2.model.AssociateAddressRequest; import com.amazonaws.services.ec2.model.DescribeAddressesRequest; import com.amazonaws.services.ec2.model.DescribeAddressesResult; import com.amazonaws.services.ec2.model.DescribeImagesRequest; import com.amazonaws.services.ec2.model.DescribeImagesResult; import com.amazonaws.services.ec2.model.DescribeSubnetsRequest; import com.amazonaws.services.ec2.model.DescribeSubnetsResult; import com.amazonaws.services.ec2.model.DescribeVpcsRequest; import com.amazonaws.services.ec2.model.DisassociateAddressRequest; import com.amazonaws.services.ec2.model.Filter; import com.amazonaws.services.ec2.model.Image; import com.amazonaws.services.ec2.model.ReleaseAddressRequest; import com.amazonaws.services.ec2.model.Subnet; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import com.amazonaws.services.ec2.model.Vpc; import com.google.common.collect.Lists; import com.google.common.net.InetAddresses; import com.sequenceiq.cloudbreak.api.model.AdjustmentType; import com.sequenceiq.cloudbreak.api.model.InstanceGroupType; import com.sequenceiq.cloudbreak.cloud.ResourceConnector; import com.sequenceiq.cloudbreak.cloud.aws.task.AwsPollTaskFactory; import com.sequenceiq.cloudbreak.cloud.aws.view.AwsCredentialView; import com.sequenceiq.cloudbreak.cloud.aws.view.AwsInstanceProfileView; import com.sequenceiq.cloudbreak.cloud.aws.view.AwsNetworkView; import com.sequenceiq.cloudbreak.cloud.context.AuthenticatedContext; import com.sequenceiq.cloudbreak.cloud.exception.CloudConnectorException; import com.sequenceiq.cloudbreak.cloud.exception.TemplatingDoesNotSupportedException; import com.sequenceiq.cloudbreak.cloud.model.CloudInstance; import com.sequenceiq.cloudbreak.cloud.model.CloudResource; import com.sequenceiq.cloudbreak.cloud.model.CloudResourceStatus; import com.sequenceiq.cloudbreak.cloud.model.CloudStack; import com.sequenceiq.cloudbreak.cloud.model.Group; import com.sequenceiq.cloudbreak.cloud.model.Network; import com.sequenceiq.cloudbreak.cloud.model.ResourceStatus; import com.sequenceiq.cloudbreak.cloud.model.TlsInfo; import com.sequenceiq.cloudbreak.cloud.notification.PersistenceNotifier; import com.sequenceiq.cloudbreak.cloud.scheduler.SyncPollingScheduler; import com.sequenceiq.cloudbreak.cloud.task.PollTask; import com.sequenceiq.cloudbreak.common.type.ResourceType; import freemarker.template.Configuration; @Service public class AwsResourceConnector implements ResourceConnector<Object> { private static final Logger LOGGER = LoggerFactory.getLogger(AwsResourceConnector.class); private static final List<String> CAPABILITY_IAM = singletonList("CAPABILITY_IAM"); private static final int INCREMENT_HOST_NUM = 256; private static final int CIDR_PREFIX = 24; private static final List<String> SUSPENDED_PROCESSES = asList("Launch", "HealthCheck", "ReplaceUnhealthy", "AZRebalance", "AlarmNotification", "ScheduledActions", "AddToLoadBalancer", "RemoveFromLoadBalancerLowPriority"); private static final List<StackStatus> ERROR_STATUSES = asList(CREATE_FAILED, ROLLBACK_IN_PROGRESS, ROLLBACK_FAILED, ROLLBACK_COMPLETE); private static final String CFS_OUTPUT_EIPALLOCATION_ID = "EIPAllocationID"; private static final String S3_ACCESS_ROLE = "S3AccessRole"; private static final String CREATED_VPC = "CreatedVpc"; private static final String CREATED_SUBNET = "CreatedSubnet"; @Inject private Configuration freemarkerConfiguration; @Inject private AwsClient awsClient; @Inject private CloudFormationStackUtil cfStackUtil; @Inject private SyncPollingScheduler<Boolean> syncPollingScheduler; @Inject private CloudFormationTemplateBuilder cloudFormationTemplateBuilder; @Inject private AwsPollTaskFactory awsPollTaskFactory; @Inject private CloudFormationStackUtil cloudFormationStackUtil; @Inject private AwsTagPreparationService awsTagPreparationService; @Value("${cb.publicip:}") private String cloudbreakPublicIp; @Value("${cb.aws.default.inbound.security.group:}") private String defaultInboundSecurityGroup; @Value("${cb.aws.vpc:}") private String cloudbreakVpc; @Value("${cb.nginx.port:9443}") private int gatewayPort; @Value("${cb.aws.cf.template.new.path:}") private String awsCloudformationTemplatePath; @Value("${cb.default.gateway.cidr:}") private String defaultGatewayCidr; @Override public List<CloudResourceStatus> launch(AuthenticatedContext ac, CloudStack stack, PersistenceNotifier resourceNotifier, AdjustmentType adjustmentType, Long threshold) throws Exception { String cFStackName = cfStackUtil.getCfStackName(ac); AwsCredentialView credentialView = new AwsCredentialView(ac.getCloudCredential()); String regionName = ac.getCloudContext().getLocation().getRegion().value(); AmazonCloudFormationClient cfClient = awsClient.createCloudFormationClient(credentialView, regionName); AmazonEC2Client amazonEC2Client = awsClient.createAccess(credentialView, regionName); AwsNetworkView awsNetworkView = new AwsNetworkView(stack.getNetwork()); boolean existingVPC = awsNetworkView.isExistingVPC(); boolean existingSubnet = awsNetworkView.isExistingSubnet(); boolean mapPublicIpOnLaunch = true; if (existingVPC && existingSubnet) { DescribeSubnetsRequest describeSubnetsRequest = new DescribeSubnetsRequest(); describeSubnetsRequest.setSubnetIds(awsNetworkView.getSubnetList()); DescribeSubnetsResult describeSubnetsResult = amazonEC2Client.describeSubnets(describeSubnetsRequest); if (!describeSubnetsResult.getSubnets().isEmpty()) { mapPublicIpOnLaunch = describeSubnetsResult.getSubnets().get(0).isMapPublicIpOnLaunch(); } } try { cfClient.describeStacks(new DescribeStacksRequest().withStackName(cFStackName)); LOGGER.info("Stack already exists: {}", cFStackName); } catch (AmazonServiceException e) { CloudResource cloudFormationStack = new CloudResource.Builder().type(ResourceType.CLOUDFORMATION_STACK).name(cFStackName).build(); resourceNotifier.notifyAllocation(cloudFormationStack, ac.getCloudContext()); String cidr = stack.getNetwork().getSubnet().getCidr(); String subnet = isNoCIDRProvided(existingVPC, existingSubnet, cidr) ? findNonOverLappingCIDR(ac, stack) : cidr; String inboundSecurityGroup = deployingToSameVPC(awsNetworkView, existingVPC) ? defaultInboundSecurityGroup : ""; AwsInstanceProfileView awsInstanceProfileView = new AwsInstanceProfileView(stack); CloudFormationTemplateBuilder.ModelContext modelContext = new CloudFormationTemplateBuilder.ModelContext() .withAuthenticatedContext(ac) .withStack(stack) .withExistingVpc(existingVPC) .withExistingIGW(awsNetworkView.isExistingIGW()) .withExistingSubnetCidr(existingSubnet ? getExistingSubnetCidr(ac, stack) : null) .mapPublicIpOnLaunch(mapPublicIpOnLaunch) .withEnableInstanceProfile(awsInstanceProfileView.isEnableInstanceProfileStrategy()) .withInstanceProfileAvailable(awsInstanceProfileView.isInstanceProfileAvailable()) .withTemplate(stack.getTemplate()) .withDefaultSubnet(subnet) .withCloudbreakPublicIp(cloudbreakPublicIp) .withDefaultInboundSecurityGroup(inboundSecurityGroup) .withGatewayPort(gatewayPort) .withDefaultGatewayCidr(defaultGatewayCidr); String cfTemplate = cloudFormationTemplateBuilder.build(modelContext); LOGGER.debug("CloudFormationTemplate: {}", cfTemplate); cfClient.createStack(createCreateStackRequest(ac, stack, cFStackName, subnet, cfTemplate)); } LOGGER.info("CloudFormation stack creation request sent with stack name: '{}' for stack: '{}'", cFStackName, ac.getCloudContext().getId()); AmazonAutoScalingClient asClient = awsClient.createAutoScalingClient(credentialView, regionName); PollTask<Boolean> task = awsPollTaskFactory.newAwsCreateStackStatusCheckerTask(ac, cfClient, asClient, CREATE_COMPLETE, CREATE_FAILED, ERROR_STATUSES, cFStackName); try { Boolean statePollerResult = task.call(); if (!task.completed(statePollerResult)) { syncPollingScheduler.schedule(task); } } catch (Exception e) { throw new CloudConnectorException(e.getMessage(), e); } AmazonAutoScalingClient amazonASClient = awsClient.createAutoScalingClient(credentialView, regionName); saveS3AccessRoleArn(ac, stack, cFStackName, cfClient, resourceNotifier); saveGeneratedSubnet(ac, stack, cFStackName, cfClient, resourceNotifier); List<CloudResource> cloudResources = getCloudResources(ac, stack, cFStackName, cfClient, amazonEC2Client, amazonASClient, mapPublicIpOnLaunch); return check(ac, cloudResources); } private boolean isNoCIDRProvided(boolean existingVPC, boolean existingSubnet, String cidr) { return existingVPC && !existingSubnet && cidr == null; } private boolean deployingToSameVPC(AwsNetworkView awsNetworkView, boolean existingVPC) { return StringUtils.isNoneEmpty(cloudbreakVpc) && existingVPC && awsNetworkView.getExistingVPC().equals(cloudbreakVpc); } private CreateStackRequest createCreateStackRequest(AuthenticatedContext ac, CloudStack stack, String cFStackName, String subnet, String cfTemplate) { return new CreateStackRequest() .withStackName(cFStackName) .withOnFailure(OnFailure.DO_NOTHING) .withTemplateBody(cfTemplate) .withTags(awsTagPreparationService.prepareTags(ac, stack.getTags())) .withCapabilities(CAPABILITY_IAM) .withParameters(getStackParameters(ac, stack, cFStackName, subnet)); } private List<CloudResource> getCloudResources(AuthenticatedContext ac, CloudStack stack, String cFStackName, AmazonCloudFormationClient client, AmazonEC2Client amazonEC2Client, AmazonAutoScalingClient amazonASClient, boolean mapPublicIpOnLaunch) { List<CloudResource> cloudResources = new ArrayList<>(); AmazonCloudFormationClient cloudFormationClient = awsClient.createCloudFormationClient(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); scheduleStatusChecks(stack, ac, cloudFormationClient); suspendAutoScaling(ac, stack); if (mapPublicIpOnLaunch) { List<String> eipAllocationIds = getElasticIpAllocationIds(cFStackName, client); Group gateWay = stack.getGroups().stream().filter(group -> group.getType() == InstanceGroupType.GATEWAY).findFirst().get(); List<String> instanceIds = cfStackUtil.getInstanceIds(amazonASClient, cfStackUtil.getAutoscalingGroupName(ac, client, gateWay.getName())); associateElasticIpsToInstances(amazonEC2Client, eipAllocationIds, instanceIds); } return cloudResources; } private void saveS3AccessRoleArn(AuthenticatedContext ac, CloudStack stack, String cFStackName, AmazonCloudFormationClient client, PersistenceNotifier resourceNotifier) { AwsInstanceProfileView awsInstanceProfileView = new AwsInstanceProfileView(stack); if (awsInstanceProfileView.isEnableInstanceProfileStrategy() && !awsInstanceProfileView.isInstanceProfileAvailable()) { String s3AccessRoleArn = getCreatedS3AccessRoleArn(cFStackName, client); CloudResource s3AccessRoleArnCloudResource = new CloudResource.Builder().type(ResourceType.S3_ACCESS_ROLE_ARN).name(s3AccessRoleArn).build(); resourceNotifier.notifyAllocation(s3AccessRoleArnCloudResource, ac.getCloudContext()); } } private void saveGeneratedSubnet(AuthenticatedContext ac, CloudStack stack, String cFStackName, AmazonCloudFormationClient client, PersistenceNotifier resourceNotifier) { AwsNetworkView awsNetworkView = new AwsNetworkView(stack.getNetwork()); if (awsNetworkView.isExistingVPC()) { String vpcId = awsNetworkView.getExistingVPC(); CloudResource vpc = new CloudResource.Builder().type(ResourceType.AWS_VPC).name(vpcId).build(); resourceNotifier.notifyAllocation(vpc, ac.getCloudContext()); } else { String vpcId = getCreatedVpc(cFStackName, client); CloudResource vpc = new CloudResource.Builder().type(ResourceType.AWS_VPC).name(vpcId).build(); resourceNotifier.notifyAllocation(vpc, ac.getCloudContext()); } if (awsNetworkView.isExistingSubnet()) { String subnetId = awsNetworkView.getExistingSubnet(); CloudResource subnet = new CloudResource.Builder().type(ResourceType.AWS_SUBNET).name(subnetId).build(); resourceNotifier.notifyAllocation(subnet, ac.getCloudContext()); } else { String subnetId = getCreatedSubnet(cFStackName, client); CloudResource subnet = new CloudResource.Builder().type(ResourceType.AWS_SUBNET).name(subnetId).build(); resourceNotifier.notifyAllocation(subnet, ac.getCloudContext()); } } private String getCreatedVpc(String cFStackName, AmazonCloudFormationClient client) { Map<String, String> outputs = getOutputs(cFStackName, client); if (outputs.containsKey(CREATED_VPC)) { return outputs.get(CREATED_VPC); } else { String outputKeyNotFound = String.format("Vpc could not be found in the Cloudformation stack('%s') output.", cFStackName); throw new CloudConnectorException(outputKeyNotFound); } } private String getCreatedSubnet(String cFStackName, AmazonCloudFormationClient client) { Map<String, String> outputs = getOutputs(cFStackName, client); if (outputs.containsKey(CREATED_SUBNET)) { return outputs.get(CREATED_SUBNET); } else { String outputKeyNotFound = String.format("Subnet could not be found in the Cloudformation stack('%s') output.", cFStackName); throw new CloudConnectorException(outputKeyNotFound); } } private String getCreatedS3AccessRoleArn(String cFStackName, AmazonCloudFormationClient client) { Map<String, String> outputs = getOutputs(cFStackName, client); if (outputs.containsKey(S3_ACCESS_ROLE)) { return outputs.get(S3_ACCESS_ROLE); } else { String outputKeyNotFound = String.format("S3AccessRole arn could not be found in the Cloudformation stack('%s') output.", cFStackName); throw new CloudConnectorException(outputKeyNotFound); } } private List<String> getElasticIpAllocationIds(String cFStackName, AmazonCloudFormationClient client) { Map<String, String> outputs = getOutputs(cFStackName, client); List<String> elasticIpIds = outputs.entrySet().stream().filter(e -> e.getKey().startsWith(CFS_OUTPUT_EIPALLOCATION_ID)).map(e -> e.getValue()) .collect(Collectors.toList()); if (!elasticIpIds.isEmpty()) { return elasticIpIds; } else { String outputKeyNotFound = String.format("Allocation Id of Elastic IP could not be found in the Cloudformation stack('%s') output.", cFStackName); throw new CloudConnectorException(outputKeyNotFound); } } private Map<String, String> getOutputs(String cFStackName, AmazonCloudFormationClient client) { DescribeStacksRequest describeStacksRequest = new DescribeStacksRequest().withStackName(cFStackName); String outputNotFound = String.format("Couldn't get Cloudformation stack's('%s') output", cFStackName); List<Output> cfStackOutputs = client.describeStacks(describeStacksRequest).getStacks() .stream().findFirst().orElseThrow(getCloudConnectorExceptionSupplier(outputNotFound)).getOutputs(); return cfStackOutputs.stream().collect(Collectors.toMap(Output::getOutputKey, Output::getOutputValue)); } private void associateElasticIpsToInstances(AmazonEC2Client amazonEC2Client, List<String> eipAllocationIds, List<String> instanceIds) { if (eipAllocationIds.size() == instanceIds.size()) { for (int i = 0; i < eipAllocationIds.size(); i++) { associateElasticIpToInstance(amazonEC2Client, eipAllocationIds.get(i), instanceIds.get(i)); } } } private void associateElasticIpToInstance(AmazonEC2Client amazonEC2Client, String eipAllocationId, String instanceId) { AssociateAddressRequest associateAddressRequest = new AssociateAddressRequest() .withAllocationId(eipAllocationId) .withInstanceId(instanceId); amazonEC2Client.associateAddress(associateAddressRequest); } private Supplier<CloudConnectorException> getCloudConnectorExceptionSupplier(String msg) { return () -> new CloudConnectorException(msg); } private void suspendAutoScaling(AuthenticatedContext ac, CloudStack stack) { AmazonAutoScalingClient amazonASClient = awsClient.createAutoScalingClient(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); for (Group group : stack.getGroups()) { String asGroupName = cfStackUtil.getAutoscalingGroupName(ac, group.getName(), ac.getCloudContext().getLocation().getRegion().value()); amazonASClient.suspendProcesses(new SuspendProcessesRequest().withAutoScalingGroupName(asGroupName).withScalingProcesses(SUSPENDED_PROCESSES)); } } private void resumeAutoScaling(AuthenticatedContext ac, CloudStack stack) { AmazonAutoScalingClient amazonASClient = awsClient.createAutoScalingClient(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); for (Group group : stack.getGroups()) { String asGroupName = cfStackUtil.getAutoscalingGroupName(ac, group.getName(), ac.getCloudContext().getLocation().getRegion().value()); amazonASClient.resumeProcesses(new ResumeProcessesRequest().withAutoScalingGroupName(asGroupName).withScalingProcesses(SUSPENDED_PROCESSES)); } } private List<Parameter> getStackParameters(AuthenticatedContext ac, CloudStack stack, String stackName, String newSubnetCidr) { AwsNetworkView awsNetworkView = new AwsNetworkView(stack.getNetwork()); AwsInstanceProfileView awsInstanceProfileView = new AwsInstanceProfileView(stack); String keyPairName = awsClient.getKeyPairName(ac); if (awsClient.existingKeyPairNameSpecified(ac)) { keyPairName = awsClient.getExistingKeyPairName(ac); } List<Parameter> parameters = new ArrayList<>(asList( new Parameter().withParameterKey("CBUserData").withParameterValue(stack.getImage().getUserData(InstanceGroupType.CORE)), new Parameter().withParameterKey("CBGateWayUserData").withParameterValue(stack.getImage().getUserData(InstanceGroupType.GATEWAY)), new Parameter().withParameterKey("StackName").withParameterValue(stackName), new Parameter().withParameterKey("StackOwner").withParameterValue(ac.getCloudContext().getOwner()), new Parameter().withParameterKey("KeyName").withParameterValue(keyPairName), new Parameter().withParameterKey("AMI").withParameterValue(stack.getImage().getImageName()), new Parameter().withParameterKey("RootDeviceName").withParameterValue(getRootDeviceName(ac, stack)) )); if (awsInstanceProfileView.isUseExistingInstanceProfile() && awsInstanceProfileView.isEnableInstanceProfileStrategy()) { parameters.add(new Parameter().withParameterKey("InstanceProfile").withParameterValue(awsInstanceProfileView.getInstanceProfile())); } if (ac.getCloudContext().getLocation().getAvailabilityZone().value() != null) { parameters.add(new Parameter().withParameterKey("AvailabilitySet") .withParameterValue(ac.getCloudContext().getLocation().getAvailabilityZone().value())); } if (awsNetworkView.isExistingVPC()) { parameters.add(new Parameter().withParameterKey("VPCId").withParameterValue(awsNetworkView.getExistingVPC())); if (awsNetworkView.isExistingIGW()) { parameters.add(new Parameter().withParameterKey("InternetGatewayId").withParameterValue(awsNetworkView.getExistingIGW())); } if (awsNetworkView.isExistingSubnet()) { parameters.add(new Parameter().withParameterKey("SubnetId").withParameterValue(awsNetworkView.getExistingSubnet())); } else { parameters.add(new Parameter().withParameterKey("SubnetCIDR").withParameterValue(newSubnetCidr)); } } return parameters; } private List<String> getExistingSubnetCidr(AuthenticatedContext ac, CloudStack stack) { AwsNetworkView awsNetworkView = new AwsNetworkView(stack.getNetwork()); String region = ac.getCloudContext().getLocation().getRegion().value(); AmazonEC2Client ec2Client = awsClient.createAccess(new AwsCredentialView(ac.getCloudCredential()), region); DescribeSubnetsRequest subnetsRequest = new DescribeSubnetsRequest().withSubnetIds(awsNetworkView.getSubnetList()); List<Subnet> subnets = ec2Client.describeSubnets(subnetsRequest).getSubnets(); if (subnets.isEmpty()) { throw new CloudConnectorException("The specified subnet does not exist (maybe it's in a different region)."); } List<String> cidrs = Lists.newArrayList(); for (Subnet subnet : subnets) { cidrs.add(subnet.getCidrBlock()); } return cidrs; } private String getRootDeviceName(AuthenticatedContext ac, CloudStack cloudStack) { AmazonEC2Client ec2Client = awsClient.createAccess(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); DescribeImagesResult images = ec2Client.describeImages(new DescribeImagesRequest().withImageIds(cloudStack.getImage().getImageName())); if (images.getImages().isEmpty()) { throw new CloudConnectorException(String.format("AMI is not available: '%s'.", cloudStack.getImage().getImageName())); } Image image = images.getImages().get(0); if (image == null) { throw new CloudConnectorException(String.format("Couldn't describe AMI '%s'.", cloudStack.getImage().getImageName())); } return image.getRootDeviceName(); } @Override public List<CloudResourceStatus> check(AuthenticatedContext authenticatedContext, List<CloudResource> resources) { return new ArrayList<>(); } @Override public List<CloudResourceStatus> terminate(AuthenticatedContext ac, CloudStack stack, List<CloudResource> resources) { LOGGER.info("Deleting stack: {}", ac.getCloudContext().getId()); AwsCredentialView credentialView = new AwsCredentialView(ac.getCloudCredential()); String regionName = ac.getCloudContext().getLocation().getRegion().value(); if (resources != null && !resources.isEmpty()) { AmazonCloudFormationClient cfClient = awsClient.createCloudFormationClient(credentialView, regionName); String cFStackName = getCloudFormationStackResource(resources).getName(); LOGGER.info("Deleting CloudFormation stack for stack: {} [cf stack id: {}]", cFStackName, ac.getCloudContext().getId()); DescribeStacksRequest describeStacksRequest = new DescribeStacksRequest().withStackName(cFStackName); try { cfClient.describeStacks(describeStacksRequest); } catch (AmazonServiceException e) { if (e.getErrorMessage().contains(cFStackName + " does not exist")) { AmazonEC2Client amazonEC2Client = awsClient.createAccess(credentialView, regionName); releaseReservedIp(amazonEC2Client, resources); return Collections.emptyList(); } else { throw e; } } resumeAutoScalingPolicies(ac, stack); DeleteStackRequest deleteStackRequest = new DeleteStackRequest().withStackName(cFStackName); cfClient.deleteStack(deleteStackRequest); PollTask<Boolean> task = awsPollTaskFactory.newAwsTerminateStackStatusCheckerTask(ac, cfClient, DELETE_COMPLETE, DELETE_FAILED, ERROR_STATUSES, cFStackName); try { Boolean statePollerResult = task.call(); if (!task.completed(statePollerResult)) { syncPollingScheduler.schedule(task); } } catch (Exception e) { throw new CloudConnectorException(e.getMessage(), e); } AmazonEC2Client amazonEC2Client = awsClient.createAccess(credentialView, regionName); releaseReservedIp(amazonEC2Client, resources); } else { AmazonEC2Client amazonEC2Client = awsClient.createAccess(credentialView, regionName); releaseReservedIp(amazonEC2Client, resources); LOGGER.info("No CloudFormation stack saved for stack."); } return check(ac, resources); } private void resumeAutoScalingPolicies(AuthenticatedContext ac, CloudStack stack) { for (Group instanceGroup : stack.getGroups()) { try { String asGroupName = cfStackUtil.getAutoscalingGroupName(ac, instanceGroup.getName(), ac.getCloudContext().getLocation().getRegion().value()); if (asGroupName != null) { AmazonAutoScalingClient amazonASClient = awsClient.createAutoScalingClient(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); List<AutoScalingGroup> asGroups = amazonASClient.describeAutoScalingGroups(new DescribeAutoScalingGroupsRequest() .withAutoScalingGroupNames(asGroupName)).getAutoScalingGroups(); if (!asGroups.isEmpty()) { if (!asGroups.get(0).getSuspendedProcesses().isEmpty()) { amazonASClient.updateAutoScalingGroup(new UpdateAutoScalingGroupRequest() .withAutoScalingGroupName(asGroupName) .withMinSize(0) .withDesiredCapacity(0)); amazonASClient.resumeProcesses(new ResumeProcessesRequest().withAutoScalingGroupName(asGroupName)); } } } else { LOGGER.info("Autoscaling Group's physical id is null (the resource doesn't exist), it is not needed to resume scaling policies."); } } catch (AmazonServiceException e) { if (e.getErrorMessage().matches("Resource.*does not exist for stack.*") || e.getErrorMessage().matches("Stack '.*' does not exist.*")) { LOGGER.info(e.getMessage()); } else { throw e; } } } } private void releaseReservedIp(AmazonEC2Client client, List<CloudResource> resources) { CloudResource elasticIpResource = getReservedIp(resources); if (elasticIpResource != null && elasticIpResource.getName() != null) { Address address; try { DescribeAddressesResult describeResult = client.describeAddresses( new DescribeAddressesRequest().withAllocationIds(elasticIpResource.getName())); address = describeResult.getAddresses().get(0); } catch (AmazonServiceException e) { if (e.getErrorMessage().equals("The allocation ID '" + elasticIpResource.getName() + "' does not exist")) { LOGGER.warn("Elastic IP with allocation ID '{}' not found. Ignoring IP release."); return; } else { throw e; } } if (address.getAssociationId() != null) { client.disassociateAddress(new DisassociateAddressRequest().withAssociationId(elasticIpResource.getName())); } client.releaseAddress(new ReleaseAddressRequest().withAllocationId(elasticIpResource.getName())); } } @Override public List<CloudResourceStatus> update(AuthenticatedContext authenticatedContext, CloudStack stack, List<CloudResource> resources) { return new ArrayList<>(); } @Override public List<CloudResourceStatus> upscale(AuthenticatedContext ac, CloudStack stack, List<CloudResource> resources) { resumeAutoScaling(ac, stack); AmazonAutoScalingClient amazonASClient = awsClient.createAutoScalingClient(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); AmazonCloudFormationClient cloudFormationClient = awsClient.createCloudFormationClient(new AwsCredentialView(ac.getCloudCredential()), ac.getCloudContext().getLocation().getRegion().value()); for (Group group : stack.getGroups()) { String asGroupName = cfStackUtil.getAutoscalingGroupName(ac, cloudFormationClient, group.getName()); amazonASClient.updateAutoScalingGroup(new UpdateAutoScalingGroupRequest() .withAutoScalingGroupName(asGroupName) .withMaxSize(group.getInstancesSize()) .withDesiredCapacity(group.getInstancesSize())); LOGGER.info("Updated Auto Scaling group's desiredCapacity: [stack: '{}', to: '{}']", ac.getCloudContext().getId(), resources.size()); } scheduleStatusChecks(stack, ac, cloudFormationClient); suspendAutoScaling(ac, stack); return singletonList(new CloudResourceStatus(getCloudFormationStackResource(resources), ResourceStatus.UPDATED)); } @Override public Object collectResourcesToRemove(AuthenticatedContext authenticatedContext, CloudStack stack, List<CloudResource> resources, List<CloudInstance> vms) { return null; } @Override public List<CloudResourceStatus> downscale(AuthenticatedContext auth, CloudStack stack, List<CloudResource> resources, List<CloudInstance> vms, Object resourcesToRemove) { List<String> instanceIds = new ArrayList<>(); for (CloudInstance vm : vms) { instanceIds.add(vm.getInstanceId()); } String asGroupName = cfStackUtil.getAutoscalingGroupName(auth, vms.get(0).getTemplate().getGroupName(), auth.getCloudContext().getLocation().getRegion().value()); DetachInstancesRequest detachInstancesRequest = new DetachInstancesRequest().withAutoScalingGroupName(asGroupName).withInstanceIds(instanceIds) .withShouldDecrementDesiredCapacity(true); AmazonAutoScalingClient amazonASClient = awsClient.createAutoScalingClient(new AwsCredentialView(auth.getCloudCredential()), auth.getCloudContext().getLocation().getRegion().value()); try { amazonASClient.detachInstances(detachInstancesRequest); } catch (AmazonServiceException e) { if (!"ValidationError".equals(e.getErrorCode()) || !e.getErrorMessage().contains("not part of Auto Scaling") || instanceIds.stream().anyMatch(id -> !e.getErrorMessage().contains(id))) { throw e; } LOGGER.info(e.getErrorMessage()); } AmazonEC2Client amazonEC2Client = awsClient.createAccess(new AwsCredentialView(auth.getCloudCredential()), auth.getCloudContext().getLocation().getRegion().value()); try { amazonEC2Client.terminateInstances(new TerminateInstancesRequest().withInstanceIds(instanceIds)); } catch (AmazonServiceException e) { if (!"InvalidInstanceID.NotFound".equals(e.getErrorCode())) { throw e; } LOGGER.info(e.getErrorMessage()); } LOGGER.info("Terminated instances in stack '{}': '{}'", auth.getCloudContext().getId(), instanceIds); return check(auth, resources); } @Override public TlsInfo getTlsInfo(AuthenticatedContext authenticatedContext, CloudStack cloudStack) { Network network = cloudStack.getNetwork(); AwsNetworkView networkView = new AwsNetworkView(network); boolean sameVPC = deployingToSameVPC(networkView, networkView.isExistingVPC()); return new TlsInfo(sameVPC); } @Override public String getStackTemplate() throws TemplatingDoesNotSupportedException { try { return freemarkerConfiguration.getTemplate(awsCloudformationTemplatePath, "UTF-8").toString(); } catch (IOException e) { throw new CloudConnectorException("can't get freemarker template", e); } } private void scheduleStatusChecks(CloudStack stack, AuthenticatedContext ac, AmazonCloudFormationClient cloudFormationClient) { for (Group group : stack.getGroups()) { String asGroupName = cfStackUtil.getAutoscalingGroupName(ac, cloudFormationClient, group.getName()); LOGGER.info("Polling Auto Scaling group until new instances are ready. [stack: {}, asGroup: {}]", ac.getCloudContext().getId(), asGroupName); PollTask<Boolean> task = awsPollTaskFactory.newASGroupStatusCheckerTask(ac, asGroupName, group.getInstancesSize(), awsClient, cfStackUtil); try { Boolean statePollerResult = task.call(); if (!task.completed(statePollerResult)) { syncPollingScheduler.schedule(task); } } catch (Exception e) { throw new CloudConnectorException(e.getMessage(), e); } } } private CloudResource getCloudFormationStackResource(List<CloudResource> cloudResources) { for (CloudResource cloudResource : cloudResources) { if (cloudResource.getType().equals(ResourceType.CLOUDFORMATION_STACK)) { return cloudResource; } } return null; } private CloudResource getReservedIp(List<CloudResource> cloudResources) { for (CloudResource cloudResource : cloudResources) { if (cloudResource.getType().equals(ResourceType.AWS_RESERVED_IP)) { return cloudResource; } } return null; } protected String findNonOverLappingCIDR(AuthenticatedContext ac, CloudStack stack) { AwsNetworkView awsNetworkView = new AwsNetworkView(stack.getNetwork()); String region = ac.getCloudContext().getLocation().getRegion().value(); AmazonEC2Client ec2Client = awsClient.createAccess(new AwsCredentialView(ac.getCloudCredential()), region); DescribeVpcsRequest vpcRequest = new DescribeVpcsRequest().withVpcIds(awsNetworkView.getExistingVPC()); Vpc vpc = ec2Client.describeVpcs(vpcRequest).getVpcs().get(0); String vpcCidr = vpc.getCidrBlock(); LOGGER.info("Subnet cidr is empty, find a non-overlapping subnet for VPC cidr: {}", vpcCidr); DescribeSubnetsRequest request = new DescribeSubnetsRequest().withFilters(new Filter("vpc-id", singletonList(awsNetworkView.getExistingVPC()))); List<Subnet> awsSubnets = ec2Client.describeSubnets(request).getSubnets(); List<String> subnetCidrs = awsSubnets.stream().map(Subnet::getCidrBlock).collect(Collectors.toList()); LOGGER.info("The selected VPCs: {}, has the following subnets: {}", vpc.getVpcId(), subnetCidrs.stream().collect(Collectors.joining(","))); return calculateSubnet(ac.getCloudContext().getName(), vpc, subnetCidrs); } private String calculateSubnet(String stackName, Vpc vpc, List<String> subnetCidrs) { SubnetUtils.SubnetInfo vpcInfo = new SubnetUtils(vpc.getCidrBlock()).getInfo(); String[] cidrParts = vpcInfo.getCidrSignature().split("/"); int netmask = Integer.valueOf(cidrParts[cidrParts.length - 1]); int netmaskBits = CIDR_PREFIX - netmask; if (netmaskBits <= 0) { throw new CloudConnectorException("The selected VPC has to be in a bigger CIDR range than /24"); } int numberOfSubnets = Double.valueOf(Math.pow(2, netmaskBits)).intValue(); int targetSubnet = 0; if (stackName != null) { byte[] b = stackName.getBytes(Charset.forName("UTF-8")); for (byte ascii : b) { targetSubnet += ascii; } } targetSubnet = Long.valueOf(targetSubnet % numberOfSubnets).intValue(); String cidr = getSubnetCidrInRange(vpc, subnetCidrs, targetSubnet, numberOfSubnets); if (cidr == null) { cidr = getSubnetCidrInRange(vpc, subnetCidrs, 0, targetSubnet); } if (cidr == null) { throw new CloudConnectorException("Cannot find non-overlapping CIDR range"); } return cidr; } private String getSubnetCidrInRange(Vpc vpc, List<String> subnetCidrs, int start, int end) { SubnetUtils.SubnetInfo vpcInfo = new SubnetUtils(vpc.getCidrBlock()).getInfo(); String lowProbe = incrementIp(vpcInfo.getLowAddress()); String highProbe = new SubnetUtils(toSubnetCidr(lowProbe)).getInfo().getHighAddress(); // start from the target subnet for (int i = 0; i < start - 1; i++) { lowProbe = incrementIp(lowProbe); highProbe = incrementIp(highProbe); } boolean foundProbe = false; for (int i = start; i < end; i++) { boolean overlapping = false; for (String subnetCidr : subnetCidrs) { SubnetUtils.SubnetInfo subnetInfo = new SubnetUtils(subnetCidr).getInfo(); if (isInRange(lowProbe, subnetInfo) || isInRange(highProbe, subnetInfo)) { overlapping = true; break; } } if (overlapping) { lowProbe = incrementIp(lowProbe); highProbe = incrementIp(highProbe); } else { foundProbe = true; break; } } if (foundProbe && isInRange(highProbe, vpcInfo)) { String subnet = toSubnetCidr(lowProbe); LOGGER.info("The following subnet cidr found: {} for VPC: {}", subnet, vpc.getVpcId()); return subnet; } else { return null; } } private String toSubnetCidr(String ip) { int ipValue = InetAddresses.coerceToInteger(InetAddresses.forString(ip)) - 1; return InetAddresses.fromInteger(ipValue).getHostAddress() + "/24"; } private String incrementIp(String ip) { int ipValue = InetAddresses.coerceToInteger(InetAddresses.forString(ip)) + INCREMENT_HOST_NUM; return InetAddresses.fromInteger(ipValue).getHostAddress(); } private boolean isInRange(String address, SubnetUtils.SubnetInfo subnetInfo) { int low = InetAddresses.coerceToInteger(InetAddresses.forString(subnetInfo.getLowAddress())); int high = InetAddresses.coerceToInteger(InetAddresses.forString(subnetInfo.getHighAddress())); int currentAddress = InetAddresses.coerceToInteger(InetAddresses.forString(address)); return low <= currentAddress && currentAddress <= high; } }