From e4abeecc4d270c20750321e8d3f4d83257642250 Mon Sep 17 00:00:00 2001 From: Gustavo Morozowski Date: Thu, 19 Apr 2012 22:09:18 -0300 Subject: [PATCH] base implemenation of nodepool --- labs/nodepool/pom.xml | 93 ++++++ .../nodepool/PooledComputeService.java | 28 ++ .../internal/BasePooledComputeService.java | 265 ++++++++++++++++ .../java/org/jclouds/nodepool/AppTest.java | 284 ++++++++++++++++++ 4 files changed, 670 insertions(+) create mode 100644 labs/nodepool/pom.xml create mode 100644 labs/nodepool/src/main/java/org/jclouds/nodepool/PooledComputeService.java create mode 100644 labs/nodepool/src/main/java/org/jclouds/nodepool/internal/BasePooledComputeService.java create mode 100644 labs/nodepool/src/test/java/org/jclouds/nodepool/AppTest.java diff --git a/labs/nodepool/pom.xml b/labs/nodepool/pom.xml new file mode 100644 index 0000000000..403658b301 --- /dev/null +++ b/labs/nodepool/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + jclouds-project + org.jclouds + 1.5.0-SNAPSHOT + ../../project/pom.xml + + org.jclouds.labs + nodepool + jclouds nodepool + bundle + + UTF-8 + 0.1.46 + ${test.aws.identity} + ${test.aws.credential} + + + + org.jclouds + jclouds-compute + ${project.version} + + + org.jclouds + jclouds-core + ${project.version} + test-jar + test + + + org.jclouds + jclouds-compute + ${project.version} + test-jar + test + + + org.jclouds.provider + aws-ec2 + ${project.version} + test + + + org.jclouds.driver + jclouds-enterprise + ${project.version} + test + + + org.jclouds.driver + jclouds-sshj + ${project.version} + test + + + org.jclouds + jclouds-scriptbuilder + ${project.version} + test + + + org.jclouds.driver + jclouds-jsch + ${project.version} + test + + + com.jcraft + jsch + test + + + + + + org.apache.felix + maven-bundle-plugin + + + ${project.artifactId} + org.jclouds.nodepool*;version="${project.version}" + org.jclouds*;version="${project.version}",* + + + + + + + diff --git a/labs/nodepool/src/main/java/org/jclouds/nodepool/PooledComputeService.java b/labs/nodepool/src/main/java/org/jclouds/nodepool/PooledComputeService.java new file mode 100644 index 0000000000..771a1bef9e --- /dev/null +++ b/labs/nodepool/src/main/java/org/jclouds/nodepool/PooledComputeService.java @@ -0,0 +1,28 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jclouds.nodepool; + +import org.jclouds.compute.ComputeService; +import org.jclouds.compute.RunNodesException; + +public interface PooledComputeService extends ComputeService { + + void startPool() throws RunNodesException; + +} diff --git a/labs/nodepool/src/main/java/org/jclouds/nodepool/internal/BasePooledComputeService.java b/labs/nodepool/src/main/java/org/jclouds/nodepool/internal/BasePooledComputeService.java new file mode 100644 index 0000000000..9bc399077c --- /dev/null +++ b/labs/nodepool/src/main/java/org/jclouds/nodepool/internal/BasePooledComputeService.java @@ -0,0 +1,265 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jclouds.nodepool.internal; + +import java.util.Map; +import java.util.Set; + +import org.jclouds.compute.ComputeService; +import org.jclouds.compute.ComputeServiceContext; +import org.jclouds.compute.RunNodesException; +import org.jclouds.compute.RunScriptOnNodesException; +import org.jclouds.compute.domain.ComputeMetadata; +import org.jclouds.compute.domain.ExecResponse; +import org.jclouds.compute.domain.Hardware; +import org.jclouds.compute.domain.Image; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.NodeMetadataBuilder; +import org.jclouds.compute.domain.Template; +import org.jclouds.compute.domain.TemplateBuilder; +import org.jclouds.compute.options.RunScriptOptions; +import org.jclouds.compute.options.TemplateOptions; +import org.jclouds.compute.predicates.NodePredicates; +import org.jclouds.domain.Location; +import org.jclouds.nodepool.PooledComputeService; +import org.jclouds.scriptbuilder.domain.Statement; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ListenableFuture; + +public class BasePooledComputeService implements PooledComputeService { + + private final ComputeService backingComputeService; + private final String backingGroup; + private final Template backingTemplate; + private final int minPoolSize; + private Map groupMapping; + + public BasePooledComputeService(ComputeService backingComputeService, String backingGroup, Template backingTemplate, int minPoolSize) { + this.backingComputeService = backingComputeService; + this.backingGroup = backingGroup; + this.backingTemplate = backingTemplate; + this.minPoolSize = minPoolSize; + } + + @Override + public void startPool() throws RunNodesException { + Set backingNodes = + backingComputeService.createNodesInGroup(backingGroup, minPoolSize, backingTemplate); + groupMapping = Maps.newHashMap(); + for (NodeMetadata node : backingNodes) { + groupMapping.put(node, "unassigned"); + } + } + + @Override + public ComputeServiceContext getContext() { + return backingComputeService.getContext(); + } + + @Override + public TemplateBuilder templateBuilder() { + return backingComputeService.templateBuilder(); + } + + @Override + public TemplateOptions templateOptions() { + return backingComputeService.templateOptions(); + } + + @Override + public Set listHardwareProfiles() { + return ImmutableSet.of(backingTemplate.getHardware()); + } + + @Override + public Set listImages() { + return ImmutableSet.of(backingTemplate.getImage()); + } + + @Override + public Set listNodes() { + Set allocatedNodes = Sets.newLinkedHashSet(); + for (ComputeMetadata node : backingComputeService.listNodes()) { + NodeMetadata metadata = backingComputeService.getNodeMetadata(node.getId()); + String group = groupMapping.get(node); + if ("unassigned".equals(group)) + continue; + NodeMetadata nodeWithUpdatedGroup = + NodeMetadataBuilder.fromNodeMetadata(metadata).group(group).build(); + allocatedNodes.add(nodeWithUpdatedGroup); + } + return allocatedNodes; + } + + @Override + public Set listAssignableLocations() { + return ImmutableSet.of(backingTemplate.getLocation()); + } + + @Override + public Set createNodesInGroup(String group, + int count, Template template) throws RunNodesException { + throw new RuntimeException("not implemented"); + } + + @Override + public Set createNodesInGroup(String group, + int count, TemplateOptions templateOptions) + throws RunNodesException { + throw new RuntimeException("not implemented"); + } + + @Override + public Set createNodesInGroup(String group, + int count) throws RunNodesException { + int allocatedCount = 0; + Set allocatedNodes = Sets.newLinkedHashSet(); + for (NodeMetadata metadata : groupMapping.keySet()) { + if (groupMapping.get(metadata).equals("unassigned")) { + groupMapping.put(metadata, "group"); + NodeMetadata nodeWithUpdatedGroup = + NodeMetadataBuilder.fromNodeMetadata(metadata).group(group).build(); + allocatedNodes.add(nodeWithUpdatedGroup); + allocatedCount += 1; + if (allocatedCount == count) break; + } + } + return allocatedNodes; + } + + @Override + public void resumeNode(String id) { + backingComputeService.resumeNode(id); + + } + + @Override + public void resumeNodesMatching(Predicate filter) { + backingComputeService.resumeNodesMatching(filter); + + } + + @Override + public void suspendNode(String id) { + backingComputeService.suspendNode(id); + } + + @Override + public void suspendNodesMatching(Predicate filter) { + backingComputeService.suspendNodesMatching(filter); + } + + @Override + public void destroyNode(String id) { + + backingComputeService.destroyNode(id); + } + + @Override + public Set destroyNodesMatching( + Predicate filter) { + return backingComputeService.destroyNodesMatching(filter); + } + + @Override + public void rebootNode(String id) { + backingComputeService.rebootNode(id); + + } + + @Override + public void rebootNodesMatching(Predicate filter) { + backingComputeService.rebootNodesMatching(filter); + + } + + @Override + public NodeMetadata getNodeMetadata(String id) { + return backingComputeService.getNodeMetadata(id); + } + + @Override + public Set listNodesDetailsMatching( + Predicate filter) { + return backingComputeService.listNodesDetailsMatching(filter); + } + + @Override + public Map runScriptOnNodesMatching( + Predicate filter, String runScript) + throws RunScriptOnNodesException { + // TODO Auto-generated method stub + return backingComputeService.runScriptOnNodesMatching(filter, runScript); + } + + @Override + public Map runScriptOnNodesMatching( + Predicate filter, Statement runScript) + throws RunScriptOnNodesException { + return backingComputeService.runScriptOnNodesMatching(filter, runScript); + } + + @Override + public Map runScriptOnNodesMatching( + Predicate filter, String runScript, + RunScriptOptions options) throws RunScriptOnNodesException { + return backingComputeService.runScriptOnNodesMatching(filter, runScript, options); + } + + @Override + public Map runScriptOnNodesMatching( + Predicate filter, Statement runScript, + RunScriptOptions options) throws RunScriptOnNodesException { + return backingComputeService.runScriptOnNodesMatching(filter, runScript, options); + } + + @Override + public ExecResponse runScriptOnNode(String id, Statement runScript, + RunScriptOptions options) { + return backingComputeService.runScriptOnNode(id, runScript, options); + } + + @Override + public ListenableFuture submitScriptOnNode(String id, + Statement runScript, RunScriptOptions options) { + return backingComputeService.submitScriptOnNode(id, runScript, options); + } + + @Override + public ExecResponse runScriptOnNode(String id, Statement runScript) { + return backingComputeService.runScriptOnNode(id, runScript); + } + + @Override + public ExecResponse runScriptOnNode(String id, String runScript, + RunScriptOptions options) { + return backingComputeService.runScriptOnNode(id, runScript, options); + } + + @Override + public ExecResponse runScriptOnNode(String id, String runScript) { + return backingComputeService.runScriptOnNode(id, runScript); + } + +} diff --git a/labs/nodepool/src/test/java/org/jclouds/nodepool/AppTest.java b/labs/nodepool/src/test/java/org/jclouds/nodepool/AppTest.java new file mode 100644 index 0000000000..1b07aafaf8 --- /dev/null +++ b/labs/nodepool/src/test/java/org/jclouds/nodepool/AppTest.java @@ -0,0 +1,284 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jclouds.nodepool; + +import static com.google.common.base.Throwables.propagate; +import static com.google.common.collect.Iterables.getOnlyElement; +import static org.jclouds.scriptbuilder.domain.Statements.newStatementList; + +import java.io.File; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; + +import junit.framework.TestCase; + +import org.jclouds.Constants; +import org.jclouds.compute.ComputeService; +import org.jclouds.compute.ComputeServiceContextFactory; +import org.jclouds.compute.RunNodesException; +import org.jclouds.compute.domain.ComputeMetadata; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.NodeState; +import org.jclouds.compute.domain.OsFamily; +import org.jclouds.compute.domain.Template; +import org.jclouds.compute.domain.TemplateBuilder; +import org.jclouds.enterprise.config.EnterpriseConfigurationModule; +import org.jclouds.logging.slf4j.config.SLF4JLoggingModule; +import org.jclouds.nodepool.internal.BasePooledComputeService; +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.domain.Statements; +import org.jclouds.scriptbuilder.statements.java.InstallJDK; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.jclouds.sshj.config.SshjSshClientModule; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Module; + +/** + * Unit test for simple App. + */ +public class AppTest extends TestCase { + private String identity; + private String credential; + private String providerName; + private File privateKey; + private File publicKey; + private String endPointUrl; + public String profile; + private int retentionTime; + public int instanceCap; + + private String imageId; + private String osFamily; + private String osVersion; + private String hardwareId; + private int ram; + private int cores; + private String initScript; + private String name; + private boolean stopOnTerminate; + private ComputeService compute; + private PooledComputeService pooledCompute; + private Collection nodes = new LinkedList(); + private static Logger LOGGER = Logger.getLogger("AppTest"); + + private long time; + + @Override + protected void setUp() throws Exception { + // setup JCloudsCloud + identity = "insert-your-identity-here"; + credential = "insert-your-credential-here"; + providerName = "aws-ec2"; + privateKey = new File("private-key"); + publicKey = new File("public-key"); + endPointUrl = ""; + profile = "aws-slave-profile"; + retentionTime = -1; + instanceCap = 3; + + // cloud instance template + name = "aws-jenkins-slave"; + // numExecutors = 1; + // description = "" + imageId = "us-east-1/ami-4dad7424"; + osFamily = ""; + osVersion = ""; + hardwareId = "t1.micro"; + ram = -1; + cores = -1; + // labels = "whii"; + initScript = "touch /tmp/hellothere"; + stopOnTerminate = true; + } + + /** + * Rigourous Test :-) + */ + public void testApp() { + createCompute(); + assertNotNull(compute); + createAndStartPool(); + assertNotNull(pooledCompute); + for (int i = 0; i < 3; i++) { + startCounter(); + provision("pool-1"); + stopCounter(); + } + for (int i = 0; i < 3; i++) { + startCounter(); + provision("pool-2"); + stopCounter(); + } + for (int i = 0; i < 3; i++) { + startCounter(); + provision("pool-3"); + stopCounter(); + } + assertEquals(9, getRunningNodesCount()); + for (NodeMetadata slave : nodes) { + assertNotNull(slave); + LOGGER.info(slave.getId() + "-" + slave.getGroup()); + terminate(slave); + } + assertEquals(0, getRunningNodesCount()); + } + + private void stopCounter() { + LOGGER.info("Elapsed time: " + (System.currentTimeMillis() - time)); + } + + private void startCounter() { + time = System.currentTimeMillis(); + } + + public AppTest getCloud() { + return this; + } + + public ComputeService getCompute() { + return pooledCompute; + } + + public NodeMetadata provision(String groupName) { + LOGGER.info("Provisioning new node"); + NodeMetadata nodeMetadata = createNodeWithJdk(groupName); + nodes.add(nodeMetadata); + return nodeMetadata; + } + + public int getRunningNodesCount() { + int nodeCount = 0; + + for (ComputeMetadata cm : pooledCompute.listNodes()) { + if (NodeMetadata.class.isInstance(cm)) { + String nodeGroup = ((NodeMetadata) cm).getGroup(); + + if (!((NodeMetadata) cm).getState().equals(NodeState.SUSPENDED) + && !((NodeMetadata) cm).getState().equals(NodeState.TERMINATED)) { + nodeCount++; + } + } + } + return nodeCount; + } + + private void createCompute() { + Properties overrides = new Properties(); + if (!Strings.isNullOrEmpty(this.endPointUrl)) { + overrides.setProperty(Constants.PROPERTY_ENDPOINT, this.endPointUrl); + } + Iterable modules = ImmutableSet. of(new SshjSshClientModule(), new SLF4JLoggingModule(), + new EnterpriseConfigurationModule()); + this.compute = new ComputeServiceContextFactory().createContext(this.providerName, this.identity, + this.credential, modules, overrides).getComputeService(); + } + + private void createAndStartPool() { + LOGGER.info("creating jclouds nodepool"); + ImmutableMap userMetadata = ImmutableMap.of("Name", name); + TemplateBuilder templateBuilder = compute.templateBuilder(); + if (!Strings.isNullOrEmpty(imageId)) { + LOGGER.info("Setting image id to " + imageId); + templateBuilder.imageId(imageId); + } else { + if (!Strings.isNullOrEmpty(osFamily)) { + LOGGER.info("Setting osFamily to " + osFamily); + templateBuilder.osFamily(OsFamily.valueOf(osFamily)); + } + if (!Strings.isNullOrEmpty(osVersion)) { + LOGGER.info("Setting osVersion to " + osVersion); + templateBuilder.osVersionMatches(osVersion); + } + } + if (!Strings.isNullOrEmpty((hardwareId))) { + LOGGER.info("Setting hardware Id to " + hardwareId); + } else { + LOGGER.info("Setting minRam " + ram + " and minCores " + cores); + templateBuilder.minCores(cores).minRam(ram); + } + + Template template = templateBuilder.build(); + + // setup the jcloudTemplate to customize the nodeMetadata with jdk, etc. + // also opening ports + AdminAccess adminAccess = AdminAccess.builder().adminUsername("jenkins").installAdminPrivateKey(false) // no + // need + .grantSudoToAdminUser(false) // no need + .adminPrivateKey(getCloud().privateKey) // temporary due to jclouds + // bug + .authorizeAdminPublicKey(true).adminPublicKey(getCloud().publicKey).build(); + + // Jenkins needs /jenkins dir. + Statement jenkinsDirStatement = Statements.newStatementList(Statements.exec("mkdir /jenkins"), + Statements.exec("chown jenkins /jenkins")); + + Statement bootstrap = newStatementList(adminAccess, jenkinsDirStatement, Statements.exec(this.initScript), + InstallJDK.fromOpenJDK()); + + template.getOptions().inboundPorts(22).userMetadata(userMetadata).runScript(bootstrap); + + pooledCompute = new BasePooledComputeService(compute, "jenkins-pool", template, 10); + + try { + pooledCompute.startPool(); + } catch (RunNodesException e) { + destroyBadNodesAndPropagate(e); + } + } + + private NodeMetadata createNodeWithJdk(String groupName) { + LOGGER.info("creating jclouds node"); + + NodeMetadata nodeMetadata = null; + + try { + nodeMetadata = getOnlyElement(pooledCompute.createNodesInGroup(groupName, 1)); + } catch (RunNodesException e) { + throw destroyBadNodesAndPropagate(e); + } + + // Check if nodeMetadata is null and throw + return nodeMetadata; + } + + private RuntimeException destroyBadNodesAndPropagate(RunNodesException e) { + for (Map.Entry nodeError : e.getNodeErrors().entrySet()) + getCloud().getCompute().destroyNode(nodeError.getKey().getId()); + throw propagate(e); + } + + public void terminate(NodeMetadata nodeMetaData) { + if (stopOnTerminate) { + LOGGER.info("Suspending the Slave : " + nodeMetaData.getName()); + final ComputeService compute = getCloud().getCompute(); + compute.suspendNode(nodeMetaData.getId()); + } else { + LOGGER.info("Terminating the Slave : " + nodeMetaData.getName()); + final ComputeService compute = getCloud().getCompute(); + compute.destroyNode(nodeMetaData.getId()); + } + } + +}