/**
* Copyright 2014-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 static net.roboconf.target.ec2.internal.Ec2IaasHandler.VOLUME_DELETE_OT_PREFIX;
import static net.roboconf.target.ec2.internal.Ec2IaasHandler.VOLUME_MOUNT_POINT_PREFIX;
import static net.roboconf.target.ec2.internal.Ec2IaasHandler.VOLUME_NAME_PREFIX;
import static net.roboconf.target.ec2.internal.Ec2IaasHandler.VOLUME_SIZE_GB_PREFIX;
import static net.roboconf.target.ec2.internal.Ec2IaasHandler.VOLUME_TYPE_PREFIX;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.AssociateAddressRequest;
import com.amazonaws.services.ec2.model.AttachVolumeRequest;
import com.amazonaws.services.ec2.model.CreateTagsRequest;
import com.amazonaws.services.ec2.model.CreateVolumeRequest;
import com.amazonaws.services.ec2.model.CreateVolumeResult;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.DescribeVolumesRequest;
import com.amazonaws.services.ec2.model.DescribeVolumesResult;
import com.amazonaws.services.ec2.model.EbsInstanceBlockDeviceSpecification;
import com.amazonaws.services.ec2.model.Filter;
import com.amazonaws.services.ec2.model.InstanceBlockDeviceMappingSpecification;
import com.amazonaws.services.ec2.model.ModifyInstanceAttributeRequest;
import com.amazonaws.services.ec2.model.Tag;
import net.roboconf.core.model.beans.Instance;
import net.roboconf.core.utils.Utils;
import net.roboconf.target.api.AbstractThreadedTargetHandler.MachineConfigurator;
import net.roboconf.target.api.TargetException;
/**
* A machine configurator for EC2.
* @author Vincent Zurczak - Linagora
*/
public class Ec2MachineConfigurator implements MachineConfigurator {
/**
* The steps of a workflow.
* <ul>
* <li>UNKNOWN_VM: the VM must be known by all the EC2 parts.</li>
* <li>TAG_VM: tag the VM.</li>
* <li>ASSOCIATE_ELASTIC_IP: an elastic IP has to be associated.</li>
* <li>RUNNING_VM: the VM must be running (started).</li>
* <li>ASSOCIATE_STORAGE: associate storage.</li>
* <li>COMPLETE: there is nothing to do anymore.</li>
* </ul>
*
* @author Vincent Zurczak - Linagora
*/
public enum State {
UNKNOWN_VM, TAG_VM, RUNNING_VM, ASSOCIATE_ELASTIC_IP, CREATE_VOLUME, ATTACH_VOLUME, COMPLETE
}
private final Instance scopedInstance;
private final String machineId, tagName;
private final String applicationName;
private String availabilityZone;
private final Map<String,String> targetProperties;
private final Logger logger = Logger.getLogger( getClass().getName());
private final Map<String,String> storageIdToVolumeId = new HashMap<> ();
private AmazonEC2 ec2Api;
private State state = State.UNKNOWN_VM;
private static final int DEFAULT_VOLUME_SIZE = 2;
/**
* Constructor.
*/
public Ec2MachineConfigurator(
Map<String,String> targetProperties,
String machineId,
String applicationName,
String rootInstanceName,
Instance scopedInstance ) {
this.machineId = machineId;
this.targetProperties = targetProperties;
this.applicationName = applicationName;
this.tagName = applicationName + "." + rootInstanceName;
this.scopedInstance = scopedInstance;
this.availabilityZone = targetProperties.get(Ec2Constants.AVAILABILITY_ZONE);
}
@Override
public Instance getScopedInstance() {
return this.scopedInstance;
}
@Override
public void close() throws IOException {
// nothing
}
@Override
public boolean configure() throws TargetException {
if( this.ec2Api == null )
this.ec2Api = Ec2IaasHandler.createEc2Client( this.targetProperties );
if( this.state == State.UNKNOWN_VM )
if( checkVmIsKnown())
this.state = State.TAG_VM;
// We cannot tag directly after the VM creation. See #197.
// We need it to be known by all the EC2 components.
if( this.state == State.TAG_VM )
if( tagResource(this.machineId, this.tagName))
this.state = State.RUNNING_VM;
if( this.state == State.RUNNING_VM )
if( checkVmIsStarted())
this.state = State.ASSOCIATE_ELASTIC_IP;
if( this.state == State.ASSOCIATE_ELASTIC_IP ) {
if( associateElasticIp())
this.state = State.CREATE_VOLUME;
}
if( this.state == State.CREATE_VOLUME ) {
if(! volumesRequested()) {
this.state = State.COMPLETE;
} else if(createOrReuseVolumes()) {
this.state = State.ATTACH_VOLUME;
}
}
if( this.state == State.ATTACH_VOLUME ) {
if(volumesCreated() && attachVolumes())
this.state = State.COMPLETE;
}
return this.state == State.COMPLETE;
}
/**
* Checks whether a VM is known (i.e. all the EC2 parts know it).
* @return true if the VM is know, false otherwise
*/
private boolean checkVmIsKnown() {
DescribeInstancesRequest dis = new DescribeInstancesRequest();
dis.setInstanceIds(Collections.singletonList(this.machineId));
DescribeInstancesResult disresult = this.ec2Api.describeInstances(dis);
return disresult.getReservations().size() > 0
&& disresult.getReservations().get( 0 ).getInstances().size() > 0;
}
/**
* Tags the specified resource, eg. a VM or volume (basically, it gives it a name).
* @param resourceId The ID of the resource to tag
* @param tagName The resource's name
* @return true if the tag was done, false otherwise
*/
private boolean tagResource(String resourceId, String tagName) {
boolean result = false;
if(! Utils.isEmptyOrWhitespaces(tagName)) {
Tag tag = new Tag( "Name", tagName );
CreateTagsRequest ctr = new CreateTagsRequest(Collections.singletonList(resourceId), Arrays.asList( tag ));
try {
this.ec2Api.createTags( ctr );
} catch(Exception e) {
this.logger.warning("Error tagging resource " + resourceId + " with name=" + tagName + ": " + e);
}
result = true;
}
return result;
}
/**
* Associates an elastic IP with the VM.
* @return true if there is nothing more to do about elastic IP configuration, false otherwise
*/
private boolean associateElasticIp() {
String elasticIp = this.targetProperties.get( Ec2Constants.ELASTIC_IP );
if( ! Utils.isEmptyOrWhitespaces( elasticIp )) {
this.logger.fine( "Associating an elastic IP with the instance. IP = " + elasticIp );
AssociateAddressRequest associateAddressRequest = new AssociateAddressRequest( this.machineId, elasticIp );
this.ec2Api.associateAddress( associateAddressRequest );
}
return true;
}
/**
* Checks whether a VM is started or not (which is stronger than {@link #checkVmIsKnown()}).
* @return true if the VM is started, false otherwise
*/
private boolean checkVmIsStarted() {
DescribeInstancesRequest dis = new DescribeInstancesRequest();
dis.setInstanceIds(Collections.singletonList(this.machineId));
DescribeInstancesResult disresult = this.ec2Api.describeInstances( dis );
// Obtain availability zone (for later use, eg. volume attachment).
// Necessary if no availability zone is specified in configuration
// (because volumes must be attached to instances in the same availability zone).
this.availabilityZone = disresult.getReservations().get(0).getInstances().get(0).getPlacement().getAvailabilityZone();
return "running".equalsIgnoreCase( disresult.getReservations().get(0).getInstances().get(0).getState().getName());
}
/**
* Checks whether EBS volume(s) creation/attachment is requested.
* @return true if requested, false otherwise
*/
private boolean volumesRequested() {
return ! Utils.isEmptyOrWhitespaces(this.targetProperties.get(Ec2Constants.USE_BLOCK_STORAGE));
}
/**
* Performs all steps necessary to create or reuse volume(s), as specified in configuration.
* @return true when volume(s) creation is done
*/
private boolean createOrReuseVolumes() {
for( String storageId : Ec2IaasHandler.findStorageIds( this.targetProperties )) {
String nameTemplate = Ec2IaasHandler.findStorageProperty(this.targetProperties, storageId, VOLUME_NAME_PREFIX);
// Lookup volume, according to its snapshot ID or Name tag.
String idOrName = Ec2IaasHandler.expandVolumeName(
nameTemplate, this.applicationName, this.scopedInstance.getName());
String volumeSnapshotOrId = lookupVolume(idOrName);
if(volumeSnapshotOrId == null) volumeSnapshotOrId = idOrName;
int size = DEFAULT_VOLUME_SIZE;
try {
size = Integer.parseInt(
Ec2IaasHandler.findStorageProperty(this.targetProperties, storageId, VOLUME_SIZE_GB_PREFIX));
} catch(Exception nfe) {
size = DEFAULT_VOLUME_SIZE;
}
if(size <= 0) size = DEFAULT_VOLUME_SIZE;
String volumeId;
if(volumeSnapshotOrId != null && volumeCreated(volumeSnapshotOrId)) {
volumeId = volumeSnapshotOrId;
} else {
volumeId = createVolume(storageId, volumeSnapshotOrId, size);
}
this.logger.info("Volume " + volumeId + " was successfully created.");
this.storageIdToVolumeId.put(storageId, volumeId);
}
return true;
}
/**
* Creates volume for EBS.
* @return volume ID of newly created volume
*/
private String createVolume(String storageId, String snapshotId, int size) {
String volumeType = Ec2IaasHandler.findStorageProperty(this.targetProperties, storageId, VOLUME_TYPE_PREFIX);
if(volumeType == null) volumeType = "standard";
CreateVolumeRequest createVolumeRequest = new CreateVolumeRequest()
.withAvailabilityZone( this.availabilityZone )
.withVolumeType( volumeType )
.withSize( size ); // The size of the volume, in gigabytes.
// EC2 snapshot IDs start with "snap-"...
if(! Utils.isEmptyOrWhitespaces(snapshotId) && snapshotId.startsWith("snap-"))
createVolumeRequest.withSnapshotId(snapshotId);
CreateVolumeResult createVolumeResult = this.ec2Api.createVolume(createVolumeRequest);
return createVolumeResult.getVolume().getVolumeId();
}
/**
* Checks whether volume is created.
* @param volumeId the EBS volume ID
* @return true if volume created, false otherwise
*/
private boolean volumeCreated(String volumeId) {
DescribeVolumesRequest dvs = new DescribeVolumesRequest();
ArrayList<String> volumeIds = new ArrayList<String>();
volumeIds.add(volumeId);
dvs.setVolumeIds(volumeIds);
DescribeVolumesResult dvsresult = null;
try {
dvsresult = this.ec2Api.describeVolumes(dvs);
} catch(Exception e) {
dvsresult = null;
}
return dvsresult != null && "available".equals(dvsresult.getVolumes().get(0).getState());
}
/**
* Checks whether all specified volumes are created.
* @return true if all volumes created, false otherwise
*/
private boolean volumesCreated() {
for( Map.Entry<String,String> entry : this.storageIdToVolumeId.entrySet()) {
String volumeId = entry.getValue();
if(! volumeCreated(volumeId)) return false;
}
return true;
}
/**
* Looks up volume, by ID or Name tag.
* @param volumeIdOrName the EBS volume ID or Name tag
* @return The volume ID of 1st matching volume found, null if no volume found
*/
private String lookupVolume(String volumeIdOrName) {
String ret = null;
if(! Utils.isEmptyOrWhitespaces(volumeIdOrName)) {
// Lookup by volume ID
DescribeVolumesRequest dvs = new DescribeVolumesRequest(Collections.singletonList(volumeIdOrName));
DescribeVolumesResult dvsresult = null;
try {
dvsresult = this.ec2Api.describeVolumes(dvs);
} catch(Exception e) {
dvsresult = null;
}
// If not found, lookup by name
if(dvsresult == null || dvsresult.getVolumes() == null || dvsresult.getVolumes().size() < 1) {
dvs = new DescribeVolumesRequest().withFilters(new Filter().withName("tag:Name").withValues(volumeIdOrName));
try {
dvsresult = this.ec2Api.describeVolumes(dvs);
} catch(Exception e) {
dvsresult = null;
}
}
if(dvsresult != null && dvsresult.getVolumes() != null && dvsresult.getVolumes().size() > 0)
ret = dvsresult.getVolumes().get(0).getVolumeId();
}
return ret;
}
/**
* Attaches volume(s) for EBS.
* @return true if successful attachment, or nothing to do. false otherwise
*/
private boolean attachVolumes() {
// If volume is found in map, it has been successfully created (no need to check here)
for( Map.Entry<String,String> entry : this.storageIdToVolumeId.entrySet()) {
String volumeId = entry.getValue();
String storageId = entry.getKey();
// Give a name to the volume before attaching
String nameTemplate = Ec2IaasHandler.findStorageProperty(this.targetProperties, storageId, VOLUME_NAME_PREFIX);
String name = Ec2IaasHandler.expandVolumeName(
nameTemplate, this.applicationName, this.scopedInstance.getName());
if(Utils.isEmptyOrWhitespaces(name)) name = "Created by Roboconf for " + this.tagName;
tagResource(volumeId, name);
// Attach volume now
String mountPoint = Ec2IaasHandler.findStorageProperty(this.targetProperties, storageId, VOLUME_MOUNT_POINT_PREFIX);
if(Utils.isEmptyOrWhitespaces(mountPoint)) mountPoint = "/dev/sdf";
AttachVolumeRequest attachRequest = new AttachVolumeRequest()
.withInstanceId(this.machineId)
.withDevice(mountPoint)
.withVolumeId(volumeId);
try {
this.ec2Api.attachVolume(attachRequest);
} catch(Exception e) {
this.logger.warning("EBS Volume attachment error: " + e);
}
// Set deleteOnTermination flag ?
if(Boolean.parseBoolean(Ec2IaasHandler.findStorageProperty(this.targetProperties, storageId, VOLUME_DELETE_OT_PREFIX))) {
EbsInstanceBlockDeviceSpecification ebsSpecification = new EbsInstanceBlockDeviceSpecification()
.withVolumeId(volumeId)
.withDeleteOnTermination(true);
InstanceBlockDeviceMappingSpecification mappingSpecification = new InstanceBlockDeviceMappingSpecification()
.withDeviceName(mountPoint)
.withEbs(ebsSpecification);
ModifyInstanceAttributeRequest request = new ModifyInstanceAttributeRequest()
.withInstanceId(this.machineId)
.withBlockDeviceMappings(mappingSpecification);
this.ec2Api.modifyInstanceAttribute(request);
}
}
return true;
}
}