hudson.plugins.ec2.EC2AbstractSlave.java Source code

Java tutorial

Introduction

Here is the source code for hudson.plugins.ec2.EC2AbstractSlave.java

Source

/*
 * 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 hudson.Util;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.Descriptor.FormException;
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.List;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletException;

import net.sf.json.JSONObject;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
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.*;

/**
 * Slave running on EC2.
 *
 * @author Kohsuke Kawaguchi
 */
public abstract class EC2AbstractSlave extends Slave {
    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 jvmopts; //e.g. -Xmx1g
    public final boolean stopOnTerminate;
    public final String idleTerminationMinutes;
    public final boolean usePrivateDnsName;
    public final boolean useDedicatedTenancy;
    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";

    @DataBoundConstructor
    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.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();
    }

    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) Hudson.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 M1Large:
            return 4;
        case C1Medium:
            return 5;
        case M2Xlarge:
            return 6;
        case C3Large:
            return 7;
        case M1Xlarge:
            return 8;
        case M22xlarge:
            return 13;
        case M3Xlarge:
            return 13;
        case C3Xlarge:
            return 14;
        case C1Xlarge:
            return 20;
        case M24xlarge:
            return 26;
        case M32xlarge:
            return 26;
        case G22xlarge:
            return 26;
        case C32xlarge:
            return 28;
        case Cc14xlarge:
            return 33;
        case Cg14xlarge:
            return 33;
        case Hi14xlarge:
            return 35;
        case Hs18xlarge:
            return 35;
        case C34xlarge:
            return 55;
        case Cc28xlarge:
            return 88;
        case Cr18xlarge:
            return 88;
        case C38xlarge:
            return 108;
        //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) {
        Instance i = null;
        try {
            DescribeInstancesRequest request = new DescribeInstancesRequest();
            request.setInstanceIds(Collections.<String>singletonList(instanceId));
            if (cloud == null)
                return null;
            AmazonEC2 ec2 = cloud.connect();
            List<Reservation> reservations = ec2.describeInstances(request).getReservations();
            if (reservations.size() > 0) {
                List<Instance> instances = reservations.get(0).getInstances();
                if (instances.size() > 0)
                    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() {
        // Do nothing by default.
    }

    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> inst_tags = new HashSet<Tag>();

            for (EC2Tag t : tags) {
                inst_tags.add(new Tag(t.getName(), t.getValue()));
            }

            DeleteTagsRequest tag_request = new DeleteTagsRequest();
            tag_request.withResources(inst.getInstanceId()).setTags(inst_tags);
            getCloud().connect().deleteTags(tag_request);
        }
    }

    /* 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 (tags != null && !tags.isEmpty()) {
            HashSet<Tag> inst_tags = new HashSet<Tag>();

            for (EC2Tag t : tags) {
                inst_tags.add(new Tag(t.getName(), t.getValue()));
            }

            CreateTagsRequest tag_request = new CreateTagsRequest();
            tag_request.withResources(inst.getInstanceId()).setTags(inst_tags);
            getCloud().connect().createTags(tag_request);
        }
    }

    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 String getAdminPassword() {
        return amiType.isWindows() ? ((WindowsData) amiType).getPassword() : "";
    }

    public boolean isUseHTTPS() {
        return amiType.isWindows() ? ((WindowsData) amiType).isUseHTTPS() : false;
    }

    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 accessId, @QueryParameter String secretKey, @QueryParameter String region) {
            AWSCredentialsProvider credentialsProvider = EC2Cloud
                    .createCredentialsProvider(useInstanceProfileForCredentials, accessId, secretKey);
            return fillZoneItems(credentialsProvider, region);
        }

        public List<Descriptor<AMITypeData>> getAMITypeDescriptors() {
            return Hudson.getInstance().<AMITypeData, Descriptor<AMITypeData>>getDescriptorList(AMITypeData.class);
        }
    }

    private static final Logger LOGGER = Logger.getLogger(EC2AbstractSlave.class.getName());

}