package com.sequenceiq.cloudbreak.cloud.aws; import java.net.URLDecoder; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.ec2.AmazonEC2Client; import com.amazonaws.services.ec2.model.DescribeInternetGatewaysRequest; import com.amazonaws.services.ec2.model.DescribeInternetGatewaysResult; import com.amazonaws.services.ec2.model.DescribeKeyPairsRequest; import com.amazonaws.services.ec2.model.DescribeKeyPairsResult; import com.amazonaws.services.ec2.model.DescribeSubnetsRequest; import com.amazonaws.services.ec2.model.DescribeSubnetsResult; import com.amazonaws.services.ec2.model.DescribeVpcAttributeRequest; import com.amazonaws.services.ec2.model.DescribeVpcAttributeResult; import com.amazonaws.services.ec2.model.InstanceType; import com.amazonaws.services.ec2.model.InternetGateway; import com.amazonaws.services.ec2.model.InternetGatewayAttachment; import com.amazonaws.services.ec2.model.RunInstancesRequest; import com.amazonaws.services.ec2.model.Subnet; import com.amazonaws.services.ec2.model.VpcAttributeName; import com.amazonaws.services.identitymanagement.AmazonIdentityManagement; import com.amazonaws.services.identitymanagement.model.AttachedPolicy; import com.amazonaws.services.identitymanagement.model.GetPolicyRequest; import com.amazonaws.services.identitymanagement.model.GetPolicyResult; import com.amazonaws.services.identitymanagement.model.GetRolePolicyRequest; import com.amazonaws.services.identitymanagement.model.GetRolePolicyResult; import com.amazonaws.services.identitymanagement.model.GetRoleRequest; import com.amazonaws.services.identitymanagement.model.ListAttachedRolePoliciesRequest; import com.amazonaws.services.identitymanagement.model.ListAttachedRolePoliciesResult; import com.amazonaws.services.identitymanagement.model.ListRolePoliciesRequest; import com.amazonaws.services.identitymanagement.model.ListRolePoliciesResult; import com.fasterxml.jackson.databind.JsonNode; import com.sequenceiq.cloudbreak.cloud.Setup; import com.sequenceiq.cloudbreak.cloud.aws.view.AwsCredentialView; import com.sequenceiq.cloudbreak.cloud.aws.view.AwsInstanceProfileView; import com.sequenceiq.cloudbreak.cloud.aws.view.AwsInstanceView; 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.model.CloudCredential; import com.sequenceiq.cloudbreak.cloud.model.CloudStack; import com.sequenceiq.cloudbreak.cloud.model.FileSystem; import com.sequenceiq.cloudbreak.cloud.model.Group; import com.sequenceiq.cloudbreak.cloud.model.Image; import com.sequenceiq.cloudbreak.cloud.notification.PersistenceNotifier; import com.sequenceiq.cloudbreak.common.type.ImageStatus; import com.sequenceiq.cloudbreak.common.type.ImageStatusResult; import com.sequenceiq.cloudbreak.util.JsonUtil; @Component public class AwsSetup implements Setup { private static final Logger LOGGER = LoggerFactory.getLogger(AwsSetup.class); private static final String IGW_DOES_NOT_EXIST_MSG = "The given internet gateway '%s' does not exist or belongs to a different region."; private static final String SUBNET_DOES_NOT_EXIST_MSG = "The given subnet '%s' does not exist or belongs to a different region."; private static final String SUBNETVPC_DOES_NOT_EXIST_MSG = "The given subnet '%s' does not belong to the given VPC '%s'."; private static final String IGWVPC_DOES_NOT_EXIST_MSG = "The given internet gateway '%s' does not belong to the given VPC '%s'."; private static final String IMAGE_OPT_IN_REQUIRED_MSG = "Unable to create cluster because AWS Marketplace subscription to the Hortonworks Data Cloud" + " HDP Services is required. In order to create a cluster, you need to accept terms and subscribe to the AWS Marketplace product."; private static final String LINK_TO_MARKETPLACE_MSG = "To do so please visit "; private static final String MARKETPLACE_HTTP_LINK = "http://aws.amazon.com/marketplace"; private static final int FINISHED_PROGRESS_VALUE = 100; private static final int UNAUTHORIZED = 403; @Value("${cb.aws.spotinstances.enabled:}") private boolean awsSpotinstanceEnabled; @Inject private CloudFormationStackUtil cfStackUtil; @Inject private AwsClient awsClient; @Override public ImageStatusResult checkImageStatus(AuthenticatedContext authenticatedContext, CloudStack stack, Image image) { return new ImageStatusResult(ImageStatus.CREATE_FINISHED, FINISHED_PROGRESS_VALUE); } @Override public void prepareImage(AuthenticatedContext authenticatedContext, CloudStack stack, Image image) { LOGGER.debug("prepare image has been executed"); } @Override public void prerequisites(AuthenticatedContext ac, CloudStack stack, PersistenceNotifier persistenceNotifier) { AwsNetworkView awsNetworkView = new AwsNetworkView(stack.getNetwork()); AwsCredentialView credentialView = new AwsCredentialView(ac.getCloudCredential()); String region = ac.getCloudContext().getLocation().getRegion().value(); verifySpotInstances(stack); AwsCredentialView awsCredentialView = new AwsCredentialView(ac.getCloudCredential()); AwsInstanceProfileView awsInstanceProfileView = new AwsInstanceProfileView(stack); validateImageOptIn(awsCredentialView, region, stack.getImage().getImageName()); if (awsClient.roleBasedCredential(awsCredentialView) && awsInstanceProfileView.isCreateInstanceProfile()) { validateInstanceProfileCreation(awsCredentialView); } if (awsNetworkView.isExistingVPC()) { try { AmazonEC2Client amazonEC2Client = awsClient.createAccess(credentialView, region); validateExistingVpc(awsNetworkView, amazonEC2Client); validateExistingIGW(awsNetworkView, amazonEC2Client); validateExistingSubnet(awsNetworkView, amazonEC2Client); } catch (AmazonServiceException e) { throw new CloudConnectorException(e.getErrorMessage()); } catch (AmazonClientException e) { throw new CloudConnectorException(e.getMessage()); } } validateExistingKeyPair(ac, credentialView, region); LOGGER.debug("setup has been executed"); } private void verifySpotInstances(CloudStack stack) { if (!awsSpotinstanceEnabled) { for (Group group : stack.getGroups()) { if (group.getInstances() != null && !group.getInstances().isEmpty() && new AwsInstanceView(group.getReferenceInstanceConfiguration().getTemplate()).getSpotPrice() != null) { throw new CloudConnectorException(String.format("Spot instances are not supported on this AMI: %s", stack.getImage())); } } } } private void validateImageOptIn(AwsCredentialView credentialView, String region, String imageName) { try { AmazonEC2Client amazonEC2Client = awsClient.createAccess(credentialView, region); RunInstancesRequest request = new RunInstancesRequest() .withMinCount(1) .withMaxCount(1) .withImageId(imageName) .withInstanceType(InstanceType.M3Large); amazonEC2Client.dryRun(request); LOGGER.info("Dry run succeeded, AMI '{}' is safe to launch.", imageName); } catch (AmazonServiceException e) { String errorMessage = e.getErrorMessage(); if (e.getErrorCode().equals("OptInRequired")) { int marketplaceLinkIndex = errorMessage.indexOf(MARKETPLACE_HTTP_LINK); if (marketplaceLinkIndex != -1) { errorMessage = IMAGE_OPT_IN_REQUIRED_MSG + " " + LINK_TO_MARKETPLACE_MSG + errorMessage.substring(marketplaceLinkIndex); } else { errorMessage = IMAGE_OPT_IN_REQUIRED_MSG; } throw new CloudConnectorException(errorMessage, e); } else { LOGGER.error(String.format("Image opt-in could not be validated for AMI '%s'.", imageName), e); } } } private void validateInstanceProfileCreation(AwsCredentialView awsCredentialView) { GetRoleRequest roleRequest = new GetRoleRequest(); String roleName = awsCredentialView.getRoleArn().split("/")[1]; LOGGER.info("Start validate {} role for S3 access.", roleName); roleRequest.withRoleName(roleName); AmazonIdentityManagement client = awsClient.createAmazonIdentityManagement(awsCredentialView); try { ListRolePoliciesRequest listRolePoliciesRequest = new ListRolePoliciesRequest(); listRolePoliciesRequest.setRoleName(roleName); ListRolePoliciesResult listRolePoliciesResult = client.listRolePolicies(listRolePoliciesRequest); for (String s : listRolePoliciesResult.getPolicyNames()) { if (checkIamOrS3Statement(roleName, client, s)) { LOGGER.info("Validation successful for s3 or iam access."); return; } } ListAttachedRolePoliciesRequest listAttachedRolePoliciesRequest = new ListAttachedRolePoliciesRequest(); listAttachedRolePoliciesRequest.setRoleName(roleName); ListAttachedRolePoliciesResult listAttachedRolePoliciesResult = client.listAttachedRolePolicies(listAttachedRolePoliciesRequest); for (AttachedPolicy attachedPolicy : listAttachedRolePoliciesResult.getAttachedPolicies()) { if (checkIamOrS3Access(client, attachedPolicy)) { LOGGER.info("Validation successful for s3 or iam access."); return; } } } catch (AmazonServiceException ase) { if (ase.getStatusCode() == UNAUTHORIZED) { String policyMEssage = "Could not get policies on the role because the arn role do not have enough permission: %s"; LOGGER.info(String.format(policyMEssage, ase.getErrorMessage())); throw new CloudConnectorException(String.format(policyMEssage, ase.getErrorMessage())); } else { LOGGER.info(ase.getMessage()); throw new CloudConnectorException(ase.getErrorMessage()); } } catch (Exception e) { LOGGER.info(e.getMessage()); throw new CloudConnectorException(e.getMessage()); } LOGGER.info("Could not get policies on the role because the arn role do not have enough permission."); throw new CloudConnectorException("Could not get policies on the role because the arn role do not have enough permission."); } private boolean checkIamOrS3Statement(String roleName, AmazonIdentityManagement client, String s) throws Exception { GetRolePolicyRequest getRolePolicyRequest = new GetRolePolicyRequest(); getRolePolicyRequest.setRoleName(roleName); getRolePolicyRequest.setPolicyName(s); GetRolePolicyResult rolePolicy = client.getRolePolicy(getRolePolicyRequest); String decode = URLDecoder.decode(rolePolicy.getPolicyDocument(), "UTF-8"); JsonNode object = JsonUtil.readTree(decode); JsonNode statement = object.get("Statement"); for (int i = 0; i < statement.size(); i++) { JsonNode action = statement.get(i).get("Action"); for (int j = 0; j < action.size(); j++) { String actionEntry = action.get(j).textValue().replaceAll(" ", "").toLowerCase(); if ("iam:createrole".equals(actionEntry) || "iam:*".equals(actionEntry)) { LOGGER.info("Role has able to operate on iam resources: {}.", action.get(j).toString()); return true; } } } return false; } private boolean checkIamOrS3Access(AmazonIdentityManagement client, AttachedPolicy attachedPolicy) { GetPolicyRequest getRolePolicyRequest = new GetPolicyRequest(); getRolePolicyRequest.setPolicyArn(attachedPolicy.getPolicyArn()); GetPolicyResult policy = client.getPolicy(getRolePolicyRequest); if (policy.getPolicy().getArn().toLowerCase().contains("iam")) { LOGGER.info("Role has policy for iam resources: {}.", policy.getPolicy().getArn()); return true; } return false; } private void validateExistingVpc(AwsNetworkView awsNetworkView, AmazonEC2Client amazonEC2Client) { DescribeVpcAttributeRequest describeVpcAttributeRequest = new DescribeVpcAttributeRequest(); describeVpcAttributeRequest.withVpcId(awsNetworkView.getExistingVPC()); boolean dnsSupported = isDnsSupported(amazonEC2Client, describeVpcAttributeRequest); boolean hostnameSupported = checkHostnameSupport(amazonEC2Client, describeVpcAttributeRequest); if (!dnsSupported || !hostnameSupported) { throw new CloudConnectorException("Please enable both DNS resolution and DNS hostnames in existing VPC: " + awsNetworkView.getExistingVPC()); } } private boolean isDnsSupported(AmazonEC2Client amazonEC2Client, DescribeVpcAttributeRequest describeVpcAttributeRequest) { describeVpcAttributeRequest.withAttribute(VpcAttributeName.EnableDnsSupport); DescribeVpcAttributeResult describeVpcAttributeResult = amazonEC2Client.describeVpcAttribute(describeVpcAttributeRequest); return describeVpcAttributeResult.getEnableDnsSupport(); } private boolean checkHostnameSupport(AmazonEC2Client amazonEC2Client, DescribeVpcAttributeRequest describeVpcAttributeRequest) { describeVpcAttributeRequest.withAttribute(VpcAttributeName.EnableDnsHostnames); DescribeVpcAttributeResult describeVpcAttributeResult = amazonEC2Client.describeVpcAttribute(describeVpcAttributeRequest); return describeVpcAttributeResult.getEnableDnsHostnames(); } private void validateExistingSubnet(AwsNetworkView awsNetworkView, AmazonEC2Client amazonEC2Client) { if (awsNetworkView.isExistingSubnet()) { DescribeSubnetsRequest describeSubnetsRequest = new DescribeSubnetsRequest(); describeSubnetsRequest.withSubnetIds(awsNetworkView.getSubnetList()); DescribeSubnetsResult describeSubnetsResult = amazonEC2Client.describeSubnets(describeSubnetsRequest); if (describeSubnetsResult.getSubnets().size() < awsNetworkView.getSubnetList().size()) { throw new CloudConnectorException(String.format(SUBNET_DOES_NOT_EXIST_MSG, awsNetworkView.getExistingSubnet())); } else { for (Subnet subnet : describeSubnetsResult.getSubnets()) { String vpcId = subnet.getVpcId(); if (vpcId != null && !vpcId.equals(awsNetworkView.getExistingVPC())) { throw new CloudConnectorException(String.format(SUBNETVPC_DOES_NOT_EXIST_MSG, awsNetworkView.getExistingSubnet(), awsNetworkView.getExistingVPC())); } } } } } private void validateExistingIGW(AwsNetworkView awsNetworkView, AmazonEC2Client amazonEC2Client) { if (awsNetworkView.isExistingIGW()) { DescribeInternetGatewaysRequest describeInternetGatewaysRequest = new DescribeInternetGatewaysRequest(); describeInternetGatewaysRequest.withInternetGatewayIds(awsNetworkView.getExistingIGW()); DescribeInternetGatewaysResult describeInternetGatewaysResult = amazonEC2Client.describeInternetGateways(describeInternetGatewaysRequest); if (describeInternetGatewaysResult.getInternetGateways().size() < 1) { throw new CloudConnectorException(String.format(IGW_DOES_NOT_EXIST_MSG, awsNetworkView.getExistingIGW())); } else { InternetGateway internetGateway = describeInternetGatewaysResult.getInternetGateways().get(0); InternetGatewayAttachment attachment = internetGateway.getAttachments().get(0); if (attachment != null && !attachment.getVpcId().equals(awsNetworkView.getExistingVPC())) { throw new CloudConnectorException(String.format(IGWVPC_DOES_NOT_EXIST_MSG, awsNetworkView.getExistingIGW(), awsNetworkView.getExistingVPC())); } } } } @Override public void validateFileSystem(CloudCredential credential, FileSystem fileSystem) throws Exception { } private void validateExistingKeyPair(AuthenticatedContext authenticatedContext, AwsCredentialView credentialView, String region) { String keyPairName = awsClient.getExistingKeyPairName(authenticatedContext); if (StringUtils.isNoneEmpty(keyPairName)) { boolean keyPairIsPresentOnEC2 = false; try { AmazonEC2Client client = awsClient.createAccess(credentialView, region); DescribeKeyPairsResult describeKeyPairsResult = client.describeKeyPairs(new DescribeKeyPairsRequest().withKeyNames(keyPairName)); keyPairIsPresentOnEC2 = describeKeyPairsResult.getKeyPairs().stream().findFirst().isPresent(); } catch (Exception e) { String errorMessage = String.format("Failed to get the key pair [name: '%s'] from EC2 [roleArn:'%s'], detailed message: %s.", keyPairName, credentialView.getRoleArn(), e.getMessage()); LOGGER.error(errorMessage, e); } if (!keyPairIsPresentOnEC2) { throw new CloudConnectorException(String.format("The key pair '%s' could not be found in the '%s' region of EC2.", keyPairName, region)); } } } }