/** * Copyright 2013-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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 net.roboconf.target.ec2.internal; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; import com.amazonaws.services.ec2.model.DescribeInstancesRequest; import com.amazonaws.services.ec2.model.DescribeInstancesResult; import com.amazonaws.services.ec2.model.Placement; import com.amazonaws.services.ec2.model.RunInstancesRequest; import com.amazonaws.services.ec2.model.RunInstancesResult; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import net.roboconf.core.agents.DataHelpers; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.helpers.InstanceHelpers; import net.roboconf.core.utils.Utils; import net.roboconf.target.api.AbstractThreadedTargetHandler; import net.roboconf.target.api.TargetException; import net.roboconf.target.api.TargetHandlerParameters; /** * @author Noël - LIG */ public class Ec2IaasHandler extends AbstractThreadedTargetHandler { public static final String TARGET_ID = "iaas-ec2"; static final String TPL_VOLUME_NAME = "%NAME%"; static final String TPL_VOLUME_APP = "%APP%"; static final String USE_BLOCK_STORAGE = "ec2.use-block-storage"; static final String VOLUME_MOUNT_POINT_PREFIX = "ec2.ebs-mount-point."; static final String VOLUME_NAME_PREFIX = "ec2.ebs-snapshot-id."; static final String VOLUME_SIZE_GB_PREFIX = "ec2.ebs-size."; static final String VOLUME_DELETE_OT_PREFIX = "ec2.ebs-delete-on-termination."; static final String VOLUME_TYPE_PREFIX = "ec2.ebs-type."; static final Map<String,String> DEFAULTS = new HashMap<> (); static { DEFAULTS.put( VOLUME_MOUNT_POINT_PREFIX, "/dev/sdf" ); DEFAULTS.put( VOLUME_NAME_PREFIX, "roboconf-" + TPL_VOLUME_APP + "-" + TPL_VOLUME_NAME ); DEFAULTS.put( VOLUME_SIZE_GB_PREFIX, "2" ); DEFAULTS.put( VOLUME_DELETE_OT_PREFIX, "false" ); } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler#getTargetId() */ @Override public String getTargetId() { return TARGET_ID; } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #createMachine(net.roboconf.target.api.TargetHandlerParameters) */ @Override public String createMachine( TargetHandlerParameters parameters ) throws TargetException { this.logger.fine( "Creating a new machine on AWS." ); // For IaaS, we only expect root instance names to be passed if( InstanceHelpers.countInstances( parameters.getScopedInstancePath()) > 1 ) throw new TargetException( "Only root instances can be passed in arguments." ); String rootInstanceName = InstanceHelpers.findRootInstancePath( parameters.getScopedInstancePath()); // Deal with the creation String instanceId; try { AmazonEC2 ec2 = createEc2Client( parameters.getTargetProperties()); String userData = DataHelpers.writeUserDataAsString( parameters.getMessagingProperties(), parameters.getDomain(), parameters.getApplicationName(), rootInstanceName ); RunInstancesRequest runInstancesRequest = prepareEC2RequestNode( parameters.getTargetProperties(), userData ); RunInstancesResult runInstanceResult = ec2.runInstances( runInstancesRequest ); instanceId = runInstanceResult.getReservation().getInstances().get( 0 ).getInstanceId(); } catch( Exception e ) { this.logger.severe( "An error occurred while creating a new machine on EC2. " + e.getMessage()); throw new TargetException( e ); } return instanceId; } /* * (non-Javadoc) * @see net.roboconf.target.api.AbstractThreadedTargetHandler#machineConfigurator( * net.roboconf.target.api.TargetHandlerParameters, java.lang.String, net.roboconf.core.model.beans.Instance) */ @Override public MachineConfigurator machineConfigurator( TargetHandlerParameters parameters, String machineId, Instance scopedInstance ) { String rootInstanceName = InstanceHelpers.findRootInstancePath( parameters.getScopedInstancePath()); return new Ec2MachineConfigurator( parameters.getTargetProperties(), machineId, parameters.getApplicationName(), rootInstanceName, scopedInstance ); } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #isMachineRunning(net.roboconf.target.api.TargetHandlerParameters, java.lang.String) */ @Override public boolean isMachineRunning( TargetHandlerParameters parameters, String machineId ) throws TargetException { boolean result = false; try { AmazonEC2 ec2 = createEc2Client( parameters.getTargetProperties()); DescribeInstancesRequest dis = new DescribeInstancesRequest(); dis.setInstanceIds(Collections.singletonList(machineId)); DescribeInstancesResult disresult = ec2.describeInstances( dis ); result = ! disresult.getReservations().isEmpty(); } catch( AmazonServiceException e ) { // nothing, the instance does not exist } catch( AmazonClientException e ) { this.logger.severe( "An error occurred while checking whether a machine is running on Amazon EC2. " + e.getMessage()); throw new TargetException( e ); } return result; } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #terminateMachine(net.roboconf.target.api.TargetHandlerParameters, java.lang.String) */ @Override public void terminateMachine( TargetHandlerParameters parameters, String machineId ) throws TargetException { this.logger.fine( "Terminating machine '" + machineId + "'." ); cancelMachineConfigurator( machineId ); try { AmazonEC2 ec2 = createEc2Client( parameters.getTargetProperties()); TerminateInstancesRequest terminateInstancesRequest = new TerminateInstancesRequest(); terminateInstancesRequest.withInstanceIds( machineId ); ec2.terminateInstances( terminateInstancesRequest ); } catch( Exception e ) { this.logger.severe( "An error occurred while terminating a machine on Amazon EC2. " + e.getMessage()); throw new TargetException( e ); } } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #retrievePublicIpAddress(net.roboconf.target.api.TargetHandlerParameters, java.lang.String) */ @Override public String retrievePublicIpAddress( TargetHandlerParameters parameters, String machineId ) throws TargetException { String result = null; try { AmazonEC2 ec2 = createEc2Client( parameters.getTargetProperties()); DescribeInstancesRequest dis = new DescribeInstancesRequest(); dis.setInstanceIds(Collections.singletonList(machineId)); DescribeInstancesResult disresult = ec2.describeInstances( dis ); if( ! disresult.getReservations().isEmpty()) { // Only one instance should match this machine ID result = disresult.getReservations().get( 0 ).getInstances().get( 0 ).getPublicIpAddress(); } } catch( AmazonServiceException e ) { // nothing, the instance does not exist } catch( Exception e ) { this.logger.severe( "An error occurred while retrieving a public IP address from Amazon EC2. " + e.getMessage()); throw new TargetException( e ); } return result; } /** * Parses the properties and saves them in a Java bean. * @param targetProperties the IaaS properties * @throws TargetException */ static void parseProperties( Map<String, String> targetProperties ) throws TargetException { // Quick check String[] properties = { Ec2Constants.EC2_ENDPOINT, Ec2Constants.EC2_ACCESS_KEY, Ec2Constants.EC2_SECRET_KEY, Ec2Constants.AMI_VM_NODE, Ec2Constants.VM_INSTANCE_TYPE, Ec2Constants.SSH_KEY_NAME, Ec2Constants.SECURITY_GROUP_NAME }; for( String property : properties ) { if( StringUtils.isBlank( targetProperties.get( property ))) throw new TargetException( "The value for " + property + " cannot be null or empty." ); } } /** * Creates a client for EC2. * @param targetProperties the target properties (not null) * @return a non-null client * @throws TargetException if properties are invalid */ public static AmazonEC2 createEc2Client( Map<String,String> targetProperties ) throws TargetException { parseProperties( targetProperties ); // Configure the IaaS client AWSCredentials credentials = new BasicAWSCredentials( targetProperties.get(Ec2Constants.EC2_ACCESS_KEY), targetProperties.get(Ec2Constants.EC2_SECRET_KEY)); AmazonEC2 ec2 = new AmazonEC2Client( credentials ); ec2.setEndpoint( targetProperties.get(Ec2Constants.EC2_ENDPOINT )); return ec2; } /** * Prepares the request. * @param targetProperties the target properties * @param userData the user data to pass * @return a request * @throws UnsupportedEncodingException */ private RunInstancesRequest prepareEC2RequestNode( Map<String,String> targetProperties, String userData ) throws UnsupportedEncodingException { RunInstancesRequest runInstancesRequest = new RunInstancesRequest(); String flavor = targetProperties.get(Ec2Constants.VM_INSTANCE_TYPE); if( Utils.isEmptyOrWhitespaces( flavor )) flavor = "t1.micro"; runInstancesRequest.setInstanceType( flavor ); runInstancesRequest.setImageId( targetProperties.get( Ec2Constants.AMI_VM_NODE )); runInstancesRequest.setMinCount( 1 ); runInstancesRequest.setMaxCount( 1 ); runInstancesRequest.setKeyName( targetProperties.get(Ec2Constants.SSH_KEY_NAME)); String secGroup = targetProperties.get(Ec2Constants.SECURITY_GROUP_NAME); if( Utils.isEmptyOrWhitespaces(secGroup)) secGroup = "default"; runInstancesRequest.setSecurityGroups(Collections.singletonList(secGroup)); String availabilityZone = targetProperties.get(Ec2Constants.AVAILABILITY_ZONE); if(! Utils.isEmptyOrWhitespaces(availabilityZone)) runInstancesRequest.setPlacement(new Placement(availabilityZone)); // The following part enables to transmit data to the VM. // When the VM is up, it will be able to read this data. String encodedUserData = new String( Base64.encodeBase64( userData.getBytes( "UTF-8" )), "UTF-8" ); runInstancesRequest.setUserData( encodedUserData ); return runInstancesRequest; } /** * Finds the storage IDs (used as property suffixes). * @param targetProperties * @return a non-null list */ static List<String> findStorageIds( Map<String,String> targetProperties ) { List<String> result = new ArrayList<> (); String prop = targetProperties.get( USE_BLOCK_STORAGE ); if( ! Utils.isEmptyOrWhitespaces( prop )) { for( String s : Utils.splitNicely( prop, "," )) { if( ! Utils.isEmptyOrWhitespaces( s )) result.add( s ); } } return result; } /** * Finds a storage property for a given storage ID. * @param targetProperties * @param storageId * @param propertyPrefix one of the constants defined in this class * @return the property's value, or the default value otherwise, if one exists */ static String findStorageProperty( Map<String,String> targetProperties, String storageId, String propertyPrefix ) { String property = propertyPrefix + storageId; String value = targetProperties.get( property ); return Utils.isEmptyOrWhitespaces( value ) ? DEFAULTS.get( propertyPrefix ) : value.trim(); } /** * Updates a volume name by replacing template variables. * @param nameTemplate (not null) * @param appName (not null) * @param instanceName (not null) * @return a string representing the expanded template */ static String expandVolumeName(String nameTemplate, String appName, String instanceName) { if(! Utils.isEmptyOrWhitespaces(nameTemplate)) { String name = nameTemplate.replace(TPL_VOLUME_NAME, instanceName); name = name.replace(TPL_VOLUME_APP, appName); name = name.replaceAll("[\\W_-]", "-"); return name; } return nameTemplate; } }