package hudson.plugins.jclouds;
import com.google.common.collect.ImmutableSet;
import hudson.Extension;
import hudson.model.Computer;
import hudson.model.Label;
import hudson.slaves.Cloud;
import hudson.slaves.NodeProvisioner.PlannedNode;
import hudson.util.Secret;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.util.FormValidation;
import hudson.util.StreamTaskListener;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.ComputeServiceContext;
import org.jclouds.compute.ComputeServiceContextFactory;
import org.jclouds.compute.domain.ComputeMetadata;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.Size;
import org.jclouds.compute.util.ComputeUtils;
import org.jclouds.rest.AuthorizationException;
import org.jclouds.ssh.jsch.config.JschSshClientModule;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
/**
*
* @author mordred
*/
public class JClouds extends Cloud {
private final String provider;
private final String user;
private final Secret secret;
private List<JCloudTemplate> templates;
/**
* Upper bound on how many instances we may provision.
*/
public final int instanceCap;
@DataBoundConstructor
public JClouds(String provider, String user, String secret, String instanceCapStr, List<JCloudTemplate> templates) {
super(String.format("jclouds-{0}-{1}", new Object[]{provider, user}));
this.provider = provider;
this.user = user;
this.secret = Secret.fromString(secret.trim());
if (instanceCapStr.equals("")) {
this.instanceCap = Integer.MAX_VALUE;
} else {
this.instanceCap = Integer.parseInt(instanceCapStr);
}
this.templates = templates;
if (templates == null) {
templates = Collections.emptyList();
}
readResolve();
}
private Object readResolve() {
if (templates != null) {
for (JCloudTemplate t : templates) {
t.setParent(this);
}
}
return this;
}
private static final Logger LOGGER = Logger.getLogger(JClouds.class.getName());
public String getProvider() {
return provider;
}
public String getUser() {
return user;
}
public String getSecret() {
return secret.getEncryptedValue();
}
public String getInstanceCapStr() {
if (instanceCap == Integer.MAX_VALUE) {
return "";
} else {
return String.valueOf(instanceCap);
}
}
public List<JCloudTemplate> getTemplates() {
return Collections.unmodifiableList(templates);
}
public JCloudTemplate getTemplate(String slave) {
for (JCloudTemplate t : templates) {
if (t.getSlave().equals(slave)) {
return t;
}
}
return null;
}
public JCloudTemplate getTemplate(Label label) {
for (JCloudTemplate t : templates) {
if (t.containsLabel(label)) {
return t;
}
}
return null;
}
/**
* Counts the number of instances currently running.
*
* <p>
* This includes those instances that may be started outside Hudson.
*/
public int countCurrentSlaves() throws AuthorizationException, Throwable {
int n = 0;
for (ComputeMetadata node : connect().listNodes()) {
n++;
}
return n;
}
@Override
public boolean canProvision(Label label) {
return getTemplate(label) != null;
}
private int calculateNodesToLaunch(int requestedWorkload) {
int current = 0;
try {
current = countCurrentSlaves();
} catch (Throwable ex) {
return 0;
}
if (current >= instanceCap) {
return 0;
}
int remaining = instanceCap - current;
return remaining < requestedWorkload ? remaining : requestedWorkload;
}
@Override
public Collection<PlannedNode> provision(Label label, int requestedWorkload) {
try {
final JCloudTemplate t = getTemplate(label);
/* How many nodes should we spawn? */
int toLaunch = calculateNodesToLaunch(requestedWorkload);
StringWriter sw = new StringWriter();
List<JCloudSlave> slaves = t.provision(new StreamTaskListener(sw),
calculateNodesToLaunch(requestedWorkload));
List<PlannedNode> r = new ArrayList<PlannedNode>();
for (final JCloudSlave slave : slaves) {
r.add(new PlannedNode(t.getDescription(),
Computer.threadPoolForRemoting.submit(new Callable<Node>() {
public Node call() throws Exception {
// TODO: record the output somewhere
try {
Hudson.getInstance().addNode(slave);
// Instances may have a long init script. If we declare
// the provisioning complete by returning without the connect
// operation, NodeProvisioner may decide that it still wants
// one more instance, because it sees that (1) all the slaves
// are offline (because it's still being launched) and
// (2) there's no capacity provisioned yet.
//
// deferring the completion of provisioning until the launch
// goes successful prevents this problem.
slave.toComputer().connect(false).get();
return slave;
} catch (Throwable ex) {
throw new RuntimeException(ex);
}
}
}), t.getNumExecutors()));
}
return r;
} catch (Throwable e) {
LOGGER.log(Level.WARNING, e.getLocalizedMessage());
return Collections.emptyList();
}
}
protected ComputeService connect()
throws AuthorizationException, Throwable {
return getComputeService(provider, user, secret.getEncryptedValue());
}
/**
* Gets the first {@link JClouds} instance configured in the current Hudson, or null if no such thing exists.
*/
public static JClouds get() {
return Hudson.getInstance().clouds.get(JClouds.class);
}
/**
* Gets the named cloud
* @param name name of cloud to get
* @return JClouds instance matching name
*/
public static JClouds get(String name) {
for (JClouds j : Hudson.getInstance().clouds.getAll(JClouds.class)) {
if (j.name.matches(name)) {
return j;
}
}
return null;
}
public static ComputeService getComputeService(String provider, String user, String secret)
throws AuthorizationException, IOException {
ComputeService client = null;
ComputeServiceContext context = new ComputeServiceContextFactory().createContext(provider, user, secret,
ImmutableSet.of(new JschSshClientModule()));
client = context.getComputeService();
return client;
}
@Extension
public static class DescriptorImpl extends Descriptor<Cloud> {
@Override
public String getDisplayName() {
return "JClouds";
}
public Set<String> getSupportedProviders() {
return ComputeUtils.getSupportedProviders();
}
public FormValidation doTestConnection(
@QueryParameter String provider,
@QueryParameter String user,
@QueryParameter String secret) throws ServletException, IOException, Throwable {
ComputeService client = null;
try {
client = getComputeService(provider, user, secret);
} catch (AuthorizationException ex) {
return FormValidation.error("Authentication Error: " + ex.getLocalizedMessage());
}
//Set<? extends ComputeMetadata> nodes = Sets.newHashSet(connection.getNodes().values());
for (Image image : client.listImages()) {
if (image != null) {
LOGGER.log(Level.INFO, "image: {0}|{1}|{2}:{3}:{4}", new Object[]{
image.getArchitecture(),
image.getOsFamily(),
image.getOsDescription(),
image.getDescription(), image.getId()
});
LOGGER.log(Level.INFO, "image: {0}", image.toString());
}
}
for (Size size : client.listSizes()) {
if (size != null) {
LOGGER.log(Level.INFO, "size: {0}", size.toString());
}
}
for (ComputeMetadata node : client.listNodes()) {
if (node != null) {
LOGGER.log(Level.INFO, "Node {0}:{1} in {2}", new Object[]{
node.getId(),
node.getName(),
node.getLocation().getId()});
}
}
return FormValidation.ok();
}
}
}