/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.usergrid.chop.api.store.amazon; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.apache.usergrid.chop.stack.BasicInstance; import org.apache.usergrid.chop.stack.ICoordinatedCluster; import org.apache.usergrid.chop.stack.ICoordinatedStack; import org.apache.usergrid.chop.stack.Instance; import org.apache.usergrid.chop.spi.InstanceManager; import org.apache.usergrid.chop.stack.InstanceState; import org.apache.usergrid.chop.spi.LaunchResult; import org.apache.usergrid.chop.stack.BasicInstanceSpec; import org.apache.usergrid.chop.stack.InstanceSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.ec2.AmazonEC2Client; 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.Filter; import com.amazonaws.services.ec2.model.InstanceStateName; 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.TerminateInstancesRequest; import com.google.inject.Inject; /** Implements all InstanceManager functionality for AmazonAWS */ public class EC2InstanceManager implements InstanceManager { private static Logger LOG = LoggerFactory.getLogger( EC2InstanceManager.class ); private static final long SLEEP_LENGTH = 3000; private AmazonEC2Client client; /** * @param amazonFig Fig object containing AWS credentials */ @Inject public EC2InstanceManager( AmazonFig amazonFig ) { client = AmazonUtils.getEC2Client( amazonFig.getAwsAccessKey(), amazonFig.getAwsSecretKey() ); } @Override public long getDefaultTimeout() { return SLEEP_LENGTH; } /** * Terminates instances with given Ids * * @param instanceIds */ @Override public void terminateInstances( final Collection<String> instanceIds ) { if( instanceIds == null || instanceIds.size() == 0 ) { return; } TerminateInstancesRequest request = ( new TerminateInstancesRequest() ).withInstanceIds( instanceIds ); client.terminateInstances( request ); } /** * All public methods except <code>terminateInstances</code> use supplied arguments * to set the appropriate data center. So this is only needed before calling <code>terminateInstances</code>. * * @param dataCenter Ec2Client's endpoint, us-east-1, us-west-2 etc. */ @Override public void setDataCenter( final String dataCenter ) { client.setEndpoint( AmazonUtils.getEndpoint( dataCenter ) ); } /** * Launches instances of given cluster. * * After launching instances, blocks for maximum <code>timeout</code> amount until all * instances get into the Running state. * * @param stack <code>ICoordinatedStack</code> object containing the <code>cluster</code> * @param cluster * @param timeout in milliseconds, if smaller than <code>getDefaultTimeout()</code> it doesn't wait * @return resulting runner instances which successfully got in Running state */ @Override public LaunchResult launchCluster( ICoordinatedStack stack, ICoordinatedCluster cluster, int timeout ) { RunInstancesResult runInstancesResult = null; try { RunInstancesRequest runInstancesRequest = new RunInstancesRequest(); runInstancesRequest.withImageId( cluster.getInstanceSpec().getImageId() ) .withInstanceType( cluster.getInstanceSpec().getType() ) .withMinCount( cluster.getSize() ).withMaxCount( cluster.getSize() ) .withKeyName( cluster.getInstanceSpec().getKeyName() ) .withSecurityGroups( stack.getIpRuleSet().getName() ); if ( stack.getDataCenter() != null && !stack.getDataCenter().isEmpty() ) { runInstancesRequest = runInstancesRequest.withPlacement( new Placement( stack.getDataCenter() ) ); client.setEndpoint( AmazonUtils.getEndpoint( stack.getDataCenter() ) ); } runInstancesResult = client.runInstances( runInstancesRequest ); } catch ( Exception e ) { LOG.error( "Error while launching cluster instances.", e ); return new EC2LaunchResult( cluster.getInstanceSpec(), Collections.EMPTY_LIST ); } LOG.info( "Created instances, setting the names now..." ); List<String> instanceIds = new ArrayList<String>( cluster.getSize() ); String instanceNames = getInstanceName( stack, cluster ); int i = 0; for( com.amazonaws.services.ec2.model.Instance instance : runInstancesResult.getReservation().getInstances() ) { try { instanceIds.add( i, instance.getInstanceId() ); LOG.debug( "Setting name of cluster instance with id: {}", instanceIds.get( i ) ); List<Tag> tags = new ArrayList<Tag>(); Tag t = new Tag(); t.setKey( "Name" ); t.setValue( instanceNames ); tags.add( t ); CreateTagsRequest ctr = new CreateTagsRequest(); ctr.setTags( tags ); ctr.withResources( instanceIds.get( i ) ); client.createTags( ctr ); } catch ( Exception e ) { LOG.warn( "Error while setting names", e ); } i++; } LOG.info( "Names of the instances are set" ); if ( timeout > SLEEP_LENGTH ) { LOG.info( "Waiting for maximum {} msec until all instances are running", timeout ); boolean stateCheck = waitUntil( instanceIds, InstanceState.Running, timeout ); if ( ! stateCheck ) { LOG.warn( "Waiting for instances to get into Running state has timed out" ); } } Collection<Instance> instances = toInstances( getEC2Instances( instanceIds ) ); return new EC2LaunchResult( cluster.getInstanceSpec(), instances ); } /** * Launches runner instances of given stack. * * Given <code>ICoordinatedStack</code> and an <code>InstanceSpec</code> * defining its runners' instance specifications, launches all runner instances. * After launching instances, blocks for maximum <code>timeout</code> amount until all * instances get into the Running state. * * @param stack * @param spec * @param timeout in milliseconds, if smaller than <code>getDefaultTimeout()</code> it doesn't wait * @return resulting runner instances which successfully got in Running state */ @Override public LaunchResult launchRunners( ICoordinatedStack stack, InstanceSpec spec, int timeout ) { RunInstancesResult runInstancesResult = null; try { RunInstancesRequest runInstancesRequest = new RunInstancesRequest(); runInstancesRequest.withImageId( spec.getImageId() ).withInstanceType( spec.getType() ) .withMinCount( stack.getRunnerCount() ).withMaxCount( stack.getRunnerCount() ) .withKeyName( spec.getKeyName() ).withSecurityGroups( stack.getIpRuleSet().getName() ); if ( stack.getDataCenter() != null && !stack.getDataCenter().isEmpty() ) { runInstancesRequest = runInstancesRequest.withPlacement( new Placement( stack.getDataCenter() ) ); client.setEndpoint( AmazonUtils.getEndpoint( stack.getDataCenter() ) ); } runInstancesResult = client.runInstances( runInstancesRequest ); } catch ( Exception e ) { LOG.error( "Error while launching runner instances.", e ); return new EC2LaunchResult( spec, Collections.EMPTY_LIST ); } LOG.info( "Created instances, setting the names now..." ); List<String> instanceIds = new ArrayList<String>( stack.getRunnerCount() ); String runnerNames = getRunnerName( stack ); int i = 0; for( com.amazonaws.services.ec2.model.Instance instance : runInstancesResult.getReservation().getInstances() ) { try { instanceIds.add( i, instance.getInstanceId() ); LOG.debug( "Setting name of runner instance with id: {}", instanceIds.get( i ) ); List<Tag> tags = new ArrayList<Tag>(); Tag t = new Tag(); t.setKey( "Name" ); t.setValue( runnerNames ); tags.add( t ); CreateTagsRequest ctr = new CreateTagsRequest(); ctr.setTags( tags ); ctr.withResources( instanceIds.get( i ) ); client.createTags( ctr ); } catch ( Exception e ) { LOG.warn( "Error while setting names", e ); } i++; } LOG.info( "Names of the instances are set" ); if ( timeout > SLEEP_LENGTH ) { LOG.info( "Waiting for maximum {} msec until all instances are running", timeout ); boolean stateCheck = waitUntil( instanceIds, InstanceState.Running, timeout ); if ( ! stateCheck ) { LOG.warn( "Waiting for instances to get into Running state has timed out" ); } } Collection<Instance> instances = toInstances( getEC2Instances( instanceIds ) ); return new EC2LaunchResult( spec, instances ); } /** * @param stack <code>ICoordinatedStack</code> object containing the <code>cluster</code> * @param cluster * @return Cluster instances which are in <code>Running</code> state */ @Override public Collection<Instance> getClusterInstances( ICoordinatedStack stack, ICoordinatedCluster cluster ) { String name = getInstanceName( stack, cluster ); if( stack.getDataCenter() != null && ! stack.getDataCenter().isEmpty() ) { client.setEndpoint( AmazonUtils.getEndpoint( stack.getDataCenter() ) ); } return toInstances( getEC2Instances( name, InstanceStateName.Running ) ); } /** * @param stack * @return Runner instances which belong to <code>stack</code> and in <code>Running</code> state */ @Override public Collection<Instance> getRunnerInstances( ICoordinatedStack stack ) { String name = getRunnerName( stack ); if( stack.getDataCenter() != null && ! stack.getDataCenter().isEmpty() ) { client.setEndpoint( AmazonUtils.getEndpoint( stack.getDataCenter() ) ); } return toInstances( getEC2Instances( name, InstanceStateName.Running ) ); } /** * @param name Causes the method to return instances with given Name tag, give null if you want to get * instances with all names * @param state Causes the method to return instances with given state, give null if you want to get instances in * all states * @return all instances that satisfy given parameters */ protected Collection<com.amazonaws.services.ec2.model.Instance> getEC2Instances( String name, InstanceStateName state ) { Collection<com.amazonaws.services.ec2.model.Instance> instances = new LinkedList<com.amazonaws.services.ec2.model.Instance>(); DescribeInstancesRequest request = new DescribeInstancesRequest(); if ( name != null ) { List<String> valuesT1 = new ArrayList<String>(); valuesT1.add( name ); Filter filter = new Filter("tag:Name", valuesT1); request = request.withFilters( filter ); } if ( state != null ) { List<String> valuesT1 = new ArrayList<String>(); valuesT1.add( state.toString() ); Filter filter = new Filter( "instance-state-name", valuesT1 ); request = request.withFilters( filter ); } DescribeInstancesResult result = null; try { result = client.describeInstances( request ); } catch ( Exception e ) { LOG.error( "Error while getting instance information from AWS.", e ); return Collections.EMPTY_LIST; } for ( Reservation reservation : result.getReservations() ) { for ( com.amazonaws.services.ec2.model.Instance in : reservation.getInstances() ) { instances.add( in ); } } return instances; } /** * Queries instances with given Ids on AWS * * @param instanceIds List of instance IDs * @return */ protected Collection<com.amazonaws.services.ec2.model.Instance> getEC2Instances( Collection<String> instanceIds ) { if( instanceIds == null || instanceIds.size() == 0 ) { return new ArrayList<com.amazonaws.services.ec2.model.Instance>(); } Collection<com.amazonaws.services.ec2.model.Instance> instances = new LinkedList<com.amazonaws.services.ec2.model.Instance>(); DescribeInstancesRequest request = new DescribeInstancesRequest(); request = request.withInstanceIds( instanceIds ); DescribeInstancesResult result = null; try { result = client.describeInstances( request ); } catch ( Exception e ) { LOG.error( "Error while getting instance information from AWS.", e ); return Collections.EMPTY_LIST; } for ( Reservation reservation : result.getReservations() ) { for ( com.amazonaws.services.ec2.model.Instance in : reservation.getInstances() ) { instances.add( in ); } } return instances; } /** * Takes a collection of AWS instances, and converts them into a collection of <code>Instance</code>s * * @param ec2s * @return */ protected Collection<Instance> toInstances( Collection<com.amazonaws.services.ec2.model.Instance> ec2s ) { Collection<Instance> instances = new ArrayList<Instance>( ec2s.size() ); for( com.amazonaws.services.ec2.model.Instance ec2 : ec2s ) { instances.add( toInstance( ec2 ) ); } return instances; } /** * Constructs and returns an BasicInstance object, using information from <code>ec2</code> * * @param ec2 * @return */ protected static Instance toInstance( com.amazonaws.services.ec2.model.Instance ec2 ) { Instance instance; BasicInstanceSpec spec; spec = new BasicInstanceSpec(); spec.setImageId( ec2.getImageId() ); spec.setKeyName( ec2.getKeyName() ); spec.setType( ec2.getInstanceType() ); instance = new BasicInstance( ec2.getInstanceId(), spec, InstanceState.fromValue( ec2.getState().getName() ), ec2.getPrivateDnsName(), ec2.getPublicDnsName(), ec2.getPrivateIpAddress(), ec2.getPublicIpAddress() ); return instance; } /** * Checks the state of all given instances in SLEEP_LENGTH intervals, returns when all instances are in expected * state or state check times out * * @param instanceIds List of instance IDs whose states are going to be checked * @param state Expected state to check * @param timeout Timeout length in milliseconds * @return true if all instances are in given state, false if timeout occured */ public boolean waitUntil ( Collection<String> instanceIds, InstanceState state, int timeout ) { List<String> instanceIdCopy = new ArrayList<String>( instanceIds ); Calendar cal = Calendar.getInstance(); cal.setTime( new Date() ); long startTime = cal.getTimeInMillis(); long timePassed; String stateStr; do { DescribeInstancesRequest dis = ( new DescribeInstancesRequest() ).withInstanceIds( instanceIdCopy ); DescribeInstancesResult disresult = client.describeInstances( dis ); // Since the request is filtered with instance IDs, there is always only one Reservation object Reservation reservation = disresult.getReservations().iterator().next(); for ( com.amazonaws.services.ec2.model.Instance in : reservation.getInstances() ) { stateStr = in.getState().getName(); LOG.info( "{} is {}", in.getInstanceId(), in.getState().getName() ); /** If expected state is ShuttingDown, also accept the Terminated ones */ if( state == InstanceState.ShuttingDown ) { if ( stateStr.equals( state.toString() ) || stateStr.equals( InstanceState.Terminated.toString() ) ) { instanceIdCopy.remove( in.getInstanceId() ); } } /** If expected state is Pending, also accept the Running ones */ else if( state == InstanceState.Pending ) { if ( stateStr.equals( state.toString() ) || stateStr.equals( InstanceState.Running.toString() ) ) { instanceIdCopy.remove( in.getInstanceId() ); } } /** If expected state is Stopping, also accept the Stopped ones */ else if( state == InstanceState.Stopping ) { if ( stateStr.equals( state.toString() ) || stateStr.equals( InstanceState.Stopped.toString() ) ) { instanceIdCopy.remove( in.getInstanceId() ); } } else { if ( in.getState().getName().equals( state.toString() ) ) { instanceIdCopy.remove( in.getInstanceId() ); } } } cal.setTime( new Date() ); timePassed = cal.getTimeInMillis() - startTime; try { Thread.sleep( SLEEP_LENGTH ); } catch ( InterruptedException e ) { LOG.warn( "Thread interrupted while sleeping", e ); } } while ( timePassed < timeout && instanceIdCopy.size() > 0 ); return ( timePassed < timeout ); } /** * @param stack Coordinated stack whose definition will be returned * @return Definition string containing stack's user, module, commit and name */ protected static String getLongName( ICoordinatedStack stack ) { StringBuilder sb = new StringBuilder(); sb.append( stack.getUser().getUsername() ) .append( "-" ).append( stack.getModule().getGroupId() ) .append( "-" ).append( stack.getModule().getArtifactId() ) .append( "-" ).append( stack.getModule().getVersion() ) .append( "-" ).append( stack.getCommit().getId() ) .append( "-" ).append( stack.getName() ); return sb.toString(); } /** * @param stack <code>ICoordinatedStack</code> object containing the <code>cluster</code> * @param cluster cluster whose name will be returned * @return Concatenates hash code of <code>getLongName</code> of given stack with cluster's name, * resulting a unique name for each cluster */ protected static String getInstanceName( ICoordinatedStack stack, ICoordinatedCluster cluster ) { StringBuilder sb = new StringBuilder(); int stackHash = getLongName( stack ).hashCode(); if( stackHash < 0 ) { stackHash += Integer.MAX_VALUE; } sb.append( stackHash ).append( "-" ).append( cluster.getName() ); return sb.toString(); } /** * @param stack <code>ICoordinatedStack</code> object the runners belong to * @return Concatenates hash code of <code>getLongName</code> of given stack with '-runner' suffix, * resulting a unique name for each stack */ protected static String getRunnerName( ICoordinatedStack stack ) { StringBuilder sb = new StringBuilder(); int stackHash = getLongName( stack ).hashCode(); if( stackHash < 0 ) { stackHash += Integer.MAX_VALUE; } sb.append( stackHash ).append( "-runner" ); return sb.toString(); } }