Author: asavu
Date: Wed Oct 12 19:07:07 2011
New Revision: 1182528

URL: http://svn.apache.org/viewvc?rev=1182528&view=rev
Log:
WHIRR-214. First iteration on refactoring the core to support the addition of 
nodes to running clusters (asavu)

Added:
    whirr/trunk/core/src/main/java/org/apache/whirr/compute/
    
whirr/trunk/core/src/main/java/org/apache/whirr/compute/BootstrapTemplate.java
    whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarter.java
    
whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarterFactory.java
    whirr/trunk/core/src/main/java/org/apache/whirr/compute/StartupProcess.java
Modified:
    whirr/trunk/CHANGES.txt
    whirr/trunk/core/src/main/java/org/apache/whirr/Cluster.java
    whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java
    
whirr/trunk/core/src/main/java/org/apache/whirr/actions/BootstrapClusterAction.java
    
whirr/trunk/core/src/test/java/org/apache/whirr/actions/BootstrapClusterActionTest.java

Modified: whirr/trunk/CHANGES.txt
URL: 
http://svn.apache.org/viewvc/whirr/trunk/CHANGES.txt?rev=1182528&r1=1182527&r2=1182528&view=diff
==============================================================================
--- whirr/trunk/CHANGES.txt (original)
+++ whirr/trunk/CHANGES.txt Wed Oct 12 19:07:07 2011
@@ -34,6 +34,9 @@ Trunk (unreleased changes)
 
     WHIRR-325. Reduce cloud provider-specific code in scripts (tomwhite and 
asavu)
 
+    WHIRR-214. First iteration on refactoring the core to support the 
+    addition of nodes to running clusters (asavu)
+
   BUG FIXES
 
     WHIRR-377. Fix broken CLI logging config. (asavu via tomwhite)

Modified: whirr/trunk/core/src/main/java/org/apache/whirr/Cluster.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/Cluster.java?rev=1182528&r1=1182527&r2=1182528&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/Cluster.java (original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/Cluster.java Wed Oct 12 
19:07:07 2011
@@ -18,24 +18,23 @@
 
 package org.apache.whirr;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.net.InetAddresses;
+import org.apache.whirr.util.DnsUtil;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.domain.Credentials;
 
 import java.io.IOException;
 import java.net.InetAddress;
 import java.util.Properties;
 import java.util.Set;
 
-import org.apache.whirr.util.DnsUtil;
-import org.jclouds.compute.domain.NodeMetadata;
-import org.jclouds.domain.Credentials;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
  * This class represents a real cluster of {@link Instance}s.

Modified: whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java?rev=1182528&r1=1182527&r2=1182528&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java 
(original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java Wed 
Oct 12 19:07:07 2011
@@ -21,20 +21,14 @@ package org.apache.whirr;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Set;
-
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import org.apache.whirr.actions.BootstrapClusterAction;
 import org.apache.whirr.actions.ConfigureClusterAction;
 import org.apache.whirr.actions.DestroyClusterAction;
+import org.apache.whirr.service.ClusterActionHandler;
 import org.apache.whirr.service.ClusterStateStore;
 import org.apache.whirr.service.ClusterStateStoreFactory;
-import org.apache.whirr.service.ClusterActionHandler;
 import org.apache.whirr.service.ComputeCache;
 import org.jclouds.compute.ComputeService;
 import org.jclouds.compute.ComputeServiceContext;
@@ -48,6 +42,11 @@ import org.jclouds.scriptbuilder.domain.
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
 import static org.apache.whirr.RolePredicates.withIds;
 import static 
org.jclouds.compute.options.RunScriptOptions.Builder.overrideCredentialsWith;
 

Modified: 
whirr/trunk/core/src/main/java/org/apache/whirr/actions/BootstrapClusterAction.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/BootstrapClusterAction.java?rev=1182528&r1=1182527&r2=1182528&view=diff
==============================================================================
--- 
whirr/trunk/core/src/main/java/org/apache/whirr/actions/BootstrapClusterAction.java
 (original)
+++ 
whirr/trunk/core/src/main/java/org/apache/whirr/actions/BootstrapClusterAction.java
 Wed Oct 12 19:07:07 2011
@@ -18,55 +18,37 @@
 
 package org.apache.whirr.actions;
 
-import static org.jclouds.compute.options.TemplateOptions.Builder.runScript;
-import static org.jclouds.scriptbuilder.domain.Statements.appendFile;
-import static org.jclouds.scriptbuilder.domain.Statements.interpret;
-import static org.jclouds.scriptbuilder.domain.Statements.newStatementList;
-
 import com.google.common.base.Function;
-import com.google.common.base.Splitter;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
 import org.apache.whirr.Cluster;
 import org.apache.whirr.Cluster.Instance;
 import org.apache.whirr.ClusterSpec;
 import org.apache.whirr.InstanceTemplate;
+import org.apache.whirr.compute.BootstrapTemplate;
+import org.apache.whirr.compute.NodeStarterFactory;
+import org.apache.whirr.compute.StartupProcess;
 import org.apache.whirr.service.ClusterActionEvent;
 import org.apache.whirr.service.ClusterActionHandler;
 import org.apache.whirr.service.jclouds.StatementBuilder;
-import org.apache.whirr.service.jclouds.TemplateBuilderStrategy;
-import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions;
 import org.jclouds.compute.ComputeService;
 import org.jclouds.compute.ComputeServiceContext;
-import org.jclouds.compute.RunNodesException;
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.domain.Template;
-import org.jclouds.compute.domain.TemplateBuilder;
-import org.jclouds.aws.ec2.AWSEC2Client;
-import org.jclouds.scriptbuilder.InitBuilder;
-import org.jclouds.scriptbuilder.domain.OsFamily;
-import org.jclouds.scriptbuilder.domain.Statement;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
 /**
  * A {@link org.apache.whirr.ClusterAction} that starts instances in a cluster 
in parallel and
  * runs bootstrap scripts on them.
@@ -106,13 +88,16 @@ public class BootstrapClusterAction exte
     for (Entry<InstanceTemplate, ClusterActionEvent> entry : 
eventMap.entrySet()) {
       final InstanceTemplate instanceTemplate = entry.getKey();
       final ClusterSpec clusterSpec = entry.getValue().getClusterSpec();
-      final int maxNumberOfRetries = clusterSpec.getMaxStartupRetries(); 
+
+      final int maxNumberOfRetries = clusterSpec.getMaxStartupRetries();
       StatementBuilder statementBuilder = 
entry.getValue().getStatementBuilder();
+
       ComputeServiceContext computeServiceContext = 
getCompute().apply(clusterSpec);
       final ComputeService computeService =
         computeServiceContext.getComputeService();
-      final Template template = buildTemplate(clusterSpec, computeService,
-          statementBuilder, entry.getValue().getTemplateBuilderStrategy());
+
+      final Template template = BootstrapTemplate.build(clusterSpec, 
computeService,
+        statementBuilder, entry.getValue().getTemplateBuilderStrategy());
       
       Future<Set<? extends NodeMetadata>> nodesFuture = executorService.submit(
           new StartupProcess(
@@ -146,79 +131,6 @@ public class BootstrapClusterAction exte
     }
   }
 
-  private Template buildTemplate(ClusterSpec clusterSpec,
-      ComputeService computeService, StatementBuilder statementBuilder,
-      TemplateBuilderStrategy strategy)
-      throws MalformedURLException {
-
-    LOG.info("Configuring template");
-    
-    Statement statement = statementBuilder.build(clusterSpec);
-    if (LOG.isDebugEnabled())
-      LOG.debug("Running script:\n{}", statement.render(OsFamily.UNIX));
-
-    Statement runScript = addUserAndAuthorizeSudo(
-        clusterSpec.getClusterUser(),
-        clusterSpec.getPublicKey(),
-        clusterSpec.getPrivateKey(),
-        statement);
-
-    TemplateBuilder templateBuilder = computeService.templateBuilder()
-      .options(runScript(runScript));
-    strategy.configureTemplateBuilder(clusterSpec, templateBuilder);
-
-    return setSpotInstancePriceIfSpecified(
-      computeService.getContext(), clusterSpec, templateBuilder.build());
-  }
-
-  /**
-   * Set maximum spot instance price based on the configuration
-   */
-  private Template setSpotInstancePriceIfSpecified(
-      ComputeServiceContext context, ClusterSpec spec, Template template) {
-
-    if (context != null && context.getProviderSpecificContext().getApi() 
instanceof AWSEC2Client) {
-      if (spec.getAwsEc2SpotPrice() > 0) {
-        template.getOptions().as(AWSEC2TemplateOptions.class)
-          .spotPrice(spec.getAwsEc2SpotPrice());
-      }
-    }
-
-    return template;
-  }
-  
-  private static Statement addUserAndAuthorizeSudo(String user,
-      String publicKey, String privateKey, Statement statement) {
-    return new InitBuilder("setup-" + user,// name of the script
-        "/tmp",// working directory
-        "/tmp/logs",// location of stdout.log and stderr.log
-        ImmutableMap.of("newUser", user, "defaultHome", "/home/users"), // 
variables
-        ImmutableList.<Statement> of(
-            createUserWithPublicAndPrivateKey(user, publicKey, privateKey),
-            makeSudoersOnlyPermitting(user),
-            statement));
-  }
-
-  // must be used inside InitBuilder, as this sets the shell variables used in 
this statement
-  static Statement createUserWithPublicAndPrivateKey(String username,
-      String publicKey, String privateKey) {
-    // note directory must be created first
-    return newStatementList(interpret("mkdir -p $DEFAULT_HOME/$NEW_USER/.ssh",
-        "useradd --shell /bin/bash -d $DEFAULT_HOME/$NEW_USER $NEW_USER\n"), 
appendFile(
-        "$DEFAULT_HOME/$NEW_USER/.ssh/authorized_keys", 
Splitter.on('\n').split(publicKey)),
-        appendFile(
-            "$DEFAULT_HOME/$NEW_USER/.ssh/id_rsa", 
Splitter.on('\n').split(privateKey)),
-        interpret("chmod 400 $DEFAULT_HOME/$NEW_USER/.ssh/*",
-            "chown -R $NEW_USER $DEFAULT_HOME/$NEW_USER\n"));
-  }
-
-  // must be used inside InitBuilder, as this sets the shell variables used in 
this statement
-  static Statement makeSudoersOnlyPermitting(String username) {
-    return newStatementList(interpret("rm /etc/sudoers", "touch /etc/sudoers", 
"chmod 0440 /etc/sudoers",
-        "chown root /etc/sudoers\n"), appendFile("/etc/sudoers", 
ImmutableSet.of("root ALL = (ALL) ALL",
-        "%adm ALL = (ALL) ALL", username + " ALL = (ALL) NOPASSWD: ALL")));
-  }
-  
   private Set<Instance> getInstances(final Set<String> roles,
       Set<? extends NodeMetadata> nodes) {
     return 
Sets.newLinkedHashSet(Collections2.transform(Sets.newLinkedHashSet(nodes),
@@ -232,177 +144,6 @@ public class BootstrapClusterAction exte
       }
     }));
   }
-  
-  class StartupProcess implements Callable<Set<? extends NodeMetadata>> {
-   
-    final private String clusterName;
-    final private int numberOfNodes;
-    final private int minNumberOfNodes;
-    final private int maxStartupRetries;
-    final private Set<String> roles;
-    final private ComputeService computeService;
-    final private Template template;
-    final private ExecutorService executorService;
-    final private NodeStarterFactory starterFactory;
-
-    private Set<NodeMetadata> successfulNodes = Sets.newLinkedHashSet();
-    private Map<NodeMetadata, Throwable> lostNodes = Maps.newHashMap();
-    
-    private Future<Set<NodeMetadata>> nodesFuture;
-        
-    StartupProcess(final String clusterName, final int numberOfNodes, 
-        final int minNumberOfNodes, final int maxStartupRetries, final 
Set<String> roles, 
-        final ComputeService computeService, final Template template, 
-        final ExecutorService executorService, final NodeStarterFactory 
starterFactory) {
-      this.clusterName = clusterName;
-      this.numberOfNodes = numberOfNodes;
-      this.minNumberOfNodes = minNumberOfNodes;
-      this.maxStartupRetries = maxStartupRetries;
-      this.roles = roles;
-      this.computeService = computeService;
-      this.template = template;
-      this.executorService = executorService;
-      this.starterFactory = starterFactory;
-    }
-
-    @Override
-    public Set<? extends NodeMetadata> call() throws Exception {
-      int retryCount = 0;
-      boolean retryRequired;
-      try {
-      do {   
-          runNodesWithTag();         
-          waitForOutcomes(); 
-          retryRequired = !isDone();
-          
-          if (++retryCount > maxStartupRetries) {
-            break; // no more retries
-          }
-        } while (retryRequired);
-        
-        if (retryRequired) {// if still required, we cannot use the cluster
-          // in this case of failed cluster startup, cleaning of the nodes are 
postponed
-          throw new IOException("Too many instance failed while bootstrapping! 
" 
-              + successfulNodes.size() + " successfully started instances 
while " + lostNodes.size() + " instances failed");      
-        }
-      } finally {
-        cleanupFailedNodes();
-      }
-      return successfulNodes;
-    }
-        
-    String getClusterName() {
-      return clusterName;
-    }
-    
-    Template getTemplate() {
-      return template;
-    }
-    
-    Set<NodeMetadata> getSuccessfulNodes() {
-      return successfulNodes;
-    }
-    
-    Map<NodeMetadata, Throwable> getNodeErrors() {
-      return lostNodes;
-    }
-    
-    boolean isDone() {
-      return successfulNodes.size() >= minNumberOfNodes;
-    }
-    
-    void runNodesWithTag() {
-      final int num = numberOfNodes - successfulNodes.size();
-      this.nodesFuture = executorService.submit(starterFactory.create(
-          computeService, clusterName, roles, num, template));
-    }
-    
-    void waitForOutcomes() throws InterruptedException {
-      try {
-        Set<? extends NodeMetadata> nodes = nodesFuture.get();
-        successfulNodes.addAll(nodes);
-      } catch (ExecutionException e) {
-        // checking RunNodesException and collect the outcome
-        Throwable th = e.getCause();
-        if (th instanceof RunNodesException) {
-          RunNodesException rnex = (RunNodesException) th;
-          successfulNodes.addAll(rnex.getSuccessfulNodes());
-          lostNodes.putAll(rnex.getNodeErrors());
-        } else {
-          LOG.error("Unexpected error while starting " + numberOfNodes + " 
nodes, minimum " 
-              + minNumberOfNodes + " nodes for " + roles + " of cluster " + 
clusterName, e);
-        }
-      }
-    }
-    
-    void cleanupFailedNodes() throws InterruptedException {
-      if (lostNodes.size() > 0) {
-        // parallel destroy of failed nodes
-        Set<Future<NodeMetadata>> deletingNodeFutures = 
Sets.newLinkedHashSet();
-        Iterator<?> it = lostNodes.keySet().iterator();
-        while (it.hasNext()) {
-          final NodeMetadata badNode = (NodeMetadata) it.next();         
-          deletingNodeFutures.add(executorService.submit(
-              new Callable<NodeMetadata>() {
-                public NodeMetadata call() throws Exception {
-                  final String nodeId = badNode.getId();
-                  LOG.info("Deleting failed node node {}", nodeId);
-                  computeService.destroyNode(nodeId);
-                  LOG.info("Node deleted: {}", nodeId);
-                  return badNode;
-                }
-              }
-            ));
-        }
-        Iterator<Future<NodeMetadata>> results = 
deletingNodeFutures.iterator();
-        while (results.hasNext()) {
-          try {
-            results.next().get();
-          } catch (ExecutionException e) {
-            LOG.warn("Error while destroying failed node:", e);
-          }
-        }
-      }
-    } 
-  }
-}
-
-class NodeStarterFactory {   
-  NodeStarter create(final ComputeService computeService, final String 
clusterName,
-      final Set<String> roles, final int num, final Template template) {
-    return new NodeStarter(computeService, clusterName, roles, num, template);
-  }
-}
-
-class NodeStarter implements Callable<Set<NodeMetadata>> {
-
-  private static final Logger LOG =
-    LoggerFactory.getLogger(NodeStarter.class);
-
-  final private ComputeService computeService;
-  final private String clusterName;
-  final private Set<String> roles;
-  final private int num;
-  final private Template template;
-  
-  public NodeStarter(final ComputeService computeService, final String 
clusterName,
-    final Set<String> roles, final int num, final Template template) {
-    this.computeService = computeService;
-    this.clusterName = clusterName;
-    this.roles = roles;
-    this.num = num;
-    this.template = template;
-  }
 
-  @SuppressWarnings("unchecked")
-  @Override
-  public Set<NodeMetadata> call() throws Exception {
-    LOG.info("Starting {} node(s) with roles {}", num,
-        roles);
-    Set<NodeMetadata> nodes = (Set<NodeMetadata>)computeService
-      .createNodesInGroup(clusterName, num, template);
-    LOG.info("Nodes started: {}", nodes);
-    return nodes;
-  }
 }
 

Added: 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/BootstrapTemplate.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/compute/BootstrapTemplate.java?rev=1182528&view=auto
==============================================================================
--- 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/BootstrapTemplate.java 
(added)
+++ 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/BootstrapTemplate.java 
Wed Oct 12 19:07:07 2011
@@ -0,0 +1,148 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.whirr.compute;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.service.jclouds.StatementBuilder;
+import org.apache.whirr.service.jclouds.TemplateBuilderStrategy;
+import org.jclouds.aws.ec2.AWSEC2Client;
+import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions;
+import org.jclouds.compute.ComputeService;
+import org.jclouds.compute.ComputeServiceContext;
+import org.jclouds.compute.domain.Template;
+import org.jclouds.compute.domain.TemplateBuilder;
+import org.jclouds.scriptbuilder.InitBuilder;
+import org.jclouds.scriptbuilder.domain.OsFamily;
+import org.jclouds.scriptbuilder.domain.Statement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+
+import static org.jclouds.compute.options.TemplateOptions.Builder.runScript;
+import static org.jclouds.scriptbuilder.domain.Statements.appendFile;
+import static org.jclouds.scriptbuilder.domain.Statements.interpret;
+import static org.jclouds.scriptbuilder.domain.Statements.newStatementList;
+
+public class BootstrapTemplate {
+
+  private static final Logger LOG =
+    LoggerFactory.getLogger(BootstrapTemplate.class);
+
+  public static Template build(
+    ClusterSpec clusterSpec,
+    ComputeService computeService,
+    StatementBuilder statementBuilder,
+    TemplateBuilderStrategy strategy
+  ) throws MalformedURLException {
+
+    LOG.info("Configuring template");
+
+    Statement runScript = addUserAndAuthorizeSudo(
+        clusterSpec.getClusterUser(),
+        clusterSpec.getPublicKey(),
+        clusterSpec.getPrivateKey(),
+        statementBuilder.build(clusterSpec));
+
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Running script:\n{}", runScript.render(OsFamily.UNIX));
+    }
+
+    TemplateBuilder templateBuilder = computeService.templateBuilder()
+      .options(runScript(runScript));
+    strategy.configureTemplateBuilder(clusterSpec, templateBuilder);
+
+    return setSpotInstancePriceIfSpecified(
+      computeService.getContext(), clusterSpec, templateBuilder.build()
+    );
+  }
+
+  private static Statement addUserAndAuthorizeSudo(
+    String user, String publicKey, String privateKey, Statement statement
+  ) {
+    return new InitBuilder(
+      "setup-" + user,// name of the script
+      "/tmp",// working directory
+      "/tmp/logs",// location of stdout.log and stderr.log
+      ImmutableMap.of("newUser", user, "defaultHome", "/home/users"), // 
variables
+      ImmutableList.<Statement> of(
+        createUserWithPublicAndPrivateKey(user, publicKey, privateKey),
+        makeSudoersOnlyPermitting(user),
+        statement)
+    );
+  }
+
+  /**
+   * Set maximum spot instance price based on the configuration
+   */
+  private static Template setSpotInstancePriceIfSpecified(
+      ComputeServiceContext context, ClusterSpec spec, Template template) {
+
+    if (context != null && context.getProviderSpecificContext().getApi() 
instanceof AWSEC2Client) {
+      if (spec.getAwsEc2SpotPrice() > 0) {
+        template.getOptions().as(AWSEC2TemplateOptions.class)
+          .spotPrice(spec.getAwsEc2SpotPrice());
+      }
+    }
+
+    return template;
+  }
+
+  // must be used inside InitBuilder, as this sets the shell variables used in 
this statement
+  private static Statement createUserWithPublicAndPrivateKey(String username,
+     String publicKey, String privateKey) {
+    // note directory must be created first
+    return newStatementList(
+      interpret(
+        "mkdir -p $DEFAULT_HOME/$NEW_USER/.ssh",
+        "useradd --shell /bin/bash -d $DEFAULT_HOME/$NEW_USER $NEW_USER\n"),
+      appendFile(
+        "$DEFAULT_HOME/$NEW_USER/.ssh/authorized_keys",
+        Splitter.on('\n').split(publicKey)),
+      appendFile(
+        "$DEFAULT_HOME/$NEW_USER/.ssh/id_rsa",
+        Splitter.on('\n').split(privateKey)),
+      interpret(
+        "chmod 400 $DEFAULT_HOME/$NEW_USER/.ssh/*",
+        "chown -R $NEW_USER $DEFAULT_HOME/$NEW_USER\n"));
+  }
+
+  // must be used inside InitBuilder, as this sets the shell variables used in 
this statement
+  private static Statement makeSudoersOnlyPermitting(String username) {
+    return newStatementList(
+      interpret(
+        "rm /etc/sudoers",
+        "touch /etc/sudoers",
+        "chmod 0440 /etc/sudoers",
+        "chown root /etc/sudoers\n"),
+      appendFile(
+        "/etc/sudoers",
+        ImmutableSet.of(
+          "root ALL = (ALL) ALL",
+          "%adm ALL = (ALL) ALL",
+          username + " ALL = (ALL) NOPASSWD: ALL")
+        )
+    );
+  }
+
+}

Added: whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarter.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarter.java?rev=1182528&view=auto
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarter.java 
(added)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarter.java 
Wed Oct 12 19:07:07 2011
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.whirr.compute;
+
+import org.jclouds.compute.ComputeService;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.Template;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+public class NodeStarter implements Callable<Set<NodeMetadata>> {
+
+  private static final Logger LOG =
+    LoggerFactory.getLogger(NodeStarter.class);
+
+  final private ComputeService computeService;
+  final private String clusterName;
+  final private Set<String> roles;
+  final private int num;
+  final private Template template;
+
+  public NodeStarter(final ComputeService computeService, final String 
clusterName,
+    final Set<String> roles, final int num, final Template template) {
+    this.computeService = computeService;
+    this.clusterName = clusterName;
+    this.roles = roles;
+    this.num = num;
+    this.template = template;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Set<NodeMetadata> call() throws Exception {
+    LOG.info("Starting {} node(s) with roles {}", num,
+        roles);
+    Set<NodeMetadata> nodes = (Set<NodeMetadata>)computeService
+      .createNodesInGroup(clusterName, num, template);
+    LOG.info("Nodes started: {}", nodes);
+    return nodes;
+  }
+}

Added: 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarterFactory.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarterFactory.java?rev=1182528&view=auto
==============================================================================
--- 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarterFactory.java 
(added)
+++ 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/NodeStarterFactory.java 
Wed Oct 12 19:07:07 2011
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.whirr.compute;
+
+import org.jclouds.compute.ComputeService;
+import org.jclouds.compute.domain.Template;
+
+import java.util.Set;
+
+public class NodeStarterFactory {
+  public NodeStarter create(final ComputeService computeService, final String 
clusterName,
+      final Set<String> roles, final int num, final Template template) {
+    return new NodeStarter(computeService, clusterName, roles, num, template);
+  }
+}

Added: 
whirr/trunk/core/src/main/java/org/apache/whirr/compute/StartupProcess.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/compute/StartupProcess.java?rev=1182528&view=auto
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/compute/StartupProcess.java 
(added)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/compute/StartupProcess.java 
Wed Oct 12 19:07:07 2011
@@ -0,0 +1,173 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.whirr.compute;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.jclouds.compute.ComputeService;
+import org.jclouds.compute.RunNodesException;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.Template;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class StartupProcess implements Callable<Set<? extends NodeMetadata>> {
+
+  private static final Logger LOG =
+    LoggerFactory.getLogger(StartupProcess.class);
+
+  final private String clusterName;
+  final private int numberOfNodes;
+  final private int minNumberOfNodes;
+  final private int maxStartupRetries;
+  final private Set<String> roles;
+  final private ComputeService computeService;
+  final private Template template;
+  final private ExecutorService executorService;
+  final private NodeStarterFactory starterFactory;
+
+  private Set<NodeMetadata> successfulNodes = Sets.newLinkedHashSet();
+  private Map<NodeMetadata, Throwable> lostNodes = Maps.newHashMap();
+
+  private Future<Set<NodeMetadata>> nodesFuture;
+
+  public StartupProcess(final String clusterName, final int numberOfNodes,
+                        final int minNumberOfNodes, final int 
maxStartupRetries, final Set<String> roles,
+                        final ComputeService computeService, final Template 
template,
+                        final ExecutorService executorService, final 
NodeStarterFactory starterFactory) {
+    this.clusterName = clusterName;
+    this.numberOfNodes = numberOfNodes;
+    this.minNumberOfNodes = minNumberOfNodes;
+    this.maxStartupRetries = maxStartupRetries;
+    this.roles = roles;
+    this.computeService = computeService;
+    this.template = template;
+    this.executorService = executorService;
+    this.starterFactory = starterFactory;
+  }
+
+  @Override
+  public Set<? extends NodeMetadata> call() throws Exception {
+    int retryCount = 0;
+    boolean retryRequired;
+    try {
+    do {
+        runNodesWithTag();
+        waitForOutcomes();
+        retryRequired = !isDone();
+
+        if (++retryCount > maxStartupRetries) {
+          break; // no more retries
+        }
+      } while (retryRequired);
+
+      if (retryRequired) {// if still required, we cannot use the cluster
+        // in this case of failed cluster startup, cleaning of the nodes are 
postponed
+        throw new IOException("Too many instance failed while bootstrapping! "
+            + successfulNodes.size() + " successfully started instances while 
" + lostNodes.size() + " instances failed");
+      }
+    } finally {
+      cleanupFailedNodes();
+    }
+    return successfulNodes;
+  }
+
+  String getClusterName() {
+    return clusterName;
+  }
+
+  Template getTemplate() {
+    return template;
+  }
+
+  Set<NodeMetadata> getSuccessfulNodes() {
+    return successfulNodes;
+  }
+
+  Map<NodeMetadata, Throwable> getNodeErrors() {
+    return lostNodes;
+  }
+
+  boolean isDone() {
+    return successfulNodes.size() >= minNumberOfNodes;
+  }
+
+  void runNodesWithTag() {
+    final int num = numberOfNodes - successfulNodes.size();
+    this.nodesFuture = executorService.submit(starterFactory.create(
+        computeService, clusterName, roles, num, template));
+  }
+
+  void waitForOutcomes() throws InterruptedException {
+    try {
+      Set<? extends NodeMetadata> nodes = nodesFuture.get();
+      successfulNodes.addAll(nodes);
+    } catch (ExecutionException e) {
+      // checking RunNodesException and collect the outcome
+      Throwable th = e.getCause();
+      if (th instanceof RunNodesException) {
+        RunNodesException rnex = (RunNodesException) th;
+        successfulNodes.addAll(rnex.getSuccessfulNodes());
+        lostNodes.putAll(rnex.getNodeErrors());
+      } else {
+        LOG.error("Unexpected error while starting " + numberOfNodes + " 
nodes, minimum "
+            + minNumberOfNodes + " nodes for " + roles + " of cluster " + 
clusterName, e);
+      }
+    }
+  }
+
+  void cleanupFailedNodes() throws InterruptedException {
+    if (lostNodes.size() > 0) {
+      // parallel destroy of failed nodes
+      Set<Future<NodeMetadata>> deletingNodeFutures = Sets.newLinkedHashSet();
+      Iterator<?> it = lostNodes.keySet().iterator();
+      while (it.hasNext()) {
+        final NodeMetadata badNode = (NodeMetadata) it.next();
+        deletingNodeFutures.add(executorService.submit(
+            new Callable<NodeMetadata>() {
+              public NodeMetadata call() throws Exception {
+                final String nodeId = badNode.getId();
+                LOG.info("Deleting failed node node {}", nodeId);
+                computeService.destroyNode(nodeId);
+                LOG.info("Node deleted: {}", nodeId);
+                return badNode;
+              }
+            }
+          ));
+      }
+      Iterator<Future<NodeMetadata>> results = deletingNodeFutures.iterator();
+      while (results.hasNext()) {
+        try {
+          results.next().get();
+        } catch (ExecutionException e) {
+          LOG.warn("Error while destroying failed node:", e);
+        }
+      }
+    }
+  }
+}

Modified: 
whirr/trunk/core/src/test/java/org/apache/whirr/actions/BootstrapClusterActionTest.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/test/java/org/apache/whirr/actions/BootstrapClusterActionTest.java?rev=1182528&r1=1182527&r2=1182528&view=diff
==============================================================================
--- 
whirr/trunk/core/src/test/java/org/apache/whirr/actions/BootstrapClusterActionTest.java
 (original)
+++ 
whirr/trunk/core/src/test/java/org/apache/whirr/actions/BootstrapClusterActionTest.java
 Wed Oct 12 19:07:07 2011
@@ -18,32 +18,17 @@
 
 package org.apache.whirr.actions;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.Stack;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.apache.commons.configuration.CompositeConfiguration;
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.configuration.PropertiesConfiguration;
 import org.apache.whirr.ClusterSpec;
 import org.apache.whirr.HandlerMapFactory;
+import org.apache.whirr.compute.NodeStarter;
+import org.apache.whirr.compute.NodeStarterFactory;
 import org.apache.whirr.service.ClusterActionHandler;
 import org.apache.whirr.service.ClusterActionHandlerFactory;
 import org.jclouds.compute.ComputeService;
@@ -71,6 +56,22 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Stack;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 public class BootstrapClusterActionTest {
 
   private static final Logger LOG =
@@ -132,7 +133,8 @@ public class BootstrapClusterActionTest 
     reaction.put(dntt, ddttStack);
     
     nodeStarterFactory = new TestNodeStarterFactory(reaction);
-    BootstrapClusterAction bootstrapper = new 
BootstrapClusterAction(getCompute, handlerMap, nodeStarterFactory);
+    BootstrapClusterAction bootstrapper =
+        new BootstrapClusterAction(getCompute, handlerMap, nodeStarterFactory);
     
     bootstrapper.execute(clusterSpec, null);
     if (nodeStarterFactory != null) {
@@ -221,10 +223,10 @@ public class BootstrapClusterActionTest 
     TestNodeStarterFactory(final Map<Set<String>, Stack<Integer>> plan) {
       this.plan = plan;
     }
-    
+
     @Override
-    NodeStarter create(ComputeService computeService, String clusterName,
-        Set<String> roles, int num, Template template) {
+    public NodeStarter create(final ComputeService computeService, final 
String clusterName,
+        final Set<String> roles, final int num, final Template template) {
       NodeStarter result = null;
       Stack<Integer> stack = plan.get(roles);
       if (stack != null) {


Reply via email to