/******************************************************************************* * Copyright (c) 2013 GigaSpaces Technologies Ltd. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. ******************************************************************************/ package org.cloudifysource.esc.driver.provisioning.privateEc2; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.Socket; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.CharEncoding; import org.apache.commons.lang.StringUtils; import org.cloudifysource.domain.cloud.Cloud; import org.cloudifysource.domain.cloud.CloudUser; import org.cloudifysource.domain.cloud.ScriptLanguages; import org.cloudifysource.domain.cloud.compute.ComputeTemplate; import org.cloudifysource.esc.driver.provisioning.CloudDriverSupport; import org.cloudifysource.esc.driver.provisioning.CloudProvisioningException; import org.cloudifysource.esc.driver.provisioning.CustomServiceDataAware; import org.cloudifysource.esc.driver.provisioning.MachineDetails; import org.cloudifysource.esc.driver.provisioning.ManagementProvisioningContext; import org.cloudifysource.esc.driver.provisioning.ProvisioningContext; import org.cloudifysource.esc.driver.provisioning.ProvisioningContextAccess; import org.cloudifysource.esc.driver.provisioning.ProvisioningContextImpl; import org.cloudifysource.esc.driver.provisioning.ProvisioningDriver; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.ParserUtils; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.PrivateEc2ParserException; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.AWSEC2Instance; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.AWSEC2Volume; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.InstanceProperties; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.PrivateEc2Template; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.VolumeMapping; import org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.VolumeProperties; import org.cloudifysource.esc.util.TarGzUtils; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; import com.amazonaws.Protocol; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.regions.Region; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; import com.amazonaws.services.ec2.model.BlockDeviceMapping; import com.amazonaws.services.ec2.model.CreateTagsRequest; import com.amazonaws.services.ec2.model.DescribeInstancesRequest; import com.amazonaws.services.ec2.model.DescribeInstancesResult; import com.amazonaws.services.ec2.model.DescribeTagsRequest; import com.amazonaws.services.ec2.model.DescribeTagsResult; import com.amazonaws.services.ec2.model.DescribeVolumesRequest; import com.amazonaws.services.ec2.model.DescribeVolumesResult; import com.amazonaws.services.ec2.model.EbsBlockDevice; import com.amazonaws.services.ec2.model.Filter; import com.amazonaws.services.ec2.model.GetConsoleOutputRequest; import com.amazonaws.services.ec2.model.GetConsoleOutputResult; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.Placement; import com.amazonaws.services.ec2.model.Reservation; import com.amazonaws.services.ec2.model.RunInstancesRequest; import com.amazonaws.services.ec2.model.RunInstancesResult; import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.ec2.model.TagDescription; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import com.amazonaws.services.ec2.model.TerminateInstancesResult; import com.amazonaws.services.ec2.model.Volume; import com.amazonaws.services.s3.model.S3Object; /** * A custom Cloud Driver to provision Amazon EC2 machines using cloud formation templates.<br /> * This driver can still start machines the usual way using cloudify groovy templates. * * @author victor * @since 2.7.0 * */ public class PrivateEC2CloudifyDriver extends CloudDriverSupport implements ProvisioningDriver, CustomServiceDataAware { /** * Runnable to retrieve console output from a EC2 instance. */ private class EC2Console implements Runnable { private AmazonEC2 ec2; private int nbAlreadyReadLines = 0; private String ipAddress; private int agentPort; private String instanceId; private String logHeader; private boolean loop = true; public EC2Console(final String instanceId, final String ipAddress, final int agentPort) throws CloudProvisioningException { this.ipAddress = ipAddress; this.agentPort = agentPort; this.instanceId = instanceId; this.logHeader = "[" + instanceId + "] "; this.ec2 = createAmazonEC2Client(cloud, getManagerComputeTemplate()); } @Override public void run() { if (logger.isLoggable(Level.FINER)) { logger.finer(logHeader + "Getting console ouput..."); } while (!Thread.interrupted() && loop) { try { sleep(); // If the agent is started, stop the tail if (isPortReachable(ipAddress, agentPort)) { if (logger.isLoggable(Level.FINEST)) { logger.finest(logHeader + "Stopping the loop..."); } loop = false; } // Read the console output final String read = this.readEc2Output(); if (read != null) { // Print the console output if there is something to print this.printOutput(read); } } catch (final IllegalStateException e) { if ("Connection pool shut down".equals(e.getMessage())) { if (logger.isLoggable(Level.FINEST)) { logger.finest(logHeader + "Getting console ouput..."); } loop = false; } } catch (final Exception e) { logger.log(Level.WARNING, logHeader + "Error occurs when getting console output", e); } } if (logger.isLoggable(Level.FINER)) { logger.finer(logHeader + "Stop scrolling console ouput..."); } } private String readEc2Output() { final GetConsoleOutputRequest request = new GetConsoleOutputRequest(instanceId); final GetConsoleOutputResult consoleOutput = ec2.getConsoleOutput(request); final String output = consoleOutput.getOutput(); if (output != null) { final String x = new String(Base64.decodeBase64(output)); return x; } else { logger.finest("[" + instanceId + "] No output yet"); } return null; } private void printOutput(final String read) { final StringTokenizer st = new StringTokenizer(read, "\n"); final int tokenCount = st.countTokens(); // On linux machines, ec2 returns the full output history. // We have to ignore lines we already read. for (int i = 0; i < this.nbAlreadyReadLines; i++) { st.nextToken(); } if (!st.hasMoreElements()) { logger.finest(logHeader + "No additional output"); } while (st.hasMoreElements()) { logger.info(logHeader + st.nextElement().toString()); } this.nbAlreadyReadLines = tokenCount; } } private static final int NB_THREADS_CONSOLE_OUTPUT = 20; private static final int DEFAULT_CLOUDIFY_AGENT_PORT = 7002; private static final int AMAZON_EXCEPTION_CODE_400 = 400; private static final int MAX_SERVERS_LIMIT = 200; private static final long WAIT_STATUS_SLEEP_TIME = 5000L; private static final String CLOUDIFY_ENV_SCRIPT = "cloudify_env.sh"; private static final String PATTERN_PROPS_JSON = "\\s*\\\"[\\w-]*\\\"\\s*:\\s*([^{(\\[\"][\\w-]+)\\s*,?"; private static final String VOLUME_PREFIX = "cloudify-storage-"; private static final String REGIONS_OVERRIDES_SYSTEM_PROPERTY = "com.amazonaws.regions.RegionUtils.fileOverride"; private static final String REGIONS_FILE = "regionsFile"; /** Key name for amazon tag resource's name. */ private static final String TK_NAME = "Name"; /** * Enumeration for supported 'resource-type' value used in com.amazonaws.services.ec2.model.Filter parameter. */ private static enum TagResourceType { INSTANCE, VOLUME; public String getValue() { return name().toLowerCase(); } } /** Counter for ec2 instances. */ private static AtomicInteger counter = new AtomicInteger(0); /** Counter for storage instances. */ private static AtomicInteger volumeCounter = new AtomicInteger(0); /** Map which contains all parsed CFN template. */ private final Map<String, PrivateEc2Template> cfnTemplatePerService = new HashMap<String, PrivateEc2Template>(); private AmazonEC2 ec2Client; private AmazonS3Uploader amazonS3Uploader; /** short name of the service (i.e without applicationName). */ private String serviceName; private PrivateEc2Template privateEc2Template; private String cloudTemplateName; private String cloudName; private ExecutorService debugExecutors; /** * ***************************************************************************************************************** * * *************************************************************************************************************** * *** */ /** * Sets the custom data file for the cloud driver instance of a specific service.<br /> * <p> * customDataFile is a simple file or a folder like the following: * * <pre> * -- applicationNameFolder --- serviceName1-cfn.template * |- serviceName2-cfn.template * |- serviceName3-cfn.template * </pre> * * </p> * * @param customDataFile * files or directory containing the amazon cloudformation template of a specific service * */ @Override public void setCustomDataFile(final File customDataFile) { logger.info("Received custom data file: " + customDataFile); final Map<String, PrivateEc2Template> map = new HashMap<String, PrivateEc2Template>(); PrivateEc2Template mapJson = null; try { if (customDataFile.isFile()) { final String templateName = this.getTemplatName(customDataFile); logger.fine("Parsing CFN Template for service=" + templateName); mapJson = ParserUtils.mapJson(PrivateEc2Template.class, customDataFile); map.put(templateName, mapJson); } else { final File[] listFiles = customDataFile.listFiles(); if (listFiles != null) { for (final File file : listFiles) { if (this.isTemplateFile(file)) { final String templateName = this.getTemplatName(file); logger.fine("Parsing CFN Template for service=" + templateName); final File pFile = this.getPropertiesFileIfExists(templateName, customDataFile.listFiles()); if (pFile != null) { // Replace properties variable with values if the properties file exists final String templateString = this.replaceProperties(file, pFile); mapJson = ParserUtils.mapJson(PrivateEc2Template.class, templateString); map.put(templateName, mapJson); } else { mapJson = ParserUtils.mapJson(PrivateEc2Template.class, file); map.put(templateName, mapJson); } } } } } } catch (final Exception e) { logger.log(Level.SEVERE, "Couldn't parse the template file: " + customDataFile.getPath()); throw new IllegalStateException(e); } this.cfnTemplatePerService.putAll(map); } private String replaceProperties(final File file, final File propertiesFile) throws IOException { logger.fine("Properties file=" + propertiesFile.getName()); final Properties props = new Properties(); props.load(new FileInputStream(propertiesFile)); String templateString = FileUtils.readFileToString(file); final Pattern p = Pattern.compile(PATTERN_PROPS_JSON); Matcher m = p.matcher(templateString); while (m.find()) { final String group = m.group(); final String group1 = m.group(1); if (props.containsKey(group1)) { final String value = props.getProperty(group1); if (logger.isLoggable(Level.FINEST)) { logger.finest("Replacing property " + group + " by " + value); } templateString = m.replaceFirst(group.replace(group1, value)); m = p.matcher(templateString); } else { throw new IllegalStateException("Couldn't find property: " + group1); } } return templateString; } private File getPropertiesFileIfExists(final String templateName, final File[] listFiles) { final String filename = templateName + "-cfn.properties"; for (final File file : listFiles) { if (filename.equals(file.getName())) { return file; } } return null; } private String getTemplatName(final File file) { String name = file.getName(); name = name.replace("-cfn.template", ""); return name; } private boolean isTemplateFile(final File file) { final String name = file.getName(); return name.endsWith("-cfn.template"); } /** Testing purpose. */ PrivateEc2Template getCFNTemplatePerService(final String serviceName) { return cfnTemplatePerService.get(serviceName); } public Cloud getCloud() { return this.cloud; } /** * ***************************************************************************************************************** */ /* * (non-Javadoc) * * @see * org.cloudifysource.esc.driver.provisioning.BaseProvisioningDriver#setConfig(org.cloudifysource.dsl.cloud.Cloud, * java.lang.String, boolean, java.lang.String) */ @Override public void setConfig(final Cloud cloud, final String cloudTemplateName, final boolean management, final String fullServiceName) { logger.fine("Running path : " + System.getProperty("user.dir")); this.serviceName = this.getSimpleServiceName(fullServiceName); this.cloudTemplateName = cloudTemplateName; this.cloudName = cloud.getName(); super.setConfig(cloud, cloudTemplateName, management, fullServiceName); if (logger.isLoggable(Level.FINER)) { logger.finer("Service name : " + this.serviceName + "(" + fullServiceName + ")"); } try { setRegionsFile(cloud); ComputeTemplate managementTemplate = getManagerComputeTemplate(); // Initialize the ec2 client this.ec2Client = this.createAmazonEC2Client(cloud, managementTemplate); // Initialize the S3 client this.amazonS3Uploader = new AmazonS3Uploader(cloud, managementTemplate); // Setup debug console output final boolean debug = BooleanUtils.toBoolean((String) managementTemplate.getCustom().get("debugMode")); if (debug) { this.debugExecutors = Executors.newFixedThreadPool(NB_THREADS_CONSOLE_OUTPUT); } } catch (final CloudProvisioningException e) { throw new IllegalArgumentException(e); } } private void setPrivateEc2Template() throws CloudProvisioningException { if (!management) { // not management. check if a specific template was set for this service this.privateEc2Template = cfnTemplatePerService.get(this.serviceName); if (this.privateEc2Template != null) { logger.fine("Found service-specific template for service: " + serviceName); return; } } String cfnTemplateFileName = null; File cloudDirectory = null; if (management) { ComputeTemplate computeTemplate = this.getManagerComputeTemplate(); cfnTemplateFileName = (String) computeTemplate.getCustom().get("cfnManagerTemplate"); if (StringUtils.isBlank(cfnTemplateFileName)) { throw new CloudProvisioningException("cfnManagerTemplate value not set on management template"); } cloudDirectory = new ProvisioningContextAccess().getManagementProvisioiningContext().getCloudFile().getParentFile(); } else { logger.fine("Using template: " + cloudTemplateName); ComputeTemplate computeTemplate = cloud.getCloudCompute().getTemplates().get(cloudTemplateName); cfnTemplateFileName = (String) computeTemplate.getCustom().get("cfnTemplate"); if (StringUtils.isBlank(cfnTemplateFileName)) { throw new CloudProvisioningException("cfnTemplate value not set on template: " + cloudTemplateName); } cloudDirectory = new ProvisioningContextAccess().getProvisioiningContext().getCloudFile().getParentFile(); } try { logger.fine("using template: " + cfnTemplateFileName + " from directory: " + cloudDirectory); this.cloud.getCustom().put("###CLOUD_DIRECTORY###", cloudDirectory); this.privateEc2Template = this.getPrivateEc2TemplateFromFile(cloudDirectory, cfnTemplateFileName); } catch (Exception e) { throw new CloudProvisioningException("Failed to read template from file : " + cfnTemplateFileName + ", reported error: " + e.getMessage(), e); } } private void setRegionsFile(final Cloud cloud) { String regionsFileName = ""; if (cloud.getCustom() != null) { regionsFileName = (String) cloud.getCustom().get(REGIONS_FILE); } if (StringUtils.isNotBlank(regionsFileName)) { File regionsFile = new File(regionsFileName); if (regionsFile.isFile()) { logger.info("setting regions overrides file: " + regionsFile.getAbsolutePath()); System.setProperty(REGIONS_OVERRIDES_SYSTEM_PROPERTY, regionsFile.getAbsolutePath()); } else { throw new IllegalArgumentException("Failed to resolve regions file: " + regionsFileName); } } } private ComputeTemplate getManagerComputeTemplate() { final String managementMachineTemplate = this.cloud.getConfiguration().getManagementMachineTemplate(); final ComputeTemplate managerTemplate = this.cloud.getCloudCompute().getTemplates().get(managementMachineTemplate); return managerTemplate; } public PrivateEc2Template getPrivateEc2TemplateFromFile(final File cloudDirectory, final String templateFileName) throws PrivateEc2ParserException, IOException { final File file = new File(cloudDirectory, templateFileName); if (!file.exists()) { throw new IllegalArgumentException("CFN Template not found: " + file.getPath()); } logger.fine("CFN template: " + file.getPath()); final String templateName = this.getTemplatName(file); final File propsFile = new File(file.getParent(), templateName + "-cfn.properties"); logger.fine("Searching for CFN properties: " + file.getPath()); PrivateEc2Template mapJson = null; if (propsFile.exists()) { // Replace properties variable with values if the properties file exists final String templateString = this.replaceProperties(file, propsFile); logger.fine("The merged template:\n" + templateString); mapJson = ParserUtils.mapJson(PrivateEc2Template.class, templateString); } else { mapJson = ParserUtils.mapJson(PrivateEc2Template.class, file); } return mapJson; } /** * Remove application name from the string.<br /> * i.e. if fullServiceName = sampleApplication.someService, it will return someService. * * @param fullServiceName * A service name. * @return The service name shortened by the application name. */ private String getSimpleServiceName(final String fullServiceName) { if (fullServiceName != null && fullServiceName.contains(".")) { return fullServiceName.substring(fullServiceName.lastIndexOf(".") + 1, fullServiceName.length()); } return fullServiceName; } private AmazonEC2 createAmazonEC2Client(final Cloud cloud, final ComputeTemplate managementTemplate) throws CloudProvisioningException { AmazonEC2 ec2; final CloudUser user = cloud.getUser(); final AWSCredentials credentials = new BasicAWSCredentials(user.getUser(), user.getApiKey()); String ec2LocationId = managementTemplate.getLocationId(); if (StringUtils.isBlank(ec2LocationId)) { throw new IllegalArgumentException("Location Id not set on the management template"); } logger.info("creating EC2 client"); final String protocol = (String) cloud.getCustom().get("protocol"); if (StringUtils.isNotBlank(protocol)) { // set the client protocol logger.info("setting the EC2 client protocol to: " + protocol); ClientConfiguration clientConfig = new ClientConfiguration(); clientConfig.setProtocol(Protocol.valueOf(protocol)); ec2 = new AmazonEC2Client(credentials, clientConfig); } else { logger.info("using the default protocol for the EC2 client (https)"); ec2 = new AmazonEC2Client(credentials); } final String endpoint = (String) cloud.getCustom().get("endpoint"); if (StringUtils.isNotBlank(endpoint)) { logger.info("setting EC2 endpoint: " + endpoint); ec2.setEndpoint(endpoint); } else if (StringUtils.isNotBlank(ec2LocationId)) { Region region = RegionUtils.convertLocationId2Region(ec2LocationId); logger.info("setting EC2 region: " + region); ec2.setRegion(region); } else { logger.warning("EC2 endpoint and location not set, please set one of them"); } return ec2; } /** * ***************************************************************************************************************** */ /** * Start machines using CFN template if provides, use JClouds otherwise. * * @param locationId * the location to allocate the machine to. * @param duration * Time duration to wait for the instance. * @param unit * Time unit to wait for the instance. * * @return The details of the started instance. * * @throws TimeoutException * In case the instance was not started in the allotted time. * @throws CloudProvisioningException * If a problem was encountered while starting the machine. */ @Override public MachineDetails startMachine(final String locationId, final long duration, final TimeUnit unit) throws TimeoutException, CloudProvisioningException { if (logger.isLoggable(Level.FINEST)) { logger.finest("Stating new machine with the following thread: threadId=" + Thread.currentThread().getId() + " serviceName=" + this.serviceName); } setPrivateEc2Template(); final String newName = this.createNewName(TagResourceType.INSTANCE, cloud.getProvider().getMachineNamePrefix()); final ProvisioningContextImpl ctx = (ProvisioningContextImpl) new ProvisioningContextAccess().getProvisioiningContext(); final MachineDetails md = this.createServer(this.privateEc2Template, newName, ctx, false, duration, unit); logger.fine("[" + md.getMachineId() + "] Cloud Server is allocated."); return md; } @Override public boolean stopMachine(final String serverIp, final long duration, final TimeUnit unit) throws CloudProvisioningException, TimeoutException, InterruptedException { if (logger.isLoggable(Level.FINEST)) { logger.finest("Stopping new machine with the following thread: threadId=" + Thread.currentThread().getId() + " serviceName=" + this.serviceName + " serverIp=" + serverIp); } logger.info("Stopping instance server ip = " + serverIp + "..."); final DescribeInstancesRequest describeInstance = new DescribeInstancesRequest(); describeInstance.withFilters(new Filter("private-ip-address", Arrays.asList(serverIp))); final DescribeInstancesResult describeInstances = ec2Client.describeInstances(describeInstance); final Reservation reservation = describeInstances.getReservations().get(0); if (reservation != null && reservation.getInstances().get(0) != null) { final TerminateInstancesRequest tir = new TerminateInstancesRequest(); tir.withInstanceIds(reservation.getInstances().get(0).getInstanceId()); final TerminateInstancesResult terminateInstances = ec2Client.terminateInstances(tir); final String instanceId = terminateInstances.getTerminatingInstances().get(0).getInstanceId(); try { this.waitStopInstanceStatus(instanceId, duration, unit); } finally { // FIXME By default, cloudify doesn't delete tags. So we should keep it that way. // Remove instance Tags // if (!terminateInstances.getTerminatingInstances().isEmpty()) { // logger.fine("Deleting tags for instance id=" + instanceId); // DeleteTagsRequest deleteTagsRequest = new DeleteTagsRequest(); // deleteTagsRequest.setResources(Arrays.asList(instanceId)); // ec2.deleteTags(deleteTagsRequest); // } } } else { logger.warning("No instance to stop: " + reservation); } return true; } private void waitStopInstanceStatus(final String instanceId, final long duration, final TimeUnit unit) throws CloudProvisioningException, TimeoutException { final long endTime = System.currentTimeMillis() + unit.toMillis(duration); while (System.currentTimeMillis() < endTime) { final DescribeInstancesRequest describeRequest = new DescribeInstancesRequest(); describeRequest.withInstanceIds(instanceId); final DescribeInstancesResult describeInstances = ec2Client.describeInstances(describeRequest); for (final Reservation resa : describeInstances.getReservations()) { for (final Instance instance : resa.getInstances()) { final InstanceStateType state = InstanceStateType.valueOf(instance.getState().getCode()); if (logger.isLoggable(Level.FINEST)) { logger.finest("instance= " + instance.getInstanceId() + " state=" + state); } switch (state) { case PENDING: case RUNNING: case STOPPING: case SHUTTING_DOWN: this.sleep(); break; case STOPPED: case TERMINATED: if (logger.isLoggable(Level.FINEST)) { logger.finest("instance (id=" + instanceId + ") was shutdown"); } return; default: throw new CloudProvisioningException("Failed to stop server - Cloud reported node in " + state.getName() + " state."); } } } } throw new TimeoutException("Stopping instace timed out (id=" + instanceId + ")"); } private void sleep() { try { Thread.sleep(WAIT_STATUS_SLEEP_TIME); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private MachineDetails createServer(final PrivateEc2Template cfnTemplate, final String machineName, final ProvisioningContextImpl ctx, final boolean management, final long duration, final TimeUnit unit) throws CloudProvisioningException, TimeoutException { final Instance ec2Instance = this.createEC2Instance(cfnTemplate, ctx, management, machineName, duration, unit); final MachineDetails md = new MachineDetails(); md.setMachineId(ec2Instance.getInstanceId()); md.setPrivateAddress(ec2Instance.getPrivateIpAddress()); md.setPublicAddress(ec2Instance.getPublicIpAddress()); md.setAgentRunning(true); md.setCloudifyInstalled(true); return md; } private void tagEC2Instance(final Instance ec2Instance, final String ec2InstanceName, final AWSEC2Instance templateInstance) throws CloudProvisioningException { final List<Tag> additionalTags = Arrays.asList(new Tag(TK_NAME, ec2InstanceName)); this.createEC2Tags(ec2Instance.getInstanceId(), templateInstance.getProperties().getTags(), additionalTags); } private void tagEC2Volumes(final String instanceId, final PrivateEc2Template cfnTemplate) throws CloudProvisioningException { final List<VolumeMapping> volumeMappings = cfnTemplate.getEC2Instance().getProperties().getVolumes(); if (volumeMappings != null) { final DescribeVolumesRequest request = new DescribeVolumesRequest(); request.withFilters(new Filter("attachment.instance-id", Arrays.asList(instanceId))); final DescribeVolumesResult describeVolumes = ec2Client.describeVolumes(request); for (final Volume volume : describeVolumes.getVolumes()) { String volumeRef = null; for (final VolumeMapping vMap : volumeMappings) { final String device = volume.getAttachments().get(0).getDevice(); if (device.equals(vMap.getDevice().getValue())) { volumeRef = vMap.getVolumeId().getValue(); break; } } if (volumeRef != null) { final AWSEC2Volume ec2Volume = cfnTemplate.getEC2Volume(volumeRef); final List<org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.Tag> templateTags = ec2Volume == null ? null : ec2Volume .getProperties().getTags(); final List<Tag> additionalTags = Arrays.asList(new Tag(TK_NAME, this.createNewName(TagResourceType.VOLUME, VOLUME_PREFIX))); this.createEC2Tags(volume.getVolumeId(), templateTags, additionalTags); } } } } private String createNewName(final TagResourceType resourceType, final String prefix) throws CloudProvisioningException { String newName = null; int attempts = 0; boolean foundFreeName = false; while (attempts < MAX_SERVERS_LIMIT) { // counter = (counter + 1) % MAX_SERVERS_LIMIT; ++attempts; switch (resourceType) { case INSTANCE: newName = prefix + counter.incrementAndGet(); break; case VOLUME: newName = prefix + volumeCounter.incrementAndGet(); break; default: // not possible throw new CloudProvisioningException("ResourceType not supported"); } // verifying this server name is not already used final DescribeTagsRequest tagRequest = new DescribeTagsRequest(); tagRequest.withFilters(new Filter("resource-type", Arrays.asList(resourceType.getValue()))); tagRequest.withFilters(new Filter("value", Arrays.asList(newName))); final DescribeTagsResult describeTags = ec2Client.describeTags(tagRequest); final List<TagDescription> tags = describeTags.getTags(); if (tags == null || tags.isEmpty()) { foundFreeName = true; break; } } if (!foundFreeName) { throw new CloudProvisioningException("Number of servers has exceeded allowed server limit (" + MAX_SERVERS_LIMIT + ")"); } return newName; } private void createEC2Tags(final String resourceId, final List<org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.Tag> templateTags, final List<Tag> additionalTags) { final List<Tag> tags = new ArrayList<Tag>(); if (templateTags != null) { for (final org.cloudifysource.esc.driver.provisioning.privateEc2.parser.beans.Tag tag : templateTags) { tags.add(tag.convertToEC2Model()); } } if (additionalTags != null) { tags.addAll(additionalTags); } if (!tags.isEmpty()) { logger.fine("Tag resourceId=" + resourceId + " tags=" + tags); final CreateTagsRequest ctr = new CreateTagsRequest(); ctr.setTags(tags); ctr.withResources(resourceId); this.ec2Client.createTags(ctr); } } private Instance waitRunningInstance(final Instance ec2instance, final long duration, final TimeUnit unit) throws CloudProvisioningException, TimeoutException { final long endTime = System.currentTimeMillis() + unit.toMillis(duration); while (System.currentTimeMillis() < endTime) { // Sleep before requesting the instance description // because we can get a AWS Error Code: InvalidInstanceID.NotFound if the request is too early. this.sleep(); final DescribeInstancesRequest describeRequest = new DescribeInstancesRequest(); describeRequest.setInstanceIds(Arrays.asList(ec2instance.getInstanceId())); final DescribeInstancesResult describeInstances = this.ec2Client.describeInstances(describeRequest); for (final Reservation resa : describeInstances.getReservations()) { for (final Instance instance : resa.getInstances()) { final InstanceStateType state = InstanceStateType.valueOf(instance.getState().getCode()); if (logger.isLoggable(Level.FINER)) { logger.finer("instance= " + instance.getInstanceId() + " state=" + state); } switch (state) { case PENDING: break; case RUNNING: logger.fine("running okay..."); return instance; case STOPPING: case SHUTTING_DOWN: case TERMINATED: case STOPPED: default: throw new CloudProvisioningException("Failed to allocate server - Cloud reported node in " + state.getName() + " state. Node details: " + ec2instance); } } } } throw new TimeoutException("Node failed to reach RUNNING mode in time"); } private MachineDetails[] getManagementServersMachineDetails() throws CloudProvisioningException { final DescribeInstancesResult describeInstances = this.requestEC2InstancesManager(); if (describeInstances == null) { return new MachineDetails[0]; } final List<MachineDetails> mds = new ArrayList<MachineDetails>(); for (final Reservation resa : describeInstances.getReservations()) { for (final Instance instance : resa.getInstances()) { final MachineDetails md = this.createMachineDetailsFromInstance(instance); mds.add(md); } } return mds.toArray(new MachineDetails[mds.size()]); } private DescribeInstancesResult requestEC2InstancesManager() { try { final DescribeInstancesRequest request = new DescribeInstancesRequest(); request.withFilters(new Filter("instance-state-name", Arrays.asList(InstanceStateType.RUNNING.getName())), new Filter("tag-key", Arrays.asList("Name")), new Filter("tag-value", Arrays.asList(cloud.getProvider().getManagementGroup() + "*"))); final DescribeInstancesResult describeInstances = ec2Client.describeInstances(request); return describeInstances; } catch (final AmazonServiceException e) { if (e.getStatusCode() == AMAZON_EXCEPTION_CODE_400) { // Not found return null; } else { throw e; } } } private MachineDetails createMachineDetailsFromInstance(final Instance instance) throws CloudProvisioningException { final ComputeTemplate template = this.cloud.getCloudCompute().getTemplates().get( this.cloudTemplateName); if (template == null) { throw new CloudProvisioningException("Could not find template " + this.cloudTemplateName); } final MachineDetails md = new MachineDetails(); md.setAgentRunning(false); md.setRemoteExecutionMode(template.getRemoteExecution()); md.setFileTransferMode(template.getFileTransfer()); md.setScriptLangeuage(template.getScriptLanguage()); md.setCloudifyInstalled(false); md.setInstallationDirectory(null); md.setMachineId(instance.getInstanceId()); md.setPrivateAddress(instance.getPrivateIpAddress()); md.setPublicAddress(instance.getPublicIpAddress()); md.setRemoteUsername(template.getUsername()); md.setRemotePassword(template.getPassword()); final String availabilityZone = instance.getPlacement().getAvailabilityZone(); md.setLocationId(RegionUtils.convertAvailabilityZone2LocationId(availabilityZone)); md.setOpenFilesLimit(this.template.getOpenFilesLimit()); return md; } private Instance createEC2Instance(final PrivateEc2Template cfnTemplate, final ProvisioningContextImpl ctx, final boolean management, final String machineName, final long duration, final TimeUnit unit) throws CloudProvisioningException, TimeoutException { final ComputeTemplate template = this.getManagerComputeTemplate(); final InstanceProperties properties = cfnTemplate.getEC2Instance().getProperties(); final String availabilityZone = properties.getAvailabilityZone() == null ? null : properties.getAvailabilityZone().getValue(); final Placement placement = availabilityZone == null ? null : new Placement(availabilityZone); final String imageId = properties.getImageId() == null ? null : properties.getImageId().getValue(); final String instanceType = properties.getInstanceType() == null ? null : properties.getInstanceType().getValue(); final String keyName = properties.getKeyName() == null ? null : properties.getKeyName().getValue(); final String privateIpAddress = properties.getPrivateIpAddress() == null ? null : properties.getPrivateIpAddress().getValue(); final List<String> securityGroupIds = properties.getSecurityGroupIdsAsString(); final List<String> securityGroups = properties.getSecurityGroupsAsString(); S3Object s3Object = null; try { String userData = null; if (properties.getUserData() != null) { // Generate ENV script for the provisioned machine final StringBuilder sb = new StringBuilder(); final String script = management ? this.generateManagementCloudifyEnv(ctx) : this.generateCloudifyEnv(ctx); s3Object = this.uploadCloudDir(ctx, script, management); final String cloudFileS3 = this.amazonS3Uploader.generatePresignedURL(s3Object); String cloudFileDir = (String) template.getRemoteDirectory(); // Remove '/' from the path if it's the last char. if (cloudFileDir.length() > 1 && cloudFileDir.endsWith("/")) { cloudFileDir = cloudFileDir.substring(0, cloudFileDir.length() - 1); } final String endOfLine = " >> /tmp/cloud.txt\n"; sb.append("#!/bin/bash\n"); sb.append("export TMP_DIRECTORY=/tmp").append(endOfLine); sb.append("export S3_ARCHIVE_FILE='" + cloudFileS3 + "'").append(endOfLine); sb.append("wget -q -O $TMP_DIRECTORY/cloudArchive.tar.gz $S3_ARCHIVE_FILE").append(endOfLine); sb.append("mkdir -p " + cloudFileDir).append(endOfLine); sb.append("tar zxvf $TMP_DIRECTORY/cloudArchive.tar.gz -C " + cloudFileDir).append(endOfLine); sb.append("rm -f $TMP_DIRECTORY/cloudArchive.tar.gz").append(endOfLine); sb.append("echo ").append(cloudFileDir).append("/").append(CLOUDIFY_ENV_SCRIPT).append(endOfLine); sb.append("chmod 755 ").append(cloudFileDir).append("/").append(CLOUDIFY_ENV_SCRIPT).append(endOfLine); sb.append("source ").append(cloudFileDir).append("/").append(CLOUDIFY_ENV_SCRIPT).append(endOfLine); sb.append(properties.getUserData().getValue()); userData = sb.toString(); logger.fine("Instanciate ec2 with user data:\n" + userData); userData = org.apache.commons.codec.binary.StringUtils.newStringUtf8(Base64.encodeBase64(userData.getBytes())); } List<BlockDeviceMapping> blockDeviceMappings = null; AWSEC2Volume volumeConfig = null; if (properties.getVolumes() != null) { blockDeviceMappings = new ArrayList<BlockDeviceMapping>(properties.getVolumes().size()); for (final VolumeMapping volMapping : properties.getVolumes()) { volumeConfig = cfnTemplate.getEC2Volume(volMapping.getVolumeId().getValue()); blockDeviceMappings.add(this.createBlockDeviceMapping(volMapping.getDevice().getValue(), volumeConfig)); } } final RunInstancesRequest runInstancesRequest = new RunInstancesRequest(); runInstancesRequest.withPlacement(placement); runInstancesRequest.withImageId(imageId); runInstancesRequest.withInstanceType(instanceType); runInstancesRequest.withKeyName(keyName); runInstancesRequest.withPrivateIpAddress(privateIpAddress); runInstancesRequest.withSecurityGroupIds(securityGroupIds); runInstancesRequest.withSecurityGroups(securityGroups); runInstancesRequest.withMinCount(1); runInstancesRequest.withMaxCount(1); runInstancesRequest.withBlockDeviceMappings(blockDeviceMappings); runInstancesRequest.withUserData(userData); if (logger.isLoggable(Level.FINEST)) { logger.finest("EC2::Instance request=" + runInstancesRequest); } final RunInstancesResult runInstances = this.ec2Client.runInstances(runInstancesRequest); if (runInstances.getReservation().getInstances().size() != 1) { throw new CloudProvisioningException("Request runInstace fails (request=" + runInstancesRequest + ")."); } Instance ec2Instance = runInstances.getReservation().getInstances().get(0); ec2Instance = this.waitRunningInstance(ec2Instance, duration, unit); this.tagEC2Instance(ec2Instance, machineName, cfnTemplate.getEC2Instance()); this.tagEC2Volumes(ec2Instance.getInstanceId(), cfnTemplate); final boolean debug = BooleanUtils.toBoolean((String) template.getCustom().get("debugMode")); if (debug) { debugExecutors.submit(new EC2Console( ec2Instance.getInstanceId(), ec2Instance.getPublicIpAddress(), DEFAULT_CLOUDIFY_AGENT_PORT)); } this.waitRunningAgent(ec2Instance.getPublicIpAddress(), duration, unit); return ec2Instance; } finally { if (s3Object != null) { this.amazonS3Uploader.deleteS3Object(s3Object.getBucketName(), s3Object.getKey()); } } } private void waitRunningAgent(final String host, final long duration, final TimeUnit unit) { long endTime = System.currentTimeMillis() + unit.toMillis(duration); while (System.currentTimeMillis() < endTime) { if (this.isPortReachable(host, DEFAULT_CLOUDIFY_AGENT_PORT)) { logger.fine("Agent is reachable on: " + host + ":" + DEFAULT_CLOUDIFY_AGENT_PORT); break; } else { this.sleep(); } } } private boolean isPortReachable(final String host, final int port) { Socket socket = null; try { socket = new Socket(host, port); return true; } catch (Exception e) { return false; } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { logger.warning("Can't close port: " + host + ":" + port); return false; } } } } private S3Object uploadCloudDir(final ProvisioningContextImpl ctx, final String script, final boolean isManagement) throws CloudProvisioningException { try { final ComputeTemplate template = this.getManagerComputeTemplate(); final String cloudDirectory = isManagement ? ((File) this.cloud.getCustom().get("###CLOUD_DIRECTORY###")).getAbsolutePath() : template.getAbsoluteUploadDir(); final String s3BucketName = (String) template.getCustom().get("s3BucketName"); // Generate env script final StringBuilder sb = new StringBuilder(); sb.append("#!/bin/bash\n"); sb.append(script); if (isManagement) { // TODO retrieve port dynamically for LUS_IP_ADDRESS sb.append("export LUS_IP_ADDRESS=`curl http://instance-data/latest/meta-data/local-ipv4`:4174"); } // Create tmp dir final File createTempFile = File.createTempFile("cloudify_env", ""); createTempFile.delete(); // Create tmp file final File tmpEnvFile = new File(createTempFile, CLOUDIFY_ENV_SCRIPT); tmpEnvFile.deleteOnExit(); // Write the script into the temp filedir FileUtils.writeStringToFile(tmpEnvFile, sb.toString(), CharEncoding.UTF_8); // Compress file logger.fine("Archive folders to upload: " + cloudDirectory + " and " + tmpEnvFile.getAbsolutePath()); String[] sourcePaths = new String[] { cloudDirectory, tmpEnvFile.getAbsolutePath() }; final File tarGzFile = TarGzUtils.createTarGz(sourcePaths, false); // Upload to S3 final S3Object s3Object = amazonS3Uploader.uploadFile(s3BucketName, tarGzFile); return s3Object; } catch (IOException e) { throw new CloudProvisioningException(e); } } private BlockDeviceMapping createBlockDeviceMapping(final String device, final AWSEC2Volume volumeConfig) throws CloudProvisioningException { final VolumeProperties volumeProperties = volumeConfig.getProperties(); final Integer iops = volumeProperties.getIops() == null ? null : volumeProperties.getIops(); final Integer size = volumeProperties.getSize(); final String snapshotId = volumeProperties.getSnapshotId() == null ? null : volumeProperties.getSnapshotId().getValue(); final String volumeType = volumeProperties.getVolumeType() == null ? null : volumeProperties.getVolumeType().getValue(); final EbsBlockDevice ebs = new EbsBlockDevice(); ebs.setIops(iops); ebs.setSnapshotId(snapshotId); ebs.setVolumeSize(size); ebs.setVolumeType(volumeType); ebs.setDeleteOnTermination(true); final BlockDeviceMapping mapping = new BlockDeviceMapping(); mapping.setDeviceName(device); mapping.setEbs(ebs); return mapping; } private String generateManagementCloudifyEnv(final ManagementProvisioningContext ctx) throws CloudProvisioningException { final ComputeTemplate template = new ComputeTemplate(); // FIXME may not work on windows because of script language template.setScriptLanguage(ScriptLanguages.LINUX_SHELL); template.setRemoteDirectory(""); try { final MachineDetails machineDetails = new MachineDetails(); machineDetails.setRemoteDirectory(getManagerComputeTemplate().getRemoteDirectory()); final MachineDetails[] mds = { machineDetails }; // As every specific environment variables will be set with user data we don't need to generate a script per // management machine. final String[] scripts = ctx.createManagementEnvironmentScript(mds, template); return scripts[0]; } catch (final FileNotFoundException e) { logger.log(Level.SEVERE, "Couldn't find file: ", e.getMessage()); throw new CloudProvisioningException(e); } } private String generateCloudifyEnv(final ProvisioningContext ctx) throws CloudProvisioningException { final ComputeTemplate template = new ComputeTemplate(); // FIXME may not work on windows because of script language template.setScriptLanguage(ScriptLanguages.LINUX_SHELL); try { final MachineDetails md = new MachineDetails(); // TODO set location id in user data. md.setLocationId(this.getManagementServersMachineDetails()[0].getLocationId()); final String script = ctx.createEnvironmentScript(md, template); return script; } catch (final FileNotFoundException e) { logger.log(Level.SEVERE, "Couldn't find file: ", e.getMessage()); throw new CloudProvisioningException(e); } } @Override public Object getComputeContext() { return null; } @Override public MachineDetails[] startManagementMachines(final long duration, final TimeUnit unit) throws TimeoutException, CloudProvisioningException { if (duration < 0) { throw new TimeoutException("Starting a new machine timed out"); } final long endTime = System.currentTimeMillis() + unit.toMillis(duration); logger.fine("DefaultCloudProvisioning: startMachine - management == " + management); setPrivateEc2Template(); final String managementMachinePrefix = this.cloud.getProvider().getManagementGroup(); if (StringUtils.isBlank(managementMachinePrefix)) { throw new CloudProvisioningException( "The management group name is missing - can't locate existing servers!"); } // first check if management already exists final MachineDetails[] existingManagementServers = this.getManagementServersMachineDetails(); if (existingManagementServers.length > 0) { final String serverDescriptions = this.createExistingServersDescription(managementMachinePrefix, existingManagementServers); throw new CloudProvisioningException("Found existing servers matching group " + managementMachinePrefix + ": " + serverDescriptions); } // launch the management machines final int numberOfManagementMachines = this.cloud.getProvider().getNumberOfManagementMachines(); MachineDetails[] createdMachines; try { createdMachines = this.doStartManagementMachines(numberOfManagementMachines, endTime, unit); } catch (final PrivateEc2ParserException e) { throw new CloudProvisioningException(e); } return createdMachines; } private String createExistingServersDescription(final String managementMachinePrefix, final MachineDetails[] existingManagementServers) { logger.info("Found existing servers matching the name: " + managementMachinePrefix); final StringBuilder sb = new StringBuilder(); boolean first = true; for (final MachineDetails machineDetails : existingManagementServers) { final String existingManagementServerDescription = createManagementServerDescription(machineDetails); if (first) { first = false; } else { sb.append(", "); } sb.append("[").append(existingManagementServerDescription).append("]"); } final String serverDescriptions = sb.toString(); return serverDescriptions; } private String createManagementServerDescription(final MachineDetails machineDetails) { final StringBuilder sb = new StringBuilder(); sb.append("Machine ID: ").append(machineDetails.getMachineId()); if (machineDetails.getPublicAddress() != null) { sb.append(", Public IP: ").append(machineDetails.getPublicAddress()); } if (machineDetails.getPrivateAddress() != null) { sb.append(", Private IP: ").append(machineDetails.getPrivateAddress()); } return sb.toString(); } private MachineDetails[] doStartManagementMachines(final int numberOfManagementMachines, final long endTime, final TimeUnit unit) throws TimeoutException, CloudProvisioningException, PrivateEc2ParserException { final ExecutorService executors = Executors.newFixedThreadPool(numberOfManagementMachines); @SuppressWarnings("unchecked") final Future<MachineDetails>[] futures = (Future<MachineDetails>[]) new Future<?>[numberOfManagementMachines]; try { final PrivateEc2Template template = this.privateEc2Template; final String managementGroup = this.cloud.getProvider().getManagementGroup(); final ProvisioningContextImpl ctx = (ProvisioningContextImpl) new ProvisioningContextAccess().getManagementProvisioiningContext(); logger.info("ctx_threadlocal=" + ctx); // Call startMachine asynchronously once for each management machine for (int i = 0; i < numberOfManagementMachines; i++) { final int index = i + 1; futures[i] = executors.submit(new Callable<MachineDetails>() { @Override public MachineDetails call() throws Exception { return createServer(template, managementGroup + index, ctx, true, endTime, unit); } }); } // Wait for each of the async calls to terminate. int numberOfErrors = 0; Exception firstCreationException = null; final MachineDetails[] createdManagementMachines = new MachineDetails[numberOfManagementMachines]; for (int i = 0; i < createdManagementMachines.length; i++) { try { createdManagementMachines[i] = futures[i].get(endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } catch (final InterruptedException e) { ++numberOfErrors; logger.log(Level.SEVERE, "Failed to start a management machine", e); if (firstCreationException == null) { firstCreationException = e; } } catch (final ExecutionException e) { ++numberOfErrors; logger.log(Level.SEVERE, "Failed to start a management machine", e); if (firstCreationException == null) { firstCreationException = e; } } } // In case of a partial error, shutdown all servers that did start up if (numberOfErrors > 0) { this.handleProvisioningFailure(numberOfManagementMachines, numberOfErrors, firstCreationException, createdManagementMachines); } return createdManagementMachines; } finally { if (executors != null) { executors.shutdownNow(); } } } private void handleProvisioningFailure(final int numberOfManagementMachines, final int numberOfErrors, final Exception firstCreationException, final MachineDetails[] createdManagementMachines) throws CloudProvisioningException { logger.severe("Of the required " + numberOfManagementMachines + " management machines, " + numberOfErrors + " failed to start."); if (numberOfManagementMachines > numberOfErrors) { logger.severe("Shutting down the other managememnt machines"); for (final MachineDetails machineDetails : createdManagementMachines) { if (machineDetails != null) { logger.severe("Shutting down machine: " + machineDetails); final TerminateInstancesRequest terminateInstancesRequest = new TerminateInstancesRequest(); terminateInstancesRequest.setInstanceIds(Arrays.asList(machineDetails.getMachineId())); ec2Client.terminateInstances(terminateInstancesRequest); } } } throw new CloudProvisioningException( "One or more managememnt machines failed. The first encountered error was: " + firstCreationException.getMessage(), firstCreationException); } @Override public void stopManagementMachines() throws TimeoutException, CloudProvisioningException { final MachineDetails[] managementServersMachineDetails = this.getManagementServersMachineDetails(); final List<String> ids = new ArrayList<String>(managementServersMachineDetails.length); for (final MachineDetails machineDetails : managementServersMachineDetails) { ids.add(machineDetails.getMachineId()); } final TerminateInstancesRequest terminateInstancesRequest = new TerminateInstancesRequest(); terminateInstancesRequest.setInstanceIds(ids); logger.info("Terminating management instances... " + terminateInstancesRequest); ec2Client.terminateInstances(terminateInstancesRequest); } @Override public String getCloudName() { return this.cloudName; } @Override public void close() { if (ec2Client != null) { ec2Client.shutdown(); } if (debugExecutors != null) { if (logger.isLoggable(Level.FINEST)) { logger.finest("Shutting down console output executor."); } debugExecutors.shutdownNow(); } } @Override public void onServiceUninstalled(final long duration, final TimeUnit unit) throws InterruptedException, TimeoutException, CloudProvisioningException { this.cfnTemplatePerService.clear(); } }