/* * The MIT License * * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.ec2; import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Util; import hudson.model.Computer; import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; import hudson.model.Hudson; import hudson.model.Node; import hudson.model.Slave; import hudson.slaves.NodeProperty; import hudson.slaves.ComputerLauncher; import hudson.slaves.RetentionStrategy; import hudson.util.ListBoxModel; 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 hudson.util.Secret; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import com.amazonaws.AmazonClientException; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.AvailabilityZone; import com.amazonaws.services.ec2.model.CreateTagsRequest; import com.amazonaws.services.ec2.model.DeleteTagsRequest; import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult; 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.InstanceType; 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 */ @SuppressWarnings("serial") public abstract class EC2AbstractSlave extends Slave { private static final Logger LOGGER = Logger.getLogger(EC2AbstractSlave.class.getName()); protected String instanceId; /** * Comes from {@link SlaveTemplate#initScript}. */ public final String initScript; public final String tmpDir; public final String remoteAdmin; // e.g. 'ubuntu' public final String templateDescription; public final String jvmopts; // e.g. -Xmx1g public final boolean stopOnTerminate; public final String idleTerminationMinutes; public final boolean usePrivateDnsName; public final boolean useDedicatedTenancy; public boolean isConnected = false; public List<EC2Tag> tags; public final String cloudName; public AMITypeData amiType; // Temporary stuff that is obtained live from EC2 public transient String publicDNS; public transient String privateDNS; /* The last instance data to be fetched for the slave */ protected transient Instance lastFetchInstance = null; /* The time at which we fetched the last instance data */ protected transient long lastFetchTime; /* * The time (in milliseconds) after which we will always re-fetch externally changeable EC2 data when we are asked * for it */ protected static final long MIN_FETCH_TIME = 20 * 1000; protected final int launchTimeout; // Deprecated by the AMITypeData data structure @Deprecated protected transient int sshPort; @Deprecated public transient String rootCommandPrefix; // e.g. 'sudo' private transient long createdTime; public static final String TEST_ZONE = "testZone"; public EC2AbstractSlave(String name, String instanceId, String description, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy<EC2Computer> retentionStrategy, String initScript, String tmpDir, List<? extends NodeProperty<?>> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List<EC2Tag> tags, String cloudName, boolean usePrivateDnsName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) throws FormException, IOException { super(name, "", remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, nodeProperties); this.instanceId = instanceId; this.templateDescription = description; this.initScript = initScript; this.tmpDir = tmpDir; this.remoteAdmin = remoteAdmin; this.jvmopts = jvmopts; this.stopOnTerminate = stopOnTerminate; this.idleTerminationMinutes = idleTerminationMinutes; this.tags = tags; this.usePrivateDnsName = usePrivateDnsName; this.useDedicatedTenancy = useDedicatedTenancy; this.cloudName = cloudName; this.launchTimeout = launchTimeout; this.amiType = amiType; readResolve(); } @Override protected Object readResolve() { /* * If instanceId is null, this object was deserialized from an old version of the plugin, where this field did * not exist (prior to version 1.18). In those versions, the node name *was* the instance ID, so we can get it * from there. */ if (instanceId == null) { instanceId = getNodeName(); } if (amiType == null) { amiType = new UnixData(rootCommandPrefix, Integer.toString(sshPort)); } return this; } public EC2Cloud getCloud() { return (EC2Cloud) Jenkins.getInstance().getCloud(cloudName); } /** * See http://aws.amazon.com/ec2/instance-types/ */ /* package */static int toNumExecutors(InstanceType it) { switch (it) { case T1Micro: return 1; case M1Small: return 1; case M1Medium: return 2; case M3Medium: return 2; case M1Large: return 4; case M3Large: return 4; case M4Large: return 4; case C1Medium: return 5; case M2Xlarge: return 6; case C3Large: return 7; case C4Large: return 7; case M1Xlarge: return 8; case M22xlarge: return 13; case M3Xlarge: return 13; case M4Xlarge: return 13; case C3Xlarge: return 14; case C4Xlarge: return 14; case C1Xlarge: return 20; case M24xlarge: return 26; case M32xlarge: return 26; case M42xlarge: return 26; case G22xlarge: return 26; case C32xlarge: return 28; case C42xlarge: return 28; case Cc14xlarge: return 33; case Cg14xlarge: return 33; case Hi14xlarge: return 35; case Hs18xlarge: return 35; case C34xlarge: return 55; case C44xlarge: return 55; case M44xlarge: return 55; case Cc28xlarge: return 88; case Cr18xlarge: return 88; case C38xlarge: return 108; case C48xlarge: return 108; case M410xlarge: return 120; // We don't have a suggestion, but we don't want to fail completely // surely? default: return 1; } } /** * EC2 instance ID. */ public String getInstanceId() { return instanceId; } @Override public Computer createComputer() { return new EC2Computer(this); } public static Instance getInstance(String instanceId, EC2Cloud cloud) { if (instanceId == null || instanceId == "" || cloud == null) return null; Instance i = null; try { DescribeInstancesRequest request = new DescribeInstancesRequest(); request.setInstanceIds(Collections.<String> singletonList(instanceId)); AmazonEC2 ec2 = cloud.connect(); List<Reservation> reservations = ec2.describeInstances(request).getReservations(); if (!reservations.isEmpty()) { List<Instance> instances = reservations.get(0).getInstances(); if (!instances.isEmpty()) { i = instances.get(0); } } } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to fetch EC2 instance: " + instanceId, e); } return i; } /** * Terminates the instance in EC2. */ public abstract void terminate(); void stop() { try { AmazonEC2 ec2 = getCloud().connect(); StopInstancesRequest request = new StopInstancesRequest(Collections.singletonList(getInstanceId())); LOGGER.fine("Sending stop request for " + getInstanceId()); ec2.stopInstances(request); LOGGER.info("EC2 instance stop request sent for " + getInstanceId()); toComputer().disconnect(null); } catch (AmazonClientException e) { Instance i = getInstance(getInstanceId(), getCloud()); LOGGER.log(Level.WARNING, "Failed to stop EC2 instance: " + getInstanceId() + " info: " + ((i != null) ? i : ""), e); } } boolean terminateInstance() { try { AmazonEC2 ec2 = getCloud().connect(); TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); LOGGER.fine("Sending terminate request for " + getInstanceId()); ec2.terminateInstances(request); LOGGER.info("EC2 instance terminate request sent for " + getInstanceId()); return true; } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId(), e); return false; } } @Override public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException { if (form == null) { return null; } EC2AbstractSlave result = (EC2AbstractSlave) super.reconfigure(req, form); /* Get rid of the old tags, as represented by ourselves. */ clearLiveInstancedata(); /* Set the new tags, as represented by our successor */ result.pushLiveInstancedata(); return result; } void idleTimeout() { LOGGER.info("EC2 instance idle time expired: " + getInstanceId()); if (!stopOnTerminate) { terminate(); } else { stop(); } } public long getLaunchTimeoutInMillis() { // this should be fine as long as launchTimeout remains an int type return launchTimeout * 1000L; } String getRemoteAdmin() { if (remoteAdmin == null || remoteAdmin.length() == 0) return amiType.isWindows() ? "Administrator" : "root"; return remoteAdmin; } String getRootCommandPrefix() { String commandPrefix = amiType.isUnix() ? ((UnixData) amiType).getRootCommandPrefix() : ""; if (commandPrefix == null || commandPrefix.length() == 0) return ""; return commandPrefix + " "; } String getJvmopts() { return Util.fixNull(jvmopts); } public int getSshPort() { String sshPort = amiType.isUnix() ? ((UnixData) amiType).getSshPort() : "22"; if (sshPort == null || sshPort.length() == 0) return 22; int port = 0; try { port = Integer.parseInt(sshPort); } catch (Exception e) { } return port != 0 ? port : 22; } public boolean getStopOnTerminate() { return stopOnTerminate; } /** * Called when the slave is connected to Jenkins */ public void onConnected() { isConnected = true; } protected boolean isAlive(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. */ protected void fetchLiveInstanceData(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; } if (getInstanceId() == null || getInstanceId() == "") { /* * The getInstanceId() implementation on EC2SpotSlave can return null if the spot request doesn't yet know * the instance id that it is starting. What happens is that null is passed to getInstanceId() which * searches AWS but without an instanceID the search returns some random box. We then fetch its metadata, * including tags, and then later, when the spot request eventually gets the instanceID correctly we push * the saved tags from that random box up to the new spot resulting in confusion and delay. */ return; } Instance i = getInstance(getInstanceId(), getCloud()); lastFetchTime = now; lastFetchInstance = i; if (i == null) return; publicDNS = i.getPublicDnsName(); privateDNS = i.getPrivateIpAddress(); createdTime = i.getLaunchTime().getTime(); tags = new LinkedList<EC2Tag>(); for (Tag t : i.getTags()) { tags.add(new EC2Tag(t.getKey(), t.getValue())); } } /* * Clears all existing tag data so that we can force the instance into a known state */ protected void clearLiveInstancedata() throws AmazonClientException { Instance inst = getInstance(getInstanceId(), getCloud()); /* Now that we have our instance, we can clear the tags on it */ if (!tags.isEmpty()) { HashSet<Tag> instTags = new HashSet<Tag>(); for (EC2Tag t : tags) { instTags.add(new Tag(t.getName(), t.getValue())); } DeleteTagsRequest tagRequest = new DeleteTagsRequest(); tagRequest.withResources(inst.getInstanceId()).setTags(instTags); getCloud().connect().deleteTags(tagRequest); } } /* * Sets tags on an instance. This will not clear existing tag data, so call clearLiveInstancedata if needed */ protected void pushLiveInstancedata() throws AmazonClientException { Instance inst = getInstance(getInstanceId(), getCloud()); /* Now that we have our instance, we can set tags on it */ if (inst != null && tags != null && !tags.isEmpty()) { HashSet<Tag> instTags = new HashSet<Tag>(); for (EC2Tag t : tags) { instTags.add(new Tag(t.getName(), t.getValue())); } CreateTagsRequest tagRequest = new CreateTagsRequest(); tagRequest.withResources(inst.getInstanceId()).setTags(instTags); getCloud().connect().createTags(tagRequest); } } public String getPublicDNS() { fetchLiveInstanceData(false); return publicDNS; } public String getPrivateDNS() { fetchLiveInstanceData(false); return privateDNS; } public List<EC2Tag> getTags() { fetchLiveInstanceData(false); return Collections.unmodifiableList(tags); } public long getCreatedTime() { fetchLiveInstanceData(false); return createdTime; } public boolean getUsePrivateDnsName() { return usePrivateDnsName; } public Secret getAdminPassword() { return amiType.isWindows() ? ((WindowsData) amiType).getPassword() : Secret.fromString(""); } public boolean isUseHTTPS() { return amiType.isWindows() && ((WindowsData) amiType).isUseHTTPS(); } public int getBootDelay() { return amiType.isWindows() ? ((WindowsData) amiType).getBootDelayInMillis() : 0; } public static ListBoxModel fillZoneItems(AWSCredentialsProvider credentialsProvider, String region) { ListBoxModel model = new ListBoxModel(); if (AmazonEC2Cloud.testMode) { model.add(TEST_ZONE); return model; } if (!StringUtils.isEmpty(region)) { AmazonEC2 client = EC2Cloud.connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); DescribeAvailabilityZonesResult zones = client.describeAvailabilityZones(); List<AvailabilityZone> zoneList = zones.getAvailabilityZones(); model.add("<not specified>", ""); for (AvailabilityZone z : zoneList) { model.add(z.getZoneName(), z.getZoneName()); } } return model; } /* * Used to determine if the slave is On Demand or Spot */ abstract public String getEc2Type(); public static abstract class DescriptorImpl extends SlaveDescriptor { @Override public abstract String getDisplayName(); @Override public boolean isInstantiable() { return false; } public ListBoxModel doFillZoneItems(@QueryParameter boolean useInstanceProfileForCredentials, @QueryParameter String credentialsId, @QueryParameter String region) { AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); return fillZoneItems(credentialsProvider, region); } public List<Descriptor<AMITypeData>> getAMITypeDescriptors() { return Jenkins.getInstance().<AMITypeData, Descriptor<AMITypeData>> getDescriptorList(AMITypeData.class); } } }