package hudson.plugins.ec2.one; import hudson.Extension; import hudson.Util; import hudson.model.Computer; import hudson.model.Descriptor.FormException; import hudson.model.Hudson; import hudson.model.Node; import hudson.model.Slave; import hudson.plugins.ec2.EC2RetentionStrategy; import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.ssh.EC2UnixLauncher; import hudson.slaves.NodeProperty; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.CreateTagsRequest; import com.amazonaws.services.ec2.model.DeleteTagsRequest; import com.amazonaws.services.ec2.model.DescribeInstancesRequest; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.InstanceStateName; import com.amazonaws.services.ec2.model.Reservation; import com.amazonaws.services.ec2.model.StopInstancesRequest; import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; /** * Slave running on EC2. * * @author Kohsuke Kawaguchi */ public final class OneEC2Slave extends Slave { /** * Comes from {@link SlaveTemplate#initScript}. */ public final String initScript; public final String remoteAdmin; // e.g. 'ubuntu' public final String rootCommandPrefix; // e.g. 'sudo' public final String jvmopts; // e.g. -Xmx1g public final boolean stopOnTerminate; public final String idleTerminationMinutes; public final boolean usePrivateDnsName; public List tags; // Temporary stuff that is obtained live from EC2 public String publicDNS; public String privateDNS; /* The last instance data to be fetched for the slave */ private Instance lastFetchInstance = null; /* The time at which we fetched the last instance data */ private long lastFetchTime = 0; /* * The time (in milliseconds) after which we will always re-fetch externally * changeable EC2 data when we are asked for it */ private static final long MIN_FETCH_TIME = 20 * 1000; /** * For data read from old Hudson, this is 0, so we use that to indicate 22. */ private final int sshPort; public static final String TEST_ZONE = "testZone"; public OneEC2Slave(final String instanceId, final String description, final String remoteFS, final int sshPort, final int numExecutors, final String labelString, final String initScript, final String remoteAdmin, final String rootCommandPrefix, final String jvmopts, final boolean stopOnTerminate, final String idleTerminationMinutes, final String publicDNS, final String privateDNS, final List tags) throws FormException, IOException { this(instanceId, description, remoteFS, sshPort, numExecutors, Mode.NORMAL, labelString, initScript, Collections .> emptyList(), remoteAdmin, rootCommandPrefix, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, false); } public OneEC2Slave(final String instanceId, final String description, final String remoteFS, final int sshPort, final int numExecutors, final String labelString, final String initScript, final String remoteAdmin, final String rootCommandPrefix, final String jvmopts, final boolean stopOnTerminate, final String idleTerminationMinutes, final String publicDNS, final String privateDNS, final List tags, final boolean usePrivateDnsName) throws FormException, IOException { this(instanceId, description, remoteFS, sshPort, numExecutors, Mode.NORMAL, labelString, initScript, Collections .> emptyList(), remoteAdmin, rootCommandPrefix, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, usePrivateDnsName); } @DataBoundConstructor public OneEC2Slave(final String instanceId, final String description, final String remoteFS, final int sshPort, final int numExecutors, final Mode mode, final String labelString, final String initScript, final List> nodeProperties, final String remoteAdmin, final String rootCommandPrefix, final String jvmopts, final boolean stopOnTerminate, final String idleTerminationMinutes, final String publicDNS, final String privateDNS, final List tags, final boolean usePrivateDnsName) throws FormException, IOException { super(instanceId, description, remoteFS, numExecutors, mode, labelString, new EC2UnixLauncher(), new EC2RetentionStrategy( idleTerminationMinutes), nodeProperties); this.initScript = initScript; this.remoteAdmin = remoteAdmin; this.rootCommandPrefix = rootCommandPrefix; this.jvmopts = jvmopts; this.sshPort = sshPort; this.stopOnTerminate = stopOnTerminate; this.idleTerminationMinutes = idleTerminationMinutes; this.publicDNS = publicDNS; this.privateDNS = privateDNS; this.tags = tags; this.usePrivateDnsName = usePrivateDnsName; } /** * Constructor for debugging. */ public OneEC2Slave(final String instanceId) throws FormException, IOException { this(instanceId, "debug", "/tmp/hudson", 22, 1, Mode.NORMAL, "debug", "", Collections.> emptyList(), null, null, null, false, null, "Fake public", "Fake private", null, false); } /** * See http://aws.amazon.com/ec2/instance-types/ */ /* package */static int toNumExecutors(final OneInstanceType type) { switch (type) { case M0: return 1; case M1: return 2; case M2: return 3; case M3: return 4; case M4: return 8; case M1Jenkins: return 2; case M2Jenkins: return 4; default: throw new AssertionError(); } } /** * EC2 instance ID. */ public String getInstanceId() { return getNodeName(); } @Override public Computer createComputer() { return new OneEC2Computer(this); } public static Instance getInstance(final String instanceId) { DescribeInstancesRequest request = new DescribeInstancesRequest(); request.setInstanceIds(Collections . singletonList(instanceId)); OneEC2Cloud cloudInstance = OneEC2Cloud.get(); if (cloudInstance == null) { return null; } AmazonEC2 ec2 = cloudInstance.connect(); List reservations = ec2.describeInstances(request).getReservations(); Instance i = null; if (reservations.size() > 0) { List instances = reservations.get(0).getInstances(); if (instances.size() > 0) { i = instances.get(0); } } return i; } /** * Terminates the instance in EC2. */ public void terminate() { try { if (!isAlive(true)) { /* * The node has been killed externally, so we've nothing to do * here */ LOGGER.info("EC2 instance already terminated: " + getInstanceId()); } else { AmazonEC2 ec2 = OneEC2Cloud.get().connect(); TerminateInstancesRequest request = new TerminateInstancesRequest( Collections.singletonList(getInstanceId())); ec2.terminateInstances(request); LOGGER.info("Terminated EC2 instance (terminated): " + getInstanceId()); } Hudson.getInstance().removeNode(this); } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId(), e); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId(), e); } } void idleTimeout() { LOGGER.info("EC2 instance idle time expired: " + getInstanceId()); if (!stopOnTerminate) { terminate(); return; } try { AmazonEC2 ec2 = OneEC2Cloud.get().connect(); StopInstancesRequest request = new StopInstancesRequest( Collections.singletonList(getInstanceId())); ec2.stopInstances(request); toComputer().disconnect(null); } catch (AmazonClientException e) { Instance i = getInstance(getNodeName()); LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId() + " info: " + ((i != null) ? i : ""), e); } LOGGER.info("EC2 instance stopped: " + getInstanceId()); } String getRemoteAdmin() { if (remoteAdmin == null || remoteAdmin.length() == 0) { return "root"; } return remoteAdmin; } String getRootCommandPrefix() { if (rootCommandPrefix == null || rootCommandPrefix.length() == 0) { return ""; } return rootCommandPrefix + " "; } String getJvmopts() { return Util.fixNull(jvmopts); } public int getSshPort() { return sshPort != 0 ? sshPort : 22; } public boolean getStopOnTerminate() { return stopOnTerminate; } private boolean isAlive(final boolean force) { fetchLiveInstanceData(force); if (lastFetchInstance == null) { return false; } if (lastFetchInstance.getState().getName() .equals(InstanceStateName.Terminated.toString())) { return false; } return true; } /* * Much of the EC2 data is beyond our direct control, therefore we need to * refresh it from time to time to ensure we reflect the reality of the * instances. */ private void fetchLiveInstanceData(final boolean force) throws AmazonClientException { /* * If we've grabbed the data recently, don't bother getting it again * unless we are forced */ long now = System.currentTimeMillis(); if ((lastFetchTime > 0) && (now - lastFetchTime < MIN_FETCH_TIME) && !force) { return; } Instance i = getInstance(getNodeName()); lastFetchTime = now; lastFetchInstance = i; if (i == null) { return; } publicDNS = i.getPublicDnsName(); privateDNS = i.getPrivateIpAddress(); tags = new LinkedList(); for (Tag t : i.getTags()) { tags.add(new OneEC2Tag(t.getKey(), t.getValue())); } } /* * Clears all existing tag data so that we can force the instance into a * known state */ private void clearLiveInstancedata() throws AmazonClientException { Instance inst = getInstance(getNodeName()); /* Now that we have our instance, we can clear the tags on it */ if (!tags.isEmpty()) { HashSet inst_tags = new HashSet(); for (OneEC2Tag t : tags) { inst_tags.add(new Tag(t.getName(), t.getValue())); } DeleteTagsRequest tag_request = new DeleteTagsRequest(); tag_request.withResources(inst.getInstanceId()).setTags( inst_tags); OneEC2Cloud.get().connect().deleteTags(tag_request); } } /* * Sets tags on an instance. This will not clear existing tag data, so call * clearLiveInstancedata if needed */ private void pushLiveInstancedata() throws AmazonClientException { Instance inst = getInstance(getNodeName()); /* Now that we have our instance, we can set tags on it */ if (tags != null && !tags.isEmpty()) { HashSet inst_tags = new HashSet(); for (OneEC2Tag t : tags) { inst_tags.add(new Tag(t.getName(), t.getValue())); } CreateTagsRequest tag_request = new CreateTagsRequest(); tag_request.withResources(inst.getInstanceId()).setTags( inst_tags); OneEC2Cloud.get().connect().createTags(tag_request); } } public String getPublicDNS() { fetchLiveInstanceData(false); return publicDNS; } public String getPrivateDNS() { fetchLiveInstanceData(false); return privateDNS; } public List getTags() { fetchLiveInstanceData(false); return Collections.unmodifiableList(tags); } @Override public Node reconfigure(final StaplerRequest req, final JSONObject form) throws FormException { if (form == null) { return null; } if (!isAlive(true)) { LOGGER.info("EC2 instance terminated externally: " + getInstanceId()); try { Hudson.getInstance().removeNode(this); } catch (IOException ioe) { LOGGER .log( Level.WARNING, "Attempt to reconfigure EC2 instance which has been externally terminated: " + getInstanceId(), ioe); } return null; } Node result = super.reconfigure(req, form); /* Get rid of the old tags, as represented by ourselves. */ clearLiveInstancedata(); /* Set the new tags, as represented by our successor */ ((OneEC2Slave) result).pushLiveInstancedata(); return result; } public boolean getUsePrivateDnsName() { return usePrivateDnsName; } @Extension public static final class DescriptorImpl extends SlaveDescriptor { @Override public String getDisplayName() { return "OpenNebula"; } @Override public boolean isInstantiable() { return false; } } private static final Logger LOGGER = Logger .getLogger(OneEC2Slave.class.getName()); }