Skip to content

Commit

Permalink
Initial work on BuildWrapper for spinning up non-slave instances duri…
Browse files Browse the repository at this point in the history
…ng a build.

TODO: Add config help. Add a lot better logging. Still really want to
clear out that net.schmizz noise, and would really like to capture
some of the logging from jclouds.compute, filter it, and dump it to
the job log. And I'm fairly sure my code could be really cleaned up.
  • Loading branch information
abayer committed May 9, 2012
1 parent 6a3124f commit 7e5b37d
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 7 deletions.
63 changes: 63 additions & 0 deletions src/main/java/jenkins/plugins/jclouds/compute/InstancesToRun.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package jenkins.plugins.jclouds.compute;

import java.io.Serializable;

import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

public final class InstancesToRun extends AbstractDescribableImpl<InstancesToRun> {
public final String cloudName;
public final String templateName;
public final int count;
public final boolean suspendOrTerminate;

@DataBoundConstructor
public InstancesToRun(String cloudName, String templateName, int count, boolean suspendOrTerminate) {
this.cloudName = Util.fixEmptyAndTrim(cloudName);
this.templateName = Util.fixEmptyAndTrim(templateName);
this.count = count;
this.suspendOrTerminate = suspendOrTerminate;
}

@Extension
public static class DescriptorImpl extends Descriptor<InstancesToRun> {
public ListBoxModel doFillCloudNameItems() {
ListBoxModel m = new ListBoxModel();
for (String cloudName : JCloudsCloud.getCloudNames()) {
m.add(cloudName, cloudName);
}

return m;
}

public ListBoxModel doFillTemplateNameItems(@QueryParameter String cloudName) {
ListBoxModel m = new ListBoxModel();
JCloudsCloud c = JCloudsCloud.getByName(cloudName);
if (c != null) {
for (JCloudsSlaveTemplate t : c.getTemplates()) {
m.add(String.format("%s in cloud %s", t.name, cloudName), t.name);
}
}
return m;
}

public FormValidation doCheckCount(@QueryParameter String value) {
return FormValidation.validatePositiveInteger(value);
}

@Override
public String getDisplayName() {
return "";
}
}
}
241 changes: 241 additions & 0 deletions src/main/java/jenkins/plugins/jclouds/compute/JCloudsBuildWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package jenkins.plugins.jclouds.compute;

import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractDescribableImpl;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

import org.jclouds.compute.ComputeService;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.NodeState;

import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Callable;

public class JCloudsBuildWrapper extends BuildWrapper {
public List<InstancesToRun> instancesToRun;
public final int MAX_ATTEMPTS = 5;

@DataBoundConstructor
public JCloudsBuildWrapper(List<InstancesToRun> instancesToRun) {
this.instancesToRun = instancesToRun;
}

public InstancesToRun getMatchingInstanceToRun(String cloudName, String templateName) {
for (InstancesToRun i : instancesToRun) {
if (i.cloudName.equals(cloudName) && i.templateName.equals(templateName)) {
return i;
}
}
return null;
}

@Override
public Environment setUp(AbstractBuild build, final Launcher launcher, final BuildListener listener) throws IOException, InterruptedException {

final Map<String,Map<String,List<NodeMetadata>>> instances;
Map<String,Map<String,List<NodeMetadata>>> spawnedInstances = new HashMap<String,Map<String,List<NodeMetadata>>>();
List<PlannedInstance> plannedInstances = new ArrayList<PlannedInstance>();

for (InstancesToRun instance : instancesToRun) {
final JCloudsCloud cloud = JCloudsCloud.getByName(instance.cloudName);
final JCloudsSlaveTemplate template = cloud.getTemplate(instance.templateName);

for (int i=0; i < instance.count; i++) {

plannedInstances.add(new PlannedInstance(instance.cloudName,
instance.templateName,
i,
Computer.threadPoolForRemoting.submit(new Callable<NodeMetadata>() {
public NodeMetadata call() throws Exception {
int attempts = 0;

while (attempts < MAX_ATTEMPTS) {
attempts++;
try {
NodeMetadata n = template.provision();
if (n != null) {
return n;
}
} catch (RuntimeException e) {
// Something to log the e.getCause() which should be a RunNodesException
}
}

return null;
}
})));
listener.getLogger().println("Queuing cloud instance: #" + i + " of " + instance.count + ", " + instance.cloudName + " " + instance.templateName);
}
}

int failedLaunches = 0;

while (plannedInstances.size() > 0) {
for (Iterator<PlannedInstance> itr = plannedInstances.iterator(); itr.hasNext();) {
PlannedInstance f = itr.next();
if (f.future.isDone()) {
try {
Map<String,List<NodeMetadata>> cloudMap = spawnedInstances.get(f.cloudName);
if (cloudMap==null) {
spawnedInstances.put(f.cloudName, cloudMap = new HashMap<String,List<NodeMetadata>>());
}
List<NodeMetadata> templateList = cloudMap.get(f.templateName);
if (templateList==null) {
cloudMap.put(f.templateName, templateList = new ArrayList<NodeMetadata>());
}

NodeMetadata n = f.future.get();

if (n != null) {
templateList.add(n);
} else {
failedLaunches++;
}
} catch (InterruptedException e) {
failedLaunches++;
listener.error("Interruption while launching instance " + f.index + " of " + f.cloudName + "/" + f.templateName + ": " + e);
} catch (ExecutionException e) {
failedLaunches++;
listener.error("Error while launching instance " + f.index + " of " + f.cloudName + "/" + f.templateName + ": " + e.getCause());
}

itr.remove();
}
}
}

instances = Collections.unmodifiableMap(spawnedInstances);

if (failedLaunches > 0) {
terminateNodes(instances, listener.getLogger());
throw new IOException("One or more instances failed to launch.");
}


return new Environment() {
@Override
public void buildEnvVars(Map<String,String> env) {
List<String> ips = getInstanceIPs(instances, listener.getLogger());
env.put("CLOUD_IPS", Util.join(ips, ","));
}

@Override
public boolean tearDown(AbstractBuild build, final BuildListener listener) throws IOException, InterruptedException {
terminateNodes(instances, listener.getLogger());

return true;

}

};

}

public List<String> getInstanceIPs(Map<String,Map<String,List<NodeMetadata>>> instances, PrintStream logger) {
List<String> ips = new ArrayList<String>();

for (String cloudName : instances.keySet()) {
for (String templateName : instances.get(cloudName).keySet()) {
for (NodeMetadata n : instances.get(cloudName).get(templateName)) {
String[] possibleIPs = JCloudsLauncher.getConnectionAddresses(n, logger);
if (possibleIPs[0] != null) {
ips.add(possibleIPs[0]);
}
}
}
}

return ips;
}



public void terminateNodes(Map<String,Map<String,List<NodeMetadata>>> instances, PrintStream logger) {
for (String cloudName : instances.keySet()) {
for (String templateName : instances.get(cloudName).keySet()) {
InstancesToRun i = getMatchingInstanceToRun(cloudName, templateName);
for (NodeMetadata n : instances.get(cloudName).get(templateName)) {
terminateNode(cloudName, n.getId(), i.suspendOrTerminate, logger);
}
}
}
}


/**
* Destroy the node calls {@link ComputeService#destroyNode}
*
*/
public void terminateNode(String cloudName, String nodeId, boolean suspendOrTerminate, PrintStream logger) {
final ComputeService compute = JCloudsCloud.getByName(cloudName).getCompute();
if (compute.getNodeMetadata(nodeId) != null &&
compute.getNodeMetadata(nodeId).getState().equals(NodeState.RUNNING)) {
if (suspendOrTerminate) {
logger.println("Suspending the Node : " + nodeId);
compute.suspendNode(nodeId);
} else {
logger.println("Terminating the Node : " + nodeId);
compute.destroyNode(nodeId);
}
} else {
logger.println("Node " + nodeId + " is already not running.");
}
}



@Extension
public static final class DescriptorImpl extends BuildWrapperDescriptor {
@Override
public String getDisplayName() {
return "JClouds Instance Creation";
}

@Override
public boolean isApplicable(AbstractProject item) {
return true;
}

}


public static final class PlannedInstance {
public final String cloudName;
public final String templateName;
public final Future<NodeMetadata> future;
public final int index;

public PlannedInstance(String cloudName, String templateName, int index, Future<NodeMetadata> future) {
this.cloudName = cloudName;
this.templateName = templateName;
this.index = index;
this.future = future;
}
}


}
19 changes: 15 additions & 4 deletions src/main/java/jenkins/plugins/jclouds/compute/JCloudsCloud.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,21 @@ public class JCloudsCloud extends Cloud {
public final List<JCloudsSlaveTemplate> templates;
private transient ComputeService compute;

public static JCloudsCloud getByName(String name) {
return (JCloudsCloud)Hudson.getInstance().clouds.getByName(name);
}

public static List<String> getCloudNames() {
List<String> cloudNames = new ArrayList<String>();
for (Cloud c : Hudson.getInstance().clouds) {
if (JCloudsCloud.class.isInstance(c)) {
cloudNames.add(c.name);
}
}

return cloudNames;
}

public static JCloudsCloud getByName(String name) {
return (JCloudsCloud)Hudson.getInstance().clouds.getByName(name);
}

@DataBoundConstructor
public JCloudsCloud(final String profile,
final String providerName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ private int bootstrap(Connection bootstrapConn, NodeMetadata nodeMetadata, Print
/**
* Get the potential addresses to connect to, opting for public first and then private.
*/
private String[] getConnectionAddresses(NodeMetadata nodeMetadata, PrintStream logger) {
public static String[] getConnectionAddresses(NodeMetadata nodeMetadata, PrintStream logger) {
if (nodeMetadata.getPublicAddresses().size() > 0) {
return nodeMetadata.getPublicAddresses().toArray(new String[nodeMetadata.getPublicAddresses().size()]);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public Set<LabelAtom> getLabelSet() {
}

public JCloudsSlave provisionSlave(TaskListener listener) throws IOException {
NodeMetadata nodeMetadata = provision(listener);
NodeMetadata nodeMetadata = provision();

try {
return new JCloudsSlave(getCloud().getDisplayName(), getFsRoot(), nodeMetadata, labelString, description, numExecutors, stopOnTerminate);
Expand All @@ -160,7 +160,7 @@ public JCloudsSlave provisionSlave(TaskListener listener) throws IOException {
}


public NodeMetadata provision(TaskListener listener) throws IOException {
public NodeMetadata provision() throws IOException {
LOGGER.info("Provisioning new jclouds node");

ImmutableMap<String, String> userMetadata = ImmutableMap.of("Name", name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<j:jelly xmlns:j="jelly:core"
xmlns:f="/lib/form">
<f:entry title="Cloud Name" field="cloudName">
<f:select />
</f:entry>

<f:entry title="Template" field="templateName">
<f:select />
</f:entry>

<f:entry title="Number of Instances" field="count">
<f:textbox />
</f:entry>

<f:entry title="${%Stop on Terminate}" field="suspendOrTerminate">
<f:checkbox />
</f:entry>

<f:entry>
<div align="right">
<f:repeatableDeleteButton />
</div>
</f:entry>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<j:jelly xmlns:j="jelly:core"
xmlns:f="/lib/form">
<f:entry field="instancesToRun">
<f:repeatableProperty field="instancesToRun" />
</f:entry>
</j:jelly>

0 comments on commit 7e5b37d

Please sign in to comment.