package hudson.plugins.ec2;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.CancelSpotInstanceRequestsRequest;
import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsRequest;
import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsResult;
import com.amazonaws.services.ec2.model.SpotInstanceRequest;
import com.amazonaws.services.ec2.model.SpotInstanceState;
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
import hudson.Extension;
import hudson.model.Hudson;
import hudson.model.Descriptor.FormException;
import hudson.plugins.ec2.ssh.EC2UnixLauncher;
import hudson.plugins.ec2.win.EC2WindowsLauncher;
import hudson.slaves.NodeProperty;
public final class EC2SpotSlave extends EC2AbstractSlave {
private static final Logger LOGGER = Logger.getLogger(EC2SpotSlave.class.getName());
private final String spotInstanceRequestId;
public EC2SpotSlave(String name, String spotInstanceRequestId, String description, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, String remoteAdmin, String jvmopts, String idleTerminationMinutes, List<EC2Tag> tags, String cloudName, boolean usePrivateDnsName, int launchTimeout, AMITypeData amiType)
throws FormException, IOException {
this(description + " (" + name + ")", spotInstanceRequestId, description, remoteFS, numExecutors, mode, initScript, tmpDir, labelString, Collections.<NodeProperty<?>> emptyList(), remoteAdmin, jvmopts, idleTerminationMinutes, tags, cloudName, usePrivateDnsName, launchTimeout, amiType);
}
@DataBoundConstructor
public EC2SpotSlave(String name, String spotInstanceRequestId, String description, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, List<? extends NodeProperty<?>> nodeProperties, String remoteAdmin, String jvmopts, String idleTerminationMinutes, List<EC2Tag> tags, String cloudName, boolean usePrivateDnsName, int launchTimeout, AMITypeData amiType)
throws FormException, IOException {
super(name, "", description, remoteFS, numExecutors, mode, labelString, amiType.isWindows() ? new EC2WindowsLauncher() :
new EC2UnixLauncher(), new EC2RetentionStrategy(idleTerminationMinutes), initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, false, idleTerminationMinutes, tags, cloudName, usePrivateDnsName, false, launchTimeout, amiType);
this.name = name;
this.spotInstanceRequestId = spotInstanceRequestId;
}
@Override
protected boolean isAlive(boolean force) {
return super.isAlive(force) || !this.isSpotRequestDead();
}
/**
* Cancel the spot request for the instance. Terminate the instance if it is up. Remove the slave from Jenkins.
*/
@Override
public void terminate() {
try {
// Cancel the spot request
AmazonEC2 ec2 = getCloud().connect();
String instanceId = getInstanceId();
List<String> requestIds = Collections.singletonList(spotInstanceRequestId);
CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(requestIds);
try {
ec2.cancelSpotInstanceRequests(cancelRequest);
LOGGER.info("Canceled Spot request: " + spotInstanceRequestId);
// Terminate the slave if it is running
if (instanceId != null && !instanceId.equals("")) {
if (!super.isAlive(true)) {
/*
* The node has been killed externally, so we've nothing to do here
*/
LOGGER.info("EC2 instance already terminated: " + instanceId);
} else {
TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(instanceId));
ec2.terminateInstances(request);
LOGGER.info("Terminated EC2 instance (terminated): " + instanceId);
}
}
} catch (AmazonServiceException e) {
// Spot request is no longer valid
LOGGER.log(Level.WARNING, "Failed to terminated instance and cancel Spot request: " + spotInstanceRequestId, e);
} catch (AmazonClientException e) {
// Spot request is no longer valid
LOGGER.log(Level.WARNING, "Failed to terminated instance and cancel Spot request: " + spotInstanceRequestId, e);
}
} catch (Exception e) {
LOGGER.log(Level.WARNING,"Failed to remove slave: ", e);
} finally {
// Remove the instance even if deletion failed, otherwise it will hang around forever in
// the nodes page. One way for this to occur is that an instance was terminated
// manually or a spot instance was killed due to pricing. If we don't remove the node,
// we screw up auto-scaling, since it will continue to count against the quota.
try {
Jenkins.getInstance().removeNode(this);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to remove slave: " + name, e);
}
}
}
/**
* Retrieve the SpotRequest for a requestId
*
* @return SpotInstanceRequest object for this slave, or null
*/
SpotInstanceRequest getSpotRequest() {
AmazonEC2 ec2 = getCloud().connect();
DescribeSpotInstanceRequestsRequest dsirRequest = new DescribeSpotInstanceRequestsRequest().withSpotInstanceRequestIds(this.spotInstanceRequestId);
DescribeSpotInstanceRequestsResult dsirResult = null;
List<SpotInstanceRequest> siRequests = null;
try {
dsirResult = ec2.describeSpotInstanceRequests(dsirRequest);
siRequests = dsirResult.getSpotInstanceRequests();
} catch (AmazonServiceException e) {
// Spot request is no longer valid
LOGGER.log(Level.WARNING, "Failed to fetch spot instance request for requestId: " + this.spotInstanceRequestId);
} catch (AmazonClientException e) {
// Spot request is no longer valid
LOGGER.log(Level.WARNING, "Failed to fetch spot instance request for requestId: " + this.spotInstanceRequestId);
}
if (dsirResult == null || siRequests.isEmpty()) {
return null;
}
return siRequests.get(0);
}
public boolean isSpotRequestDead() {
SpotInstanceState requestState = SpotInstanceState.fromValue(this.getSpotRequest().getState());
return requestState == SpotInstanceState.Cancelled
|| requestState == SpotInstanceState.Closed
|| requestState == SpotInstanceState.Failed;
}
/**
* Accessor for the spotInstanceRequestId
*/
public String getSpotInstanceRequestId() {
return spotInstanceRequestId;
}
@Override
public String getInstanceId() {
if (instanceId == null || instanceId.equals("")) {
SpotInstanceRequest sr = this.getSpotRequest();
if (sr != null)
instanceId = sr.getInstanceId();
}
return instanceId;
}
@Override
public void onConnected() {
// The spot request has been fulfilled and is connected. If the Spot
// request had tags, we want those on the instance.
pushLiveInstancedata();
}
@Extension
public static final class DescriptorImpl extends EC2AbstractSlave.DescriptorImpl {
@Override
public String getDisplayName() {
return Messages.EC2SpotSlave_AmazonEC2SpotInstance();
}
}
@Override
public String getEc2Type() {
String spotMaxBidPrice = this.getSpotRequest().getSpotPrice();
return Messages.EC2SpotSlave_Spot1() + spotMaxBidPrice.substring(0, spotMaxBidPrice.length() - 3)
+ Messages.EC2SpotSlave_Spot2();
}
}