|
@@ -0,0 +1,665 @@
|
|
|
|
+package hudson.plugins.ec2.one;
|
|
|
|
+
|
|
|
|
+import hudson.Extension;
|
|
|
|
+import hudson.Util;
|
|
|
|
+import hudson.model.Describable;
|
|
|
|
+import hudson.model.TaskListener;
|
|
|
|
+import hudson.model.Descriptor;
|
|
|
|
+import hudson.model.Descriptor.FormException;
|
|
|
|
+import hudson.model.Hudson;
|
|
|
|
+import hudson.model.Label;
|
|
|
|
+import hudson.model.Node;
|
|
|
|
+import hudson.model.labels.LabelAtom;
|
|
|
|
+import hudson.plugins.ec2.EC2Computer;
|
|
|
|
+import hudson.plugins.ec2.EC2Slave;
|
|
|
|
+import hudson.util.FormValidation;
|
|
|
|
+
|
|
|
|
+import java.io.IOException;
|
|
|
|
+import java.io.PrintStream;
|
|
|
|
+import java.net.URL;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.Arrays;
|
|
|
|
+import java.util.Collections;
|
|
|
|
+import java.util.HashSet;
|
|
|
|
+import java.util.LinkedList;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.Set;
|
|
|
|
+
|
|
|
|
+import javax.servlet.ServletException;
|
|
|
|
+
|
|
|
|
+import org.apache.commons.codec.binary.Base64;
|
|
|
|
+import org.apache.commons.lang.StringUtils;
|
|
|
|
+import org.kohsuke.stapler.DataBoundConstructor;
|
|
|
|
+import org.kohsuke.stapler.QueryParameter;
|
|
|
|
+
|
|
|
|
+import com.amazonaws.AmazonClientException;
|
|
|
|
+import com.amazonaws.services.ec2.AmazonEC2;
|
|
|
|
+import com.amazonaws.services.ec2.model.CreateTagsRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeImagesRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeInstancesResult;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeSubnetsRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.DescribeSubnetsResult;
|
|
|
|
+import com.amazonaws.services.ec2.model.Filter;
|
|
|
|
+import com.amazonaws.services.ec2.model.Image;
|
|
|
|
+import com.amazonaws.services.ec2.model.Instance;
|
|
|
|
+import com.amazonaws.services.ec2.model.InstanceStateName;
|
|
|
|
+import com.amazonaws.services.ec2.model.KeyPair;
|
|
|
|
+import com.amazonaws.services.ec2.model.Placement;
|
|
|
|
+import com.amazonaws.services.ec2.model.Reservation;
|
|
|
|
+import com.amazonaws.services.ec2.model.RunInstancesRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.SecurityGroup;
|
|
|
|
+import com.amazonaws.services.ec2.model.StartInstancesRequest;
|
|
|
|
+import com.amazonaws.services.ec2.model.StartInstancesResult;
|
|
|
|
+import com.amazonaws.services.ec2.model.Tag;
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Template of {@link EC2Slave} to launch.
|
|
|
|
+ *
|
|
|
|
+ * @author Kohsuke Kawaguchi
|
|
|
|
+ */
|
|
|
|
+public class OneSlaveTemplate implements Describable<OneSlaveTemplate> {
|
|
|
|
+ public final String ami;
|
|
|
|
+
|
|
|
|
+ public final String description;
|
|
|
|
+
|
|
|
|
+ public final String zone;
|
|
|
|
+
|
|
|
|
+ public final String securityGroups;
|
|
|
|
+
|
|
|
|
+ public final String remoteFS;
|
|
|
|
+
|
|
|
|
+ public final String sshPort;
|
|
|
|
+
|
|
|
|
+ public final OneInstanceType type;
|
|
|
|
+
|
|
|
|
+ public final String labels;
|
|
|
|
+
|
|
|
|
+ public final String initScript;
|
|
|
|
+
|
|
|
|
+ public final String userData;
|
|
|
|
+
|
|
|
|
+ public final String numExecutors;
|
|
|
|
+
|
|
|
|
+ public final String remoteAdmin;
|
|
|
|
+
|
|
|
|
+ public final String rootCommandPrefix;
|
|
|
|
+
|
|
|
|
+ public final String jvmopts;
|
|
|
|
+
|
|
|
|
+ public final String subnetId;
|
|
|
|
+
|
|
|
|
+ public final String idleTerminationMinutes;
|
|
|
|
+
|
|
|
|
+ public final int instanceCap;
|
|
|
|
+
|
|
|
|
+ public final boolean stopOnTerminate;
|
|
|
|
+
|
|
|
|
+ private final List<OneEC2Tag> tags;
|
|
|
|
+
|
|
|
|
+ public final boolean usePrivateDnsName;
|
|
|
|
+
|
|
|
|
+ protected transient OneEC2Cloud parent;
|
|
|
|
+
|
|
|
|
+ private transient/* almost final */Set<LabelAtom> labelSet;
|
|
|
|
+
|
|
|
|
+ private transient/* almost final */Set<String> securityGroupSet;
|
|
|
|
+
|
|
|
|
+ @DataBoundConstructor
|
|
|
|
+ public OneSlaveTemplate(final String ami, final String zone,
|
|
|
|
+ final String securityGroups, final String remoteFS,
|
|
|
|
+ final String sshPort, final OneInstanceType type,
|
|
|
|
+ final String labelString, final String description,
|
|
|
|
+ final String initScript, final String userData,
|
|
|
|
+ final String numExecutors, final String remoteAdmin,
|
|
|
|
+ final String rootCommandPrefix, final String jvmopts,
|
|
|
|
+ final boolean stopOnTerminate, final String subnetId,
|
|
|
|
+ final List<OneEC2Tag> tags,
|
|
|
|
+ final String idleTerminationMinutes,
|
|
|
|
+ final boolean usePrivateDnsName, final String instanceCapStr) {
|
|
|
|
+ this.ami = ami;
|
|
|
|
+ this.zone = zone;
|
|
|
|
+ this.securityGroups = securityGroups;
|
|
|
|
+ this.remoteFS = remoteFS;
|
|
|
|
+ this.sshPort = sshPort;
|
|
|
|
+ this.type = type;
|
|
|
|
+ labels = Util.fixNull(labelString);
|
|
|
|
+ this.description = description;
|
|
|
|
+ this.initScript = initScript;
|
|
|
|
+ this.userData = userData;
|
|
|
|
+ this.numExecutors = Util.fixNull(numExecutors).trim();
|
|
|
|
+ this.remoteAdmin = remoteAdmin;
|
|
|
|
+ this.rootCommandPrefix = rootCommandPrefix;
|
|
|
|
+ this.jvmopts = jvmopts;
|
|
|
|
+ this.stopOnTerminate = stopOnTerminate;
|
|
|
|
+ this.subnetId = subnetId;
|
|
|
|
+ this.tags = tags;
|
|
|
|
+ this.idleTerminationMinutes = idleTerminationMinutes;
|
|
|
|
+ this.usePrivateDnsName = usePrivateDnsName;
|
|
|
|
+
|
|
|
|
+ if (null == instanceCapStr || instanceCapStr.equals("")) {
|
|
|
|
+ instanceCap = Integer.MAX_VALUE;
|
|
|
|
+ } else {
|
|
|
|
+ instanceCap = Integer.parseInt(instanceCapStr);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ readResolve(); // initialize
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public OneEC2Cloud getParent() {
|
|
|
|
+ return parent;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getLabelString() {
|
|
|
|
+ return labels;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getDisplayName() {
|
|
|
|
+ return description + " (" + ami + ")";
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ String getZone() {
|
|
|
|
+ return zone;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getSecurityGroupString() {
|
|
|
|
+ return securityGroups;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Set<String> getSecurityGroupSet() {
|
|
|
|
+ return securityGroupSet;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Set<String> parseSecurityGroups() {
|
|
|
|
+ if (securityGroups == null || "".equals(securityGroups.trim())) {
|
|
|
|
+ return Collections.emptySet();
|
|
|
|
+ } else {
|
|
|
|
+ return new HashSet<String>(Arrays.asList(securityGroups
|
|
|
|
+ .split("\\s*,\\s*")));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public int getNumExecutors() {
|
|
|
|
+ try {
|
|
|
|
+ return Integer.parseInt(numExecutors);
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
+ return OneEC2Slave.toNumExecutors(type);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public int getSshPort() {
|
|
|
|
+ try {
|
|
|
|
+ return Integer.parseInt(sshPort);
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
+ return 22;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getRemoteAdmin() {
|
|
|
|
+ return remoteAdmin;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getRootCommandPrefix() {
|
|
|
|
+ return rootCommandPrefix;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getSubnetId() {
|
|
|
|
+ return subnetId;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public List<OneEC2Tag> getTags() {
|
|
|
|
+ if (null == tags) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ return Collections.unmodifiableList(tags);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getidleTerminationMinutes() {
|
|
|
|
+ return idleTerminationMinutes;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Set<LabelAtom> getLabelSet() {
|
|
|
|
+ return labelSet;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public int getInstanceCap() {
|
|
|
|
+ return instanceCap;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String getInstanceCapStr() {
|
|
|
|
+ if (instanceCap == Integer.MAX_VALUE) {
|
|
|
|
+ return "";
|
|
|
|
+ } else {
|
|
|
|
+ return String.valueOf(instanceCap);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Does this contain the given label?
|
|
|
|
+ *
|
|
|
|
+ * @param l
|
|
|
|
+ * can be null to indicate "don't care".
|
|
|
|
+ */
|
|
|
|
+ public boolean containsLabel(final Label l) {
|
|
|
|
+ return l == null || labelSet.contains(l);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Provisions a new EC2 slave.
|
|
|
|
+ *
|
|
|
|
+ * @return always non-null. This needs to be then added to
|
|
|
|
+ * {@link Hudson#addNode(Node)}.
|
|
|
|
+ */
|
|
|
|
+ public OneEC2Slave provision(final TaskListener listener)
|
|
|
|
+ throws AmazonClientException, IOException {
|
|
|
|
+ PrintStream logger = listener.getLogger();
|
|
|
|
+ AmazonEC2 ec2 = getParent().connect();
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ logger.println("Launching " + ami);
|
|
|
|
+ KeyPair keyPair =
|
|
|
|
+ (null != parent.getPrivateKey()) ? parent.getPrivateKey()
|
|
|
|
+ .find(ec2) : null;
|
|
|
|
+
|
|
|
|
+ RunInstancesRequest riRequest =
|
|
|
|
+ new RunInstancesRequest(ami, 1, 1);
|
|
|
|
+
|
|
|
|
+ List<Filter> diFilters = new ArrayList<Filter>();
|
|
|
|
+ diFilters.add(new Filter("image-id").withValues(ami));
|
|
|
|
+
|
|
|
|
+ if (StringUtils.isNotBlank(getZone())) {
|
|
|
|
+ Placement placement = new Placement(getZone());
|
|
|
|
+ riRequest.setPlacement(placement);
|
|
|
|
+ diFilters.add(new Filter("availability-zone")
|
|
|
|
+ .withValues(getZone()));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (StringUtils.isNotBlank(getSubnetId())) {
|
|
|
|
+ riRequest.setSubnetId(getSubnetId());
|
|
|
|
+ diFilters.add(new Filter("subnet-id")
|
|
|
|
+ .withValues(getSubnetId()));
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * If we have a subnet ID then we can only use VPC security
|
|
|
|
+ * groups
|
|
|
|
+ */
|
|
|
|
+ if (!securityGroupSet.isEmpty()) {
|
|
|
|
+ List<String> group_ids = new ArrayList<String>();
|
|
|
|
+
|
|
|
|
+ DescribeSecurityGroupsRequest group_req =
|
|
|
|
+ new DescribeSecurityGroupsRequest();
|
|
|
|
+ group_req.withFilters(new Filter("group-name")
|
|
|
|
+ .withValues(securityGroupSet));
|
|
|
|
+ DescribeSecurityGroupsResult group_result =
|
|
|
|
+ ec2.describeSecurityGroups(group_req);
|
|
|
|
+
|
|
|
|
+ for (SecurityGroup group : group_result
|
|
|
|
+ .getSecurityGroups()) {
|
|
|
|
+ if (group.getVpcId() != null
|
|
|
|
+ && !group.getVpcId().isEmpty()) {
|
|
|
|
+ List<Filter> filters = new ArrayList<Filter>();
|
|
|
|
+ filters.add(new Filter("vpc-id")
|
|
|
|
+ .withValues(group.getVpcId()));
|
|
|
|
+ filters.add(new Filter("state")
|
|
|
|
+ .withValues("available"));
|
|
|
|
+ filters.add(new Filter("subnet-id")
|
|
|
|
+ .withValues(getSubnetId()));
|
|
|
|
+
|
|
|
|
+ DescribeSubnetsRequest subnet_req =
|
|
|
|
+ new DescribeSubnetsRequest();
|
|
|
|
+ subnet_req.withFilters(filters);
|
|
|
|
+ DescribeSubnetsResult subnet_result =
|
|
|
|
+ ec2.describeSubnets(subnet_req);
|
|
|
|
+
|
|
|
|
+ List subnets = subnet_result.getSubnets();
|
|
|
|
+ if (subnets != null && !subnets.isEmpty()) {
|
|
|
|
+ group_ids.add(group.getGroupId());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (securityGroupSet.size() != group_ids.size()) {
|
|
|
|
+ throw new AmazonClientException(
|
|
|
|
+ "Security groups must all be VPC security groups to work in a VPC context");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!group_ids.isEmpty()) {
|
|
|
|
+ riRequest.setSecurityGroupIds(group_ids);
|
|
|
|
+ diFilters.add(new Filter("instance.group-id")
|
|
|
|
+ .withValues(group_ids));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ /* No subnet: we can use standard security groups by name */
|
|
|
|
+ riRequest.setSecurityGroups(securityGroupSet);
|
|
|
|
+ if (securityGroupSet.size() > 0) {
|
|
|
|
+ diFilters.add(new Filter("group-name")
|
|
|
|
+ .withValues(securityGroupSet));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (null != userData) {
|
|
|
|
+ String userDataString =
|
|
|
|
+ Base64.encodeBase64String(userData.getBytes());
|
|
|
|
+ riRequest.setUserData(userDataString);
|
|
|
|
+ }
|
|
|
|
+ if (null != keyPair) {
|
|
|
|
+ riRequest.setKeyName(keyPair.getKeyName());
|
|
|
|
+ diFilters.add(new Filter("key-name").withValues(keyPair
|
|
|
|
+ .getKeyName()));
|
|
|
|
+ }
|
|
|
|
+ riRequest.setInstanceType(type.toString());
|
|
|
|
+ diFilters.add(new Filter("instance-type").withValues(type
|
|
|
|
+ .toString()));
|
|
|
|
+
|
|
|
|
+ HashSet<Tag> inst_tags = null;
|
|
|
|
+ if (tags != null && !tags.isEmpty()) {
|
|
|
|
+ inst_tags = new HashSet<Tag>();
|
|
|
|
+ for (OneEC2Tag t : tags) {
|
|
|
|
+ diFilters.add(new Filter("tag:" + t.getName())
|
|
|
|
+ .withValues(t.getValue()));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ DescribeInstancesRequest diRequest =
|
|
|
|
+ new DescribeInstancesRequest();
|
|
|
|
+ diFilters.add(new Filter("instance-state-name").withValues(
|
|
|
|
+ InstanceStateName.Stopped.toString(),
|
|
|
|
+ InstanceStateName.Stopping.toString()));
|
|
|
|
+ diRequest.setFilters(diFilters);
|
|
|
|
+ logger.println("Looking for existing instances: " + diRequest);
|
|
|
|
+
|
|
|
|
+ DescribeInstancesResult diResult =
|
|
|
|
+ ec2.describeInstances(diRequest);
|
|
|
|
+
|
|
|
|
+ filterReservations(diResult.getReservations(), diFilters);
|
|
|
|
+
|
|
|
|
+ if (diResult.getReservations().size() == 0) {
|
|
|
|
+ // Have to create a new instance
|
|
|
|
+ Instance inst =
|
|
|
|
+ ec2.runInstances(riRequest).getReservation()
|
|
|
|
+ .getInstances().get(0);
|
|
|
|
+
|
|
|
|
+ /* Now that we have our instance, we can set tags on it */
|
|
|
|
+ if (inst_tags != null) {
|
|
|
|
+ CreateTagsRequest tag_request =
|
|
|
|
+ new CreateTagsRequest();
|
|
|
|
+ tag_request.withResources(inst.getInstanceId())
|
|
|
|
+ .setTags(inst_tags);
|
|
|
|
+ ec2.createTags(tag_request);
|
|
|
|
+
|
|
|
|
+ // That was a remote request - we should also update our
|
|
|
|
+ // local instance data.
|
|
|
|
+ inst.setTags(inst_tags);
|
|
|
|
+ }
|
|
|
|
+ logger.println("No existing instance found - created: "
|
|
|
|
+ + inst);
|
|
|
|
+ return newSlave(inst);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Instance inst =
|
|
|
|
+ diResult.getReservations().get(0).getInstances().get(0);
|
|
|
|
+ logger.println("Found existing stopped instance: " + inst);
|
|
|
|
+ List<String> instances = new ArrayList<String>();
|
|
|
|
+ instances.add(inst.getInstanceId());
|
|
|
|
+ StartInstancesRequest siRequest =
|
|
|
|
+ new StartInstancesRequest(instances);
|
|
|
|
+ StartInstancesResult siResult = ec2.startInstances(siRequest);
|
|
|
|
+ logger.println("Starting existing instance: " + inst
|
|
|
|
+ + " result:" + siResult);
|
|
|
|
+
|
|
|
|
+ List<Node> nodes = Hudson.getInstance().getNodes();
|
|
|
|
+ for (int i = 0, len = nodes.size(); i < len; i++) {
|
|
|
|
+ if (!(nodes.get(i) instanceof EC2Slave)) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+ OneEC2Slave ec2Node = (OneEC2Slave) nodes.get(i);
|
|
|
|
+ if (ec2Node.getInstanceId().equals(inst.getInstanceId())) {
|
|
|
|
+ logger.println("Found existing corresponding: "
|
|
|
|
+ + ec2Node);
|
|
|
|
+ return ec2Node;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Existing slave not found
|
|
|
|
+ logger.println("Creating new slave for existing instance: "
|
|
|
|
+ + inst);
|
|
|
|
+ return newSlave(inst);
|
|
|
|
+
|
|
|
|
+ } catch (FormException e) {
|
|
|
|
+ throw new AssertionError(); // we should have discovered all
|
|
|
|
+ // configuration issues upfront
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private OneEC2Slave newSlave(final Instance inst)
|
|
|
|
+ throws FormException, IOException {
|
|
|
|
+ return new OneEC2Slave(inst.getInstanceId(), description,
|
|
|
|
+ remoteFS, getSshPort(), getNumExecutors(), labels, initScript,
|
|
|
|
+ remoteAdmin, rootCommandPrefix, jvmopts, stopOnTerminate,
|
|
|
|
+ idleTerminationMinutes, inst.getPublicDnsName(),
|
|
|
|
+ inst.getPrivateDnsName(), OneEC2Tag.fromAmazonTags(inst
|
|
|
|
+ .getTags()), usePrivateDnsName);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Provisions a new EC2 slave based on the currently running instance on
|
|
|
|
+ * EC2, instead of starting a new one.
|
|
|
|
+ */
|
|
|
|
+ public OneEC2Slave attach(final String instanceId,
|
|
|
|
+ final TaskListener listener)
|
|
|
|
+ throws AmazonClientException, IOException {
|
|
|
|
+ PrintStream logger = listener.getLogger();
|
|
|
|
+ AmazonEC2 ec2 = getParent().connect();
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ logger.println("Attaching to " + instanceId);
|
|
|
|
+ DescribeInstancesRequest request =
|
|
|
|
+ new DescribeInstancesRequest();
|
|
|
|
+ request.setInstanceIds(Collections.singletonList(instanceId));
|
|
|
|
+ Instance inst =
|
|
|
|
+ ec2.describeInstances(request).getReservations().get(0)
|
|
|
|
+ .getInstances().get(0);
|
|
|
|
+ return newSlave(inst);
|
|
|
|
+ } catch (FormException e) {
|
|
|
|
+ throw new AssertionError(); // we should have discovered all
|
|
|
|
+ // configuration issues upfront
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Initializes data structure that we don't persist.
|
|
|
|
+ */
|
|
|
|
+ protected Object readResolve() {
|
|
|
|
+ labelSet = Label.parse(labels);
|
|
|
|
+ securityGroupSet = parseSecurityGroups();
|
|
|
|
+ return this;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Descriptor<OneSlaveTemplate> getDescriptor() {
|
|
|
|
+ return Hudson.getInstance().getDescriptor(getClass());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Extension
|
|
|
|
+ public static final class DescriptorImpl extends
|
|
|
|
+ Descriptor<OneSlaveTemplate> {
|
|
|
|
+ @Override
|
|
|
|
+ public String getDisplayName() {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Since this shares much of the configuration with {@link EC2Computer},
|
|
|
|
+ * check its help page, too.
|
|
|
|
+ */
|
|
|
|
+ @Override
|
|
|
|
+ public String getHelpFile(final String fieldName) {
|
|
|
|
+ String p = super.getHelpFile(fieldName);
|
|
|
|
+ if (p == null) {
|
|
|
|
+ p =
|
|
|
|
+ Hudson.getInstance().getDescriptor(OneEC2Slave.class)
|
|
|
|
+ .getHelpFile(fieldName);
|
|
|
|
+ }
|
|
|
|
+ return p;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /***
|
|
|
|
+ * Check that the AMI requested is available in the cloud and can be
|
|
|
|
+ * used.
|
|
|
|
+ */
|
|
|
|
+ public FormValidation doValidateAmi(
|
|
|
|
+ @QueryParameter final String url,
|
|
|
|
+ @QueryParameter final String accessId,
|
|
|
|
+ @QueryParameter final String secretKey,
|
|
|
|
+ final @QueryParameter String ami)
|
|
|
|
+ throws IOException, ServletException {
|
|
|
|
+ AmazonEC2 ec2 =
|
|
|
|
+ OneEC2Cloud.connect(accessId, secretKey, new URL(url));
|
|
|
|
+ if (ec2 != null) {
|
|
|
|
+ try {
|
|
|
|
+ List<String> images = new LinkedList<String>();
|
|
|
|
+ images.add(ami);
|
|
|
|
+ List<String> owners = new LinkedList<String>();
|
|
|
|
+ List<String> users = new LinkedList<String>();
|
|
|
|
+ DescribeImagesRequest request =
|
|
|
|
+ new DescribeImagesRequest();
|
|
|
|
+ request.setImageIds(images);
|
|
|
|
+ request.setOwners(owners);
|
|
|
|
+ request.setExecutableUsers(users);
|
|
|
|
+ List<Image> img =
|
|
|
|
+ ec2.describeImages(request).getImages();
|
|
|
|
+ if (img == null || img.isEmpty()) {
|
|
|
|
+ // de-registered AMI causes an empty list to be
|
|
|
|
+ // returned. so be defensive
|
|
|
|
+ // against other possibilities
|
|
|
|
+ return FormValidation
|
|
|
|
+ .error("No such AMI, or not usable with this accessId: "
|
|
|
|
+ + ami);
|
|
|
|
+ }
|
|
|
|
+ return FormValidation.ok(img.get(0).getImageLocation()
|
|
|
|
+ + " by " + img.get(0).getImageOwnerAlias());
|
|
|
|
+ } catch (AmazonClientException e) {
|
|
|
|
+ return FormValidation.error(e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ return FormValidation.ok(); // can't test
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public FormValidation doCheckIdleTerminationMinutes(
|
|
|
|
+ @QueryParameter final String value) {
|
|
|
|
+ if (value == null || value.trim() == "") {
|
|
|
|
+ return FormValidation.ok();
|
|
|
|
+ }
|
|
|
|
+ try {
|
|
|
|
+ int val = Integer.parseInt(value);
|
|
|
|
+ if (val >= 0) {
|
|
|
|
+ return FormValidation.ok();
|
|
|
|
+ }
|
|
|
|
+ } catch (NumberFormatException nfe) {
|
|
|
|
+ }
|
|
|
|
+ return FormValidation
|
|
|
|
+ .error("Idle Termination time must be a non-negative integer (or null)");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public FormValidation doCheckInstanceCapStr(
|
|
|
|
+ @QueryParameter final String value) {
|
|
|
|
+ if (value == null || value.trim() == "") {
|
|
|
|
+ return FormValidation.ok();
|
|
|
|
+ }
|
|
|
|
+ try {
|
|
|
|
+ int val = Integer.parseInt(value);
|
|
|
|
+ if (val >= 0) {
|
|
|
|
+ return FormValidation.ok();
|
|
|
|
+ }
|
|
|
|
+ } catch (NumberFormatException nfe) {
|
|
|
|
+ }
|
|
|
|
+ return FormValidation
|
|
|
|
+ .error("InstanceCap must be a non-negative integer (or null)");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void filterReservations(final List<Reservation> reservations,
|
|
|
|
+ final List<Filter> filters) {
|
|
|
|
+
|
|
|
|
+ for (Filter f : filters) {
|
|
|
|
+ filterReservationsBy(reservations, f.getName(), f.getValues());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void filterReservationsBy(
|
|
|
|
+ final List<Reservation> reservations, final String name,
|
|
|
|
+ final List<String> values) {
|
|
|
|
+ List<Reservation> emptyReservations = new ArrayList<Reservation>();
|
|
|
|
+
|
|
|
|
+ for (Reservation reservation : reservations) {
|
|
|
|
+ filterReservationBy(reservation, name, values);
|
|
|
|
+
|
|
|
|
+ if (reservation.getInstances().size() == 0) {
|
|
|
|
+ emptyReservations.add(reservation);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ reservations.removeAll(emptyReservations);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * @param reservation
|
|
|
|
+ * @param name
|
|
|
|
+ * @param values
|
|
|
|
+ */
|
|
|
|
+ private void filterReservationBy(final Reservation reservation,
|
|
|
|
+ final String name, final List<String> values) {
|
|
|
|
+ List<Instance> instances = new ArrayList<Instance>();
|
|
|
|
+ for (Instance inst : reservation.getInstances()) {
|
|
|
|
+ if (name.equalsIgnoreCase("image-id")) {
|
|
|
|
+ String data = inst.getImageId();
|
|
|
|
+ for (String value : values) {
|
|
|
|
+ if (value.equalsIgnoreCase(data)) {
|
|
|
|
+ instances.add(inst);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (name.equalsIgnoreCase("instance-type")) {
|
|
|
|
+ String data = inst.getInstanceType();
|
|
|
|
+ for (String value : values) {
|
|
|
|
+ if (value.equalsIgnoreCase(data)) {
|
|
|
|
+ instances.add(inst);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (name.equalsIgnoreCase("instance-state-name")) {
|
|
|
|
+ String data = inst.getState().getName();
|
|
|
|
+ for (String value : values) {
|
|
|
|
+ if (value.equalsIgnoreCase(data)) {
|
|
|
|
+ instances.add(inst);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (name.equalsIgnoreCase("key-name")) {
|
|
|
|
+ String data = inst.getKeyName();
|
|
|
|
+ for (String value : values) {
|
|
|
|
+ if (value.equalsIgnoreCase(data)) {
|
|
|
|
+ instances.add(inst);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // TODO: Include more filters
|
|
|
|
+ // TODO: Improve filtering mecanism
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ reservation.getInstances().retainAll(instances);
|
|
|
|
+ }
|
|
|
|
+}
|