You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@whirr.apache.org by as...@apache.org on 2012/01/25 14:22:36 UTC

svn commit: r1235735 [1/2] - in /whirr/trunk: ./ cli/src/main/java/org/apache/whirr/cli/ cli/src/main/java/org/apache/whirr/cli/command/ cli/src/main/resources/META-INF/services/ cli/src/test/java/org/apache/whirr/cli/ cli/src/test/java/org/apache/whir...

Author: asavu
Date: Wed Jan 25 13:22:35 2012
New Revision: 1235735

URL: http://svn.apache.org/viewvc?rev=1235735&view=rev
Log:
WHIRR-479. ScriptBasedClusterAction should allow filtering by role and instance-id (asavu)

Added:
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/HelpCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RoleLifecycleCommand.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/BaseCommandTest.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureServicesAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/StartServicesAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/StopServicesAction.java
    whirr/trunk/core/src/test/java/org/apache/whirr/actions/CleanupClusterActionTest.java
    whirr/trunk/core/src/test/java/org/apache/whirr/actions/ConfigureServicesActionTest.java
    whirr/trunk/core/src/test/java/org/apache/whirr/actions/ScriptBasedClusterActionTest.java
    whirr/trunk/core/src/test/java/org/apache/whirr/actions/StartServicesActionTest.java
    whirr/trunk/core/src/test/java/org/apache/whirr/actions/StopServicesActionTest.java
Removed:
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureClusterAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/StartClusterAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/StopClusterAction.java
Modified:
    whirr/trunk/CHANGES.txt
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/Main.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/CleanupClusterCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ConfigureServicesCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyClusterCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyInstanceCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/LaunchClusterCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListProvidersCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RunScriptCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StartServicesCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StopServicesCommand.java
    whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/VersionCommand.java
    whirr/trunk/cli/src/main/resources/META-INF/services/org.apache.whirr.command.Command
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/MainTest.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyClusterCommandTest.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyInstanceCommandTest.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/LaunchClusterCommandTest.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/RunScriptCommandTest.java
    whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/VersionCommandTest.java
    whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/CleanupClusterAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/actions/ScriptBasedClusterAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/command/AbstractClusterCommand.java
    whirr/trunk/core/src/main/java/org/apache/whirr/command/Command.java
    whirr/trunk/core/src/main/java/org/apache/whirr/service/DryRunModule.java
    whirr/trunk/core/src/test/java/org/apache/whirr/service/DryRunModuleTest.java
    whirr/trunk/core/src/test/resources/META-INF/services/org.apache.whirr.service.ClusterActionHandler

Modified: whirr/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/whirr/trunk/CHANGES.txt?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/CHANGES.txt (original)
+++ whirr/trunk/CHANGES.txt Wed Jan 25 13:22:35 2012
@@ -27,6 +27,9 @@ Trunk (unreleased changes)
 
     WHIRR-483. Upgrade to jclouds 1.3.1 (asavu)
 
+    WHIRR-479. ScriptBasedClusterAction should allow filtering by 
+    role and instance-id (asavu) 
+
   BUG FIXES
 
     WHIRR-367. Wrong groupId for zookeeper (Joe Crobak via asavu)

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/Main.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/Main.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/Main.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/Main.java Wed Jan 25 13:22:35 2012
@@ -18,7 +18,6 @@
 
 package org.apache.whirr.cli;
 
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import org.apache.whirr.command.Command;
@@ -33,6 +32,8 @@ import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.SortedSet;
 
+import static com.google.common.collect.Lists.newArrayList;
+
 /**
  * The entry point for the Whirr CLI.
  */
@@ -42,6 +43,10 @@ public class Main {
   private int maxLen = 0;
   
   Main(Command... commands) throws IOException {
+    this(newArrayList(commands));
+  }
+  
+  Main(Iterable<Command> commands) throws IOException {
     for (Command command : commands) {
       commandMap.put(command.getName(), command);
       maxLen = Math.max(maxLen, command.getName().length());
@@ -92,7 +97,7 @@ public class Main {
 
   public static void main(String... args) throws Exception {
     ServiceLoader<Command> loader = ServiceLoader.load(Command.class);
-    Main main = new Main(Lists.newArrayList(loader).toArray(new Command[0]));
+    Main main = new Main(loader);
 
     int rc = main.run(System.in, System.out, System.err, Arrays.asList(args));
     System.exit(rc);

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/CleanupClusterCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/CleanupClusterCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/CleanupClusterCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/CleanupClusterCommand.java Wed Jan 25 13:22:35 2012
@@ -18,7 +18,6 @@
 
 package org.apache.whirr.cli.command;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
@@ -54,11 +53,11 @@ public class CleanupClusterCommand exten
       List<String> args) throws Exception {
     
     OptionSet optionSet = parser.parse(args.toArray(new String[0]));
-
     if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
+      printUsage(err);
       return -1;
     }
+
     try {
       ClusterSpec clusterSpec = getClusterSpec(optionSet);
       ClusterController controller = createClusterController(clusterSpec.getServiceName());
@@ -66,13 +65,13 @@ public class CleanupClusterCommand exten
       return 0;
 
     } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
+      printErrorAndHelpHint(err, e);
       return -1;
     }
   }
 
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
     stream.println("Usage: whirr cleanup-cluster [OPTIONS]");
     stream.println();
     parser.printHelpOn(stream);

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ConfigureServicesCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ConfigureServicesCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ConfigureServicesCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ConfigureServicesCommand.java Wed Jan 25 13:22:35 2012
@@ -18,23 +18,18 @@
 
 package org.apache.whirr.cli.command;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
-import org.apache.whirr.command.AbstractClusterCommand;
 import org.apache.whirr.state.ClusterStateStoreFactory;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.util.List;
 
 /**
  * A command to configure the cluster services
  */
-public class ConfigureServicesCommand extends AbstractClusterCommand {
+public class ConfigureServicesCommand extends RoleLifecycleCommand {
 
   public ConfigureServicesCommand() throws IOException {
     this(new ClusterControllerFactory());
@@ -48,33 +43,16 @@ public class ConfigureServicesCommand ex
                                   ClusterStateStoreFactory stateStoreFactory) {
     super("configure-services", "Configure the cluster services.", factory, stateStoreFactory);
   }
-  
-  @Override
-  public int run(InputStream in, PrintStream out, PrintStream err,
-      List<String> args) throws Exception {
-    
-    OptionSet optionSet = parser.parse(args.toArray(new String[0]));
-
-    if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
-      return -1;
-    }
-    try {
-      ClusterSpec clusterSpec = getClusterSpec(optionSet);
-      ClusterController controller = createClusterController(clusterSpec.getServiceName());
-      controller.configureServices(clusterSpec);
-      return 0;
-
-    } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
-      return -1;
-    }
-  }
 
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
-    stream.println("Usage: whirr configure-services [OPTIONS]");
-    stream.println();
-    parser.printHelpOn(stream);
+  @Override
+  public int runLifecycleStep(ClusterSpec clusterSpec, ClusterController controller, OptionSet optionSet)
+      throws IOException, InterruptedException {
+    controller.configureServices(
+        clusterSpec,
+        getCluster(clusterSpec, controller),
+        getTargetRolesOrEmpty(optionSet),
+        getTargetInstanceIdsOrEmpty(optionSet)
+    );
+    return 0;
   }
 }

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyClusterCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyClusterCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyClusterCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyClusterCommand.java Wed Jan 25 13:22:35 2012
@@ -23,7 +23,6 @@ import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.List;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 
 import org.apache.whirr.ClusterController;
@@ -51,7 +50,7 @@ public class DestroyClusterCommand exten
     OptionSet optionSet = parser.parse(args.toArray(new String[0]));
 
     if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
+      printUsage(err);
       return -1;
     }
     try {
@@ -61,15 +60,9 @@ public class DestroyClusterCommand exten
       controller.destroyCluster(clusterSpec);
       return 0;
     } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
+      printErrorAndHelpHint(err, e);
       return -1;
     }
   }
 
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
-    stream.println("Usage: whirr destroy-cluster [OPTIONS]");
-    stream.println();
-    parser.printHelpOn(stream);
-  }
 }

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyInstanceCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyInstanceCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyInstanceCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/DestroyInstanceCommand.java Wed Jan 25 13:22:35 2012
@@ -23,7 +23,6 @@ import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.List;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
 
@@ -57,12 +56,12 @@ public class DestroyInstanceCommand exte
 
     OptionSet optionSet = parser.parse(args.toArray(new String[0]));
     if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
+      printUsage(err);
       return -1;
     }
     try {
       if (!optionSet.hasArgument(instanceOption)) {
-        throw new IllegalArgumentException("You need to specify an instance ID.");
+        throw new IllegalArgumentException("--instance-id is a mandatory argument");
       }
       ClusterSpec clusterSpec = getClusterSpec(optionSet);
       ClusterController controller = createClusterController(clusterSpec.getServiceName());
@@ -73,13 +72,13 @@ public class DestroyInstanceCommand exte
       return 0;
 
     } catch(IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
+      printErrorAndHelpHint(err, e);
       return -1;
     }
   }
 
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
     stream.println("Usage: whirr destroy-instance --instance-id <region/ID> [OPTIONS]");
     stream.println();
     parser.printHelpOn(stream);

Added: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/HelpCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/HelpCommand.java?rev=1235735&view=auto
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/HelpCommand.java (added)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/HelpCommand.java Wed Jan 25 13:22:35 2012
@@ -0,0 +1,59 @@
+/**
+ * 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.cli.command;
+
+import org.apache.whirr.command.Command;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.ServiceLoader;
+
+public class HelpCommand extends Command {
+
+  public HelpCommand() {
+    super("help", "Show help about an action");
+  }
+
+  @Override
+  public int run(InputStream in, PrintStream out, PrintStream err, List<String> args) throws Exception {
+    if (args.size() == 0) {
+      printUsage(out);
+      return -1;
+    }
+
+    String helpForCommand = args.get(0);
+    ServiceLoader<Command> loader = ServiceLoader.load(Command.class);
+    for(Command command : loader) {
+      if (command.getName().equals(helpForCommand)) {
+        command.printUsage(out);
+        return 0;
+      }
+    }
+
+    err.println("No command found with that name: " + helpForCommand);
+    return -2;
+  }
+
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
+    stream.println("Usage: whirr help <command>");
+  }
+}

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/LaunchClusterCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/LaunchClusterCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/LaunchClusterCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/LaunchClusterCommand.java Wed Jan 25 13:22:35 2012
@@ -23,7 +23,6 @@ import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.List;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 
 import org.apache.whirr.Cluster;
@@ -50,10 +49,10 @@ public class LaunchClusterCommand extend
   public int run(InputStream in, PrintStream out, PrintStream err,
       List<String> args) throws Exception {
     
-    OptionSet optionSet = parser.parse(args.toArray(new String[0]));
+    OptionSet optionSet = parser.parse(args.toArray(new String[args.size()]));
 
     if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
+      printUsage(err);
       return -1;
     }
     
@@ -71,15 +70,8 @@ public class LaunchClusterCommand extend
       
       return 0;
     } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
+      printErrorAndHelpHint(err, e);
       return -1;
     }
   }
-
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
-    stream.println("Usage: whirr launch-cluster [OPTIONS]");
-    stream.println();
-    parser.printHelpOn(stream);
-  }
 }

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java Wed Jan 25 13:22:35 2012
@@ -25,7 +25,6 @@ import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.List;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 
 import org.apache.whirr.Cluster;
@@ -58,10 +57,10 @@ public class ListClusterCommand extends 
   public int run(InputStream in, PrintStream out, PrintStream err,
       List<String> args) throws Exception {
     
-    OptionSet optionSet = parser.parse(args.toArray(new String[0]));
+    OptionSet optionSet = parser.parse(args.toArray(new String[args.size()]));
 
     if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
+      printUsage(err);
       return -1;
     }
     try {
@@ -83,15 +82,8 @@ public class ListClusterCommand extends 
       }
       return 0;
     } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
+      printErrorAndHelpHint(err, e);
       return -1;
     }
   }
-
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
-    stream.println("Usage: whirr list-cluster [OPTIONS]");
-    stream.println();
-    parser.printHelpOn(stream);
-  }
 }

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListProvidersCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListProvidersCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListProvidersCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/ListProvidersCommand.java Wed Jan 25 13:22:35 2012
@@ -59,8 +59,9 @@ public class ListProvidersCommand extend
     return 0;
   }
 
-  private void printUsage(PrintStream out) {
-    out.println("whirr list-providers <compute OR blobstore>");
+  @Override
+  public void printUsage(PrintStream out) {
+    out.println("Usage: whirr list-providers <compute OR blobstore>");
   }
 
   private void listBlobstoreProviders(PrintStream out) {

Added: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RoleLifecycleCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RoleLifecycleCommand.java?rev=1235735&view=auto
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RoleLifecycleCommand.java (added)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RoleLifecycleCommand.java Wed Jan 25 13:22:35 2012
@@ -0,0 +1,114 @@
+/**
+ * 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.cli.command;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import org.apache.whirr.ClusterController;
+import org.apache.whirr.ClusterControllerFactory;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.command.AbstractClusterCommand;
+import org.apache.whirr.state.ClusterStateStoreFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Set;
+
+import static com.google.common.collect.Sets.newHashSet;
+
+public abstract class RoleLifecycleCommand  extends AbstractClusterCommand {
+
+  private final static ImmutableSet<String> EMPTYSET = ImmutableSet.of();
+
+  private OptionSpec<String> rolesOption = parser
+      .accepts("roles", "Cluster roles to target")
+      .withRequiredArg().ofType(String.class);
+
+  private OptionSpec<String> instanceIdsOption = parser
+      .accepts("instance-ids", "Cluster instance IDs to target")
+      .withRequiredArg().ofType(String.class);
+
+  public RoleLifecycleCommand(String name, String description,
+      ClusterControllerFactory factory, ClusterStateStoreFactory stateStoreFactory) {
+    super(name, description, factory, stateStoreFactory);
+  }
+
+  /**
+   * Implement this method to trigger the relevant role lifecycle action
+   */
+  public abstract int runLifecycleStep(ClusterSpec clusterSpec, ClusterController controller, OptionSet optionSet)
+      throws IOException, InterruptedException;
+
+  @Override
+  public int run(InputStream in, PrintStream out, PrintStream err, List<String> args) throws Exception {
+    OptionSet optionSet = parser.parse(args.toArray(new String[args.size()]));
+    if (!optionSet.nonOptionArguments().isEmpty()) {
+      printUsage(err);
+      return -1;
+    }
+
+    try {
+      ClusterSpec clusterSpec = getClusterSpec(optionSet);
+      return runLifecycleStep(
+          clusterSpec,
+          createClusterController(clusterSpec.getServiceName()),
+          optionSet
+      );
+
+    } catch (IllegalArgumentException e) {
+      printErrorAndHelpHint(err, e);
+      return -1;
+    }
+  }
+
+  /**
+   * Get the list of targeted roles for this command or an empty set
+   */
+  protected Set<String> getTargetRolesOrEmpty(OptionSet optionSet) {
+    if (optionSet.hasArgument(rolesOption)) {
+      return newHashSet(Splitter.on(",").split(optionSet.valueOf(rolesOption)));
+    }
+    return EMPTYSET;
+  }
+
+  /**
+   * Get the list of targeted instance IDs for this command or an empty set
+   */
+  protected Set<String> getTargetInstanceIdsOrEmpty(OptionSet optionSet) {
+    if (optionSet.hasArgument(instanceIdsOption)) {
+      return newHashSet(Splitter.on(",").split(optionSet.valueOf(instanceIdsOption)));
+    }
+    return EMPTYSET;
+  }
+  
+  /**
+   * Print a generic usage indication for this class of commands
+   */
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
+    stream.println("Usage: whirr " + getName() + " [OPTIONS] [--roles role1,role2] [--instance-ids id1,id2]");
+    stream.println();
+    parser.printHelpOn(stream);
+  }
+
+}

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RunScriptCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RunScriptCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RunScriptCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/RunScriptCommand.java Wed Jan 25 13:22:35 2012
@@ -24,7 +24,6 @@ import com.google.common.collect.Iterabl
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.io.Files;
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
 import org.apache.commons.lang.StringUtils;
@@ -89,13 +88,13 @@ public class RunScriptCommand extends Ab
     OptionSet optionSet = parser.parse(args.toArray(new String[0]));
     if (!optionSet.has(scriptOption)) {
       err.println("Please specify a script file to be executed.");
-      printUsage(parser, err);
+      err.println("Get more help: whirr help " + getName());
       return -1;
     }
 
     if (!(new File(optionSet.valueOf(scriptOption))).exists()) {
       err.printf("Script file '%s' not found.", optionSet.valueOf(scriptOption));
-      printUsage(parser, err);
+      err.println("Get more help: whirr help " + getName());
       return -2;
     }
 
@@ -109,8 +108,7 @@ public class RunScriptCommand extends Ab
         spec, condition, execFile(optionSet.valueOf(scriptOption))));
 
     } catch(IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
+      printErrorAndHelpHint(err, e);
       return -3;
     }
   }
@@ -168,8 +166,8 @@ public class RunScriptCommand extends Ab
       "\n");
   }
 
-  private void printUsage(OptionParser parser,
-                          PrintStream stream) throws IOException {
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
     stream.println("Usage: whirr run-script [OPTIONS] --script <script> " +
       "[--instances id1,id2] [--roles role1,role2]");
     stream.println();

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StartServicesCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StartServicesCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StartServicesCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StartServicesCommand.java Wed Jan 25 13:22:35 2012
@@ -18,23 +18,18 @@
 
 package org.apache.whirr.cli.command;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
-import org.apache.whirr.command.AbstractClusterCommand;
 import org.apache.whirr.state.ClusterStateStoreFactory;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.util.List;
 
 /**
  * A command to start the cluster services
  */
-public class StartServicesCommand extends AbstractClusterCommand {
+public class StartServicesCommand extends RoleLifecycleCommand {
 
   public StartServicesCommand() throws IOException {
     this(new ClusterControllerFactory());
@@ -48,33 +43,16 @@ public class StartServicesCommand extend
                               ClusterStateStoreFactory stateStoreFactory) {
     super("start-services", "Start the cluster services.", factory, stateStoreFactory);
   }
-  
-  @Override
-  public int run(InputStream in, PrintStream out, PrintStream err,
-      List<String> args) throws Exception {
-    
-    OptionSet optionSet = parser.parse(args.toArray(new String[0]));
-
-    if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
-      return -1;
-    }
-    try {
-      ClusterSpec clusterSpec = getClusterSpec(optionSet);
-      ClusterController controller = createClusterController(clusterSpec.getServiceName());
-      controller.startServices(clusterSpec);
-      return 0;
-
-    } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
-      return -1;
-    }
-  }
 
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
-    stream.println("Usage: whirr start-services [OPTIONS]");
-    stream.println();
-    parser.printHelpOn(stream);
+  @Override
+  public int runLifecycleStep(ClusterSpec clusterSpec, ClusterController controller, OptionSet optionSet)
+      throws IOException, InterruptedException {
+    controller.startServices(
+        clusterSpec,
+        getCluster(clusterSpec, controller),
+        getTargetRolesOrEmpty(optionSet),
+        getTargetInstanceIdsOrEmpty(optionSet)
+    );
+    return 0;
   }
 }

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StopServicesCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StopServicesCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StopServicesCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/StopServicesCommand.java Wed Jan 25 13:22:35 2012
@@ -18,23 +18,18 @@
 
 package org.apache.whirr.cli.command;
 
-import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
-import org.apache.whirr.command.AbstractClusterCommand;
 import org.apache.whirr.state.ClusterStateStoreFactory;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.util.List;
 
 /**
  * A command to stop the cluster services
  */
-public class StopServicesCommand extends AbstractClusterCommand {
+public class StopServicesCommand extends RoleLifecycleCommand {
 
   public StopServicesCommand() throws IOException {
     this(new ClusterControllerFactory());
@@ -48,33 +43,16 @@ public class StopServicesCommand extends
                              ClusterStateStoreFactory stateStoreFactory) {
     super("stop-services", "Stop the cluster services.", factory, stateStoreFactory);
   }
-  
-  @Override
-  public int run(InputStream in, PrintStream out, PrintStream err,
-      List<String> args) throws Exception {
-    
-    OptionSet optionSet = parser.parse(args.toArray(new String[0]));
-
-    if (!optionSet.nonOptionArguments().isEmpty()) {
-      printUsage(parser, err);
-      return -1;
-    }
-    try {
-      ClusterSpec clusterSpec = getClusterSpec(optionSet);
-      ClusterController controller = createClusterController(clusterSpec.getServiceName());
-      controller.stopServices(clusterSpec);
-      return 0;
-
-    } catch (IllegalArgumentException e) {
-      err.println(e.getMessage());
-      printUsage(parser, err);
-      return -1;
-    }
-  }
 
-  private void printUsage(OptionParser parser, PrintStream stream) throws IOException {
-    stream.println("Usage: whirr stop-services [OPTIONS]");
-    stream.println();
-    parser.printHelpOn(stream);
+  @Override
+  public int runLifecycleStep(ClusterSpec clusterSpec, ClusterController controller, OptionSet optionSet)
+      throws IOException, InterruptedException {
+    controller.stopServices(
+        clusterSpec,
+        getCluster(clusterSpec, controller),
+        getTargetRolesOrEmpty(optionSet),
+        getTargetInstanceIdsOrEmpty(optionSet)
+    );
+    return 0;
   }
 }

Modified: whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/VersionCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/VersionCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/VersionCommand.java (original)
+++ whirr/trunk/cli/src/main/java/org/apache/whirr/cli/command/VersionCommand.java Wed Jan 25 13:22:35 2012
@@ -18,6 +18,7 @@
 
 package org.apache.whirr.cli.command;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.List;
@@ -45,4 +46,8 @@ public class VersionCommand extends Comm
     return 0;
   }
 
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
+  }
+
 }

Modified: whirr/trunk/cli/src/main/resources/META-INF/services/org.apache.whirr.command.Command
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/main/resources/META-INF/services/org.apache.whirr.command.Command?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/main/resources/META-INF/services/org.apache.whirr.command.Command (original)
+++ whirr/trunk/cli/src/main/resources/META-INF/services/org.apache.whirr.command.Command Wed Jan 25 13:22:35 2012
@@ -20,3 +20,4 @@ org.apache.whirr.cli.command.ListCluster
 org.apache.whirr.cli.command.ListProvidersCommand
 org.apache.whirr.cli.command.RunScriptCommand
 org.apache.whirr.cli.command.VersionCommand
+org.apache.whirr.cli.command.HelpCommand

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/MainTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/MainTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/MainTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/MainTest.java Wed Jan 25 13:22:35 2012
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.when;
 import com.google.common.collect.Lists;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.Collections;
@@ -48,6 +49,10 @@ public class MainTest {
         List<String> args) throws Exception {
       return 0;
     }
+
+    @Override
+    public void printUsage(PrintStream stream) throws IOException {
+    }
   }
   
   @Test

Added: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/BaseCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/BaseCommandTest.java?rev=1235735&view=auto
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/BaseCommandTest.java (added)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/BaseCommandTest.java Wed Jan 25 13:22:35 2012
@@ -0,0 +1,96 @@
+/**
+ * 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.cli.command;
+
+import com.google.common.collect.ListMultimap;
+import org.apache.whirr.service.DryRunModule;
+import org.jclouds.compute.callables.RunScriptOnNode;
+import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSsh;
+import org.jclouds.compute.callables.SudoAwareInitManager;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.scriptbuilder.InitBuilder;
+import org.junit.Before;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import static junit.framework.Assert.fail;
+
+public class BaseCommandTest {
+
+  protected ByteArrayOutputStream outBytes;
+  protected PrintStream out;
+
+  protected ByteArrayOutputStream errBytes;
+  protected PrintStream err;
+
+  @Before
+  public void setUp() {
+    outBytes = new ByteArrayOutputStream();
+    out = new PrintStream(outBytes);
+
+    errBytes = new ByteArrayOutputStream();
+    err = new PrintStream(errBytes);
+  }
+
+  protected void assertNoEntryForPhases(DryRunModule.DryRun dryRun, String... phases) throws Exception {
+    for (String phaseName : phases) {
+      try {
+        fail("Found entry: " + getEntryForPhase(dryRun.getExecutions(), phaseName) + " for phase: " + phaseName);
+      } catch (IllegalStateException e) {
+        // No entry found - OK
+      }
+    }
+  }
+
+  protected void assertExecutedPhases(DryRunModule.DryRun dryRun, String... phases) throws Exception {
+    for (String phaseName : phases) {
+      try {
+        getEntryForPhase(dryRun.getExecutions(), phaseName);
+      } catch (IllegalStateException e) {
+        fail("No entry found for phase: " + phaseName);
+      }
+    }
+  }
+
+  private Map.Entry<NodeMetadata, RunScriptOnNode> getEntryForPhase(
+      ListMultimap<NodeMetadata, RunScriptOnNode> executions, String phaseName)
+      throws Exception {
+    for (Map.Entry<NodeMetadata, RunScriptOnNode> entry : executions.entries()) {
+      if (getScriptName(entry.getValue()).startsWith(phaseName)) {
+        return entry;
+      }
+    }
+    throw new IllegalStateException("phase not found: " + phaseName);
+  }
+
+  private String getScriptName(RunScriptOnNode script) throws Exception {
+    if (script instanceof RunScriptOnNodeAsInitScriptUsingSsh) {
+      Field initField = SudoAwareInitManager.class
+          .getDeclaredField("init");
+      initField.setAccessible(true);
+      return ((InitBuilder) initField
+          .get(script))
+          .getInstanceName();
+    }
+    throw new IllegalArgumentException();
+  }
+}

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyClusterCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyClusterCommandTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyClusterCommandTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyClusterCommandTest.java Wed Jan 25 13:22:35 2012
@@ -18,57 +18,35 @@
 
 package org.apache.whirr.cli.command;
 
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 import com.google.common.collect.Lists;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.PrintStream;
-import java.util.Collections;
-import java.util.Map;
-
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.configuration.PropertiesConfiguration;
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
 import org.apache.whirr.util.KeyPair;
-import org.hamcrest.Matcher;
-import org.junit.Before;
 import org.junit.Test;
-import org.junit.internal.matchers.StringContains;
 
-public class DestroyClusterCommandTest {
+import java.io.File;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DestroyClusterCommandTest extends BaseCommandTest {
 
-  private ByteArrayOutputStream outBytes;
-  private PrintStream out;
-  private ByteArrayOutputStream errBytes;
-  private PrintStream err;
-
-  @Before
-  public void setUp() {
-    outBytes = new ByteArrayOutputStream();
-    out = new PrintStream(outBytes);
-    errBytes = new ByteArrayOutputStream();
-    err = new PrintStream(errBytes);
-  }
-  
   @Test
   public void testInsufficientOptions() throws Exception {
     DestroyClusterCommand command = new DestroyClusterCommand();
     int rc = command.run(null, null, err, Collections.<String>emptyList());
     assertThat(rc, is(-1));
-    assertThat(errBytes.toString(), containsUsageString());
-  }
-  
-  private Matcher<String> containsUsageString() {
-    return StringContains.containsString("Usage: whirr destroy-cluster [OPTIONS]");
+    assertThat(errBytes.toString(), containsString("Option 'cluster-name' not set"));
   }
   
   @Test

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyInstanceCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyInstanceCommandTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyInstanceCommandTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/DestroyInstanceCommandTest.java Wed Jan 25 13:22:35 2012
@@ -18,46 +18,27 @@
 
 package org.apache.whirr.cli.command;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 import com.google.common.collect.Lists;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.PrintStream;
-import java.util.Collections;
-import java.util.Map;
-
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
 import org.apache.whirr.util.KeyPair;
-import org.junit.Before;
 import org.junit.Test;
 
-public class DestroyInstanceCommandTest {
-
-  private ByteArrayOutputStream outBytes;
-  private PrintStream out;
-
-  private ByteArrayOutputStream errBytes;
-  private PrintStream err;
+import java.io.File;
+import java.util.Collections;
+import java.util.Map;
 
-  @Before
-  public void setUp() {
-    outBytes = new ByteArrayOutputStream();
-    out = new PrintStream(outBytes);
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-    errBytes = new ByteArrayOutputStream();
-    err = new PrintStream(errBytes);
-  }
+public class DestroyInstanceCommandTest extends BaseCommandTest {
 
   @Test
   public void testInstanceIdMandatory() throws Exception {
@@ -66,10 +47,7 @@ public class DestroyInstanceCommandTest 
     assertThat(rc, is(-1));
 
     String errOutput = errBytes.toString();
-    assertThat(errOutput, containsString("You need to specify " +
-        "an instance ID."));
-    assertThat(errOutput, containsString("Usage: whirr destroy-instance" +
-        " --instance-id <region/ID> [OPTIONS]"));
+    assertThat(errOutput, containsString("--instance-id is a mandatory argument"));
   }
 
   @Test

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/LaunchClusterCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/LaunchClusterCommandTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/LaunchClusterCommandTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/LaunchClusterCommandTest.java Wed Jan 25 13:22:35 2012
@@ -18,22 +18,8 @@
 
 package org.apache.whirr.cli.command;
 
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.PrintStream;
-import java.util.Collections;
-import java.util.Map;
-
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.configuration.PropertiesConfiguration;
 import org.apache.whirr.Cluster;
@@ -41,37 +27,31 @@ import org.apache.whirr.ClusterControlle
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
 import org.apache.whirr.InstanceTemplate;
+import org.apache.whirr.service.DryRunModule;
 import org.apache.whirr.util.KeyPair;
-import org.hamcrest.Matcher;
-import org.junit.Before;
+import org.hamcrest.MatcherAssert;
 import org.junit.Test;
-import org.junit.internal.matchers.StringContains;
 
-public class LaunchClusterCommandTest {
+import java.io.File;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-  private ByteArrayOutputStream outBytes;
-  private PrintStream out;
-  private ByteArrayOutputStream errBytes;
-  private PrintStream err;
-
-  @Before
-  public void setUp() {
-    outBytes = new ByteArrayOutputStream();
-    out = new PrintStream(outBytes);
+public class LaunchClusterCommandTest extends BaseCommandTest{
 
-    errBytes = new ByteArrayOutputStream();
-    err = new PrintStream(errBytes);
-  }
   @Test
   public void testInsufficientArgs() throws Exception {
     LaunchClusterCommand command = new LaunchClusterCommand();
     int rc = command.run(null, null, err, Collections.<String>emptyList());
     assertThat(rc, is(-1));
-    assertThat(errBytes.toString(), containsUsageString());
-  }
-  
-  private Matcher<String> containsUsageString() {
-    return StringContains.containsString("Usage: whirr launch-cluster [OPTIONS]");
+    assertThat(errBytes.toString(), containsString("Option 'cluster-name' not set."));
   }
   
   @Test
@@ -139,7 +119,7 @@ public class LaunchClusterCommandTest {
         "--cluster-name", "test-cluster",
         "--instance-templates", "1 hadoop-namenode+hadoop-jobtracker,3 hadoop-datanode+hadoop-tasktracker",
         "--instance-templates-max-percent-failures", "60 hadoop-datanode+hadoop-tasktracker",
-        "--provider", "ec2",
+        "--provider", "aws-ec2",
         "--identity", "myusername", "--credential", "mypassword",
         "--private-key-file", keys.get("private").getAbsolutePath(),
         "--version", "version-string"
@@ -159,7 +139,7 @@ public class LaunchClusterCommandTest {
         .roles("hadoop-datanode", "hadoop-tasktracker").build()
     ));
     expectedClusterSpec.setServiceName("hadoop");
-    expectedClusterSpec.setProvider("ec2");
+    expectedClusterSpec.setProvider("aws-ec2");
     expectedClusterSpec.setIdentity("myusername");
     expectedClusterSpec.setCredential("mypassword");
     expectedClusterSpec.setClusterName("test-cluster");
@@ -172,4 +152,23 @@ public class LaunchClusterCommandTest {
     
     assertThat(outBytes.toString(), containsString("Started cluster of 0 instances")); 
   }
+
+  @Test
+  public void testLaunchClusterUsingDryRun() throws Exception {
+    DryRunModule.resetDryRun();
+
+    ClusterControllerFactory factory = new ClusterControllerFactory();
+    LaunchClusterCommand launchCluster = new LaunchClusterCommand(factory);
+
+    int rc = launchCluster.run(null, out, err, ImmutableList.of(
+        "--cluster-name", "test-cluster-launch",
+        "--state-store", "none",
+        "--instance-templates", "1 zookeeper+cassandra, 1 zookeeper+elasticsearch",
+        "--provider", "stub",
+        "--identity", "dummy"
+    ));
+
+    MatcherAssert.assertThat(rc, is(0));
+    assertExecutedPhases(DryRunModule.DryRun.INSTANCE, "setup", "configure", "start");
+  }
 }

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java Wed Jan 25 13:22:35 2012
@@ -19,6 +19,7 @@
 package org.apache.whirr.cli.command;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThat;
 import static org.mockito.Matchers.any;
@@ -29,9 +30,7 @@ import static org.mockito.Mockito.when;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.PrintStream;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -44,42 +43,22 @@ import org.apache.whirr.state.ClusterSta
 import org.apache.whirr.state.ClusterStateStoreFactory;
 import org.apache.whirr.state.MemoryClusterStateStore;
 import org.apache.whirr.util.KeyPair;
-import org.hamcrest.Matcher;
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.domain.NodeMetadataBuilder;
 import org.jclouds.compute.domain.NodeState;
 import org.jclouds.domain.Credentials;
 import org.jclouds.domain.LocationBuilder;
 import org.jclouds.domain.LocationScope;
-import org.junit.Before;
 import org.junit.Test;
-import org.junit.internal.matchers.StringContains;
 
-public class ListClusterCommandTest {
-
-  private ByteArrayOutputStream outBytes;
-  private PrintStream out;
-  private ByteArrayOutputStream errBytes;
-  private PrintStream err;
-
-  @Before
-  public void setUp() {
-    outBytes = new ByteArrayOutputStream();
-    out = new PrintStream(outBytes);
-    errBytes = new ByteArrayOutputStream();
-    err = new PrintStream(errBytes);
-  }
+public class ListClusterCommandTest extends BaseCommandTest {
 
   @Test
   public void testInsufficientOptions() throws Exception {
     ListClusterCommand command = new ListClusterCommand();
     int rc = command.run(null, null, err, Collections.<String>emptyList());
     assertThat(rc, is(-1));
-    assertThat(errBytes.toString(), containsUsageString());
-  }
-
-  private Matcher<String> containsUsageString() {
-    return StringContains.containsString("Usage: whirr list-cluster [OPTIONS]");
+    assertThat(errBytes.toString(), containsString("Option 'cluster-name' not set."));
   }
 
   @Test

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/RunScriptCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/RunScriptCommandTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/RunScriptCommandTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/RunScriptCommandTest.java Wed Jan 25 13:22:35 2012
@@ -33,13 +33,10 @@ import org.apache.whirr.util.KeyPair;
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.domain.Credentials;
 import org.jclouds.scriptbuilder.domain.Statement;
-import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.PrintStream;
 import java.util.Map;
 import java.util.Set;
 
@@ -53,22 +50,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-public class RunScriptCommandTest {
-
-  private ByteArrayOutputStream outBytes;
-  private PrintStream out;
-
-  private ByteArrayOutputStream errBytes;
-  private PrintStream err;
-
-  @Before
-  public void setUp() {
-    outBytes = new ByteArrayOutputStream();
-    out = new PrintStream(outBytes);
-
-    errBytes = new ByteArrayOutputStream();
-    err = new PrintStream(errBytes);
-  }
+public class RunScriptCommandTest extends BaseCommandTest {
 
   @Test
   public void testScriptPathIsMandatory() throws Exception {

Modified: whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/VersionCommandTest.java
URL: http://svn.apache.org/viewvc/whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/VersionCommandTest.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/VersionCommandTest.java (original)
+++ whirr/trunk/cli/src/test/java/org/apache/whirr/cli/command/VersionCommandTest.java Wed Jan 25 13:22:35 2012
@@ -18,27 +18,16 @@
 
 package org.apache.whirr.cli.command;
 
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.startsWith;
 import static org.junit.Assert.assertThat;
 
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
 import java.util.Collections;
 
-import org.junit.Before;
 import org.junit.Test;
 
-public class VersionCommandTest {
-
-  private ByteArrayOutputStream outBytes;
-  private PrintStream out;
-
-  @Before
-  public void setUp() {
-    outBytes = new ByteArrayOutputStream();
-    out = new PrintStream(outBytes);
-  }
+public class VersionCommandTest extends BaseCommandTest{
 
   @Test
   public void testOverrides() throws Exception {
@@ -46,6 +35,7 @@ public class VersionCommandTest {
     int rc = command.run(null, out, null, Collections.<String>emptyList());
     assertThat(rc, is(0));
     assertThat(outBytes.toString(), startsWith("Apache Whirr "));
+    assertThat(outBytes.toString(), containsString("jclouds"));
   }
   
 }

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=1235735&r1=1235734&r2=1235735&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 Jan 25 13:22:35 2012
@@ -21,14 +21,15 @@ package org.apache.whirr;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import org.apache.whirr.actions.BootstrapClusterAction;
 import org.apache.whirr.actions.CleanupClusterAction;
-import org.apache.whirr.actions.ConfigureClusterAction;
+import org.apache.whirr.actions.ConfigureServicesAction;
 import org.apache.whirr.actions.DestroyClusterAction;
-import org.apache.whirr.actions.StartClusterAction;
-import org.apache.whirr.actions.StopClusterAction;
+import org.apache.whirr.actions.StartServicesAction;
+import org.apache.whirr.actions.StopServicesAction;
 import org.apache.whirr.service.ClusterActionHandler;
 import org.apache.whirr.state.ClusterStateStore;
 import org.apache.whirr.state.ClusterStateStoreFactory;
@@ -62,6 +63,7 @@ public class ClusterController {
   private static final Logger LOG = LoggerFactory.getLogger(ClusterController.class);
 
   private static final Map<String, ClusterActionHandler> HANDLERS = HandlerMapFactory.create();
+  private static final ImmutableSet<String> EMPTYSET = ImmutableSet.of();
 
   private final Function<ClusterSpec, ComputeServiceContext> getCompute;
   private final ClusterStateStoreFactory stateStoreFactory;
@@ -144,7 +146,13 @@ public class ClusterController {
 
   public Cluster configureServices(ClusterSpec clusterSpec, Cluster cluster)
     throws IOException, InterruptedException {
-    ConfigureClusterAction configurer = new ConfigureClusterAction(getCompute(), HANDLERS);
+    return configureServices(clusterSpec, cluster, EMPTYSET, EMPTYSET);
+  }
+  
+  public Cluster configureServices(ClusterSpec clusterSpec, Cluster cluster, Set<String> targetRoles,
+        Set<String> targetInstanceIds) throws IOException, InterruptedException {
+    ConfigureServicesAction configurer = new ConfigureServicesAction(getCompute(), HANDLERS,
+        targetRoles, targetInstanceIds);
     return configurer.execute(clusterSpec, cluster);
   }
 
@@ -157,7 +165,12 @@ public class ClusterController {
 
   public Cluster startServices(ClusterSpec clusterSpec, Cluster cluster)
     throws IOException, InterruptedException {
-    StartClusterAction starter = new StartClusterAction(getCompute(), HANDLERS);
+    return startServices(clusterSpec, cluster, EMPTYSET, EMPTYSET);
+  }
+  
+  public Cluster startServices(ClusterSpec clusterSpec, Cluster cluster,
+      Set<String> targetRoles, Set<String> targetInstanceIds) throws IOException, InterruptedException {
+    StartServicesAction starter = new StartServicesAction(getCompute(), HANDLERS, targetRoles, targetInstanceIds);
     return starter.execute(clusterSpec, cluster);
   }
 
@@ -170,7 +183,13 @@ public class ClusterController {
 
   public Cluster stopServices(ClusterSpec clusterSpec, Cluster cluster)
     throws IOException, InterruptedException {
-    StopClusterAction stopper = new StopClusterAction(getCompute(), HANDLERS);
+    return stopServices(clusterSpec, cluster, EMPTYSET, EMPTYSET);
+
+  }
+  
+  public Cluster stopServices(ClusterSpec clusterSpec, Cluster cluster, Set<String> targetRoles,
+    Set<String> targetInstanceIds) throws IOException, InterruptedException {
+    StopServicesAction stopper = new StopServicesAction(getCompute(), HANDLERS, targetRoles, targetInstanceIds);
     return stopper.execute(clusterSpec, cluster);
   }
 
@@ -186,7 +205,7 @@ public class ClusterController {
     CleanupClusterAction cleanner = new CleanupClusterAction(getCompute(), HANDLERS);
     return cleanner.execute(clusterSpec, cluster);
   }
-
+  
   /**
    * Stop the cluster and destroy all resources associated with it.
    *

Modified: whirr/trunk/core/src/main/java/org/apache/whirr/actions/CleanupClusterAction.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/CleanupClusterAction.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/actions/CleanupClusterAction.java (original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/actions/CleanupClusterAction.java Wed Jan 25 13:22:35 2012
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A {@link ClusterAction} for cleaning-up the cluster services
@@ -35,11 +36,21 @@ public class CleanupClusterAction extend
   private static final Logger LOG = LoggerFactory.getLogger(CleanupClusterAction.class);
 
   public CleanupClusterAction(
-    final Function<ClusterSpec, ComputeServiceContext> getCompute,
-    final Map<String, ClusterActionHandler> handlerMap) {
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap
+  ) {
     super(getCompute, handlerMap);
   }
 
+  public CleanupClusterAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap,
+      Set<String> targetRoles,
+      Set<String> targetInstanceIds
+  ) {
+    super(getCompute, handlerMap, targetRoles, targetInstanceIds);
+  }
+
   @Override
   protected String getAction() {
     return ClusterActionHandler.CLEANUP_ACTION;

Added: whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureServicesAction.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureServicesAction.java?rev=1235735&view=auto
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureServicesAction.java (added)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureServicesAction.java Wed Jan 25 13:22:35 2012
@@ -0,0 +1,99 @@
+/**
+ * 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.actions;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.primitives.Ints;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.InstanceTemplate;
+import org.apache.whirr.RolePredicates;
+import org.apache.whirr.service.ClusterActionEvent;
+import org.apache.whirr.service.ClusterActionHandler;
+import org.apache.whirr.service.FirewallManager.Rule;
+import org.jclouds.compute.ComputeServiceContext;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+
+/**
+ * A {@link org.apache.whirr.ClusterAction} for running a configuration script on instances
+ * in the cluster after it has been bootstrapped.
+ */
+public class ConfigureServicesAction extends ScriptBasedClusterAction {
+
+  public ConfigureServicesAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap
+  ) {
+    super(getCompute, handlerMap);
+  }
+
+  public ConfigureServicesAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap,
+      Set<String> targetRoles,
+      Set<String> targetInstanceIds
+  ) {
+    super(getCompute, handlerMap, targetRoles, targetInstanceIds);
+  }
+
+  @Override
+  protected String getAction() {
+    return ClusterActionHandler.CONFIGURE_ACTION;
+  }
+
+  /**
+   * Apply the firewall rules specified via configuration.
+   */
+  protected void eventSpecificActions(Entry<InstanceTemplate, ClusterActionEvent> entry) 
+      throws IOException {
+    ClusterActionEvent event = entry.getValue();
+    ClusterSpec clusterSpec = event.getClusterSpec();
+    
+    Map<String, List<String>> firewallRules = clusterSpec.getFirewallRules();
+    for (String role: firewallRules.keySet()) {
+      if (!roleIsInTarget(role)) {
+        continue;   // skip execution for this role
+      }
+      Rule rule = Rule.create();
+      
+      if (role == null) {
+        rule.destination(event.getCluster().getInstances());
+      } else {
+        rule.destination(RolePredicates.role(role));
+      }
+      
+      List<String> ports = firewallRules.get(role);
+      rule.ports(Ints.toArray(Collections2.transform(ports, new Function<String,Integer>() {
+        @Override
+        public Integer apply(String input) {
+          return Integer.valueOf(input);
+        }
+      })));
+
+      event.getFirewallManager().addRule(rule);
+    }
+  }
+
+}

Modified: whirr/trunk/core/src/main/java/org/apache/whirr/actions/ScriptBasedClusterAction.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/ScriptBasedClusterAction.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/actions/ScriptBasedClusterAction.java (original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/actions/ScriptBasedClusterAction.java Wed Jan 25 13:22:35 2012
@@ -18,23 +18,14 @@
 
 package org.apache.whirr.actions;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.whirr.RolePredicates.onlyRolesIn;
-import static org.jclouds.compute.options.RunScriptOptions.Builder.overrideCredentialsWith;
-
-import java.io.IOException;
-import java.util.Collection;
-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 javax.annotation.Nullable;
-
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ComputationException;
+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 org.apache.whirr.Cluster;
 import org.apache.whirr.Cluster.Instance;
 import org.apache.whirr.ClusterAction;
@@ -53,12 +44,21 @@ import org.jclouds.scriptbuilder.domain.
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
-import com.google.common.collect.ComputationException;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.Collection;
+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 static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.whirr.RolePredicates.onlyRolesIn;
+import static org.jclouds.compute.options.RunScriptOptions.Builder.overrideCredentialsWith;
 
 /**
  * A {@link ClusterAction} that provides the base functionality for running
@@ -70,12 +70,26 @@ public abstract class ScriptBasedCluster
       .getLogger(ScriptBasedClusterAction.class);
 
   private final Map<String, ClusterActionHandler> handlerMap;
+  private final ImmutableSet<String> targetRoles;
+  private final ImmutableSet<String> targetInstanceIds;
+
+  protected ScriptBasedClusterAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap
+  ) {
+    this(getCompute, handlerMap, ImmutableSet.<String>of(), ImmutableSet.<String>of());
+  }
 
   protected ScriptBasedClusterAction(
       Function<ClusterSpec, ComputeServiceContext> getCompute,
-      final Map<String, ClusterActionHandler> handlerMap) {
+      Map<String, ClusterActionHandler> handlerMap,
+      Set<String> targetRoles,
+      Set<String> targetInstanceIds
+  ) {
     super(getCompute);
     this.handlerMap = checkNotNull(handlerMap, "handlerMap");
+    this.targetRoles = ImmutableSet.copyOf(checkNotNull(targetRoles, "targetRoles"));
+    this.targetInstanceIds = ImmutableSet.copyOf(checkNotNull(targetInstanceIds, "targetInstanceIds"));
   }
 
   public Cluster execute(ClusterSpec clusterSpec, Cluster cluster)
@@ -84,20 +98,23 @@ public abstract class ScriptBasedCluster
     Map<InstanceTemplate, ClusterActionEvent> eventMap = Maps.newHashMap();
     Cluster newCluster = cluster;
     for (InstanceTemplate instanceTemplate : clusterSpec.getInstanceTemplates()) {
+      if (shouldIgnoreInstanceTemplate(instanceTemplate)) {
+        continue; // skip execution if this group of instances is not in target
+      }
       StatementBuilder statementBuilder = new StatementBuilder();
 
-      ComputeServiceContext computeServiceContext = getCompute().apply(
-          clusterSpec);
+      ComputeServiceContext computeServiceContext = getCompute().apply(clusterSpec);
       FirewallManager firewallManager = new FirewallManager(
           computeServiceContext, clusterSpec, newCluster);
 
-      ClusterActionEvent event = new ClusterActionEvent(getAction(),
-          clusterSpec, instanceTemplate, newCluster, statementBuilder,
-          getCompute(), firewallManager);
+      ClusterActionEvent event = new ClusterActionEvent(getAction(), clusterSpec,
+          instanceTemplate, newCluster, statementBuilder, getCompute(), firewallManager);
 
       eventMap.put(instanceTemplate, event);
       for (String role : instanceTemplate.getRoles()) {
-        safeGetActionHandler(role).beforeAction(event);
+        if (roleIsInTarget(role)) {
+          safeGetActionHandler(role).beforeAction(event);
+        }
       }
 
       // cluster may have been updated by handler
@@ -110,13 +127,18 @@ public abstract class ScriptBasedCluster
     newCluster = Iterables.get(eventMap.values(), 0).getCluster();
 
     for (InstanceTemplate instanceTemplate : clusterSpec.getInstanceTemplates()) {
+      if (shouldIgnoreInstanceTemplate(instanceTemplate)) {
+        continue;
+      }
       ClusterActionEvent event = eventMap.get(instanceTemplate);
       for (String role : instanceTemplate.getRoles()) {
-        event.setCluster(newCluster);
-        safeGetActionHandler(role).afterAction(event);
+        if (roleIsInTarget(role)) {
+          event.setCluster(newCluster);
+          safeGetActionHandler(role).afterAction(event);
 
-        // cluster may have been updated by handler
-        newCluster = event.getCluster();
+          // cluster may have been updated by handler
+          newCluster = event.getCluster();
+        }
       }
     }
 
@@ -136,56 +158,44 @@ public abstract class ScriptBasedCluster
     final ExecutorService executorService = Executors.newCachedThreadPool();
     final Collection<Future<ExecResponse>> futures = Sets.newHashSet();
 
-    ClusterSpec clusterSpec = eventMap.values().iterator().next()
-        .getClusterSpec();
-
-    ComputeServiceContext computeServiceContext = getCompute().apply(
-        clusterSpec);
-    final ComputeService computeService = computeServiceContext
-        .getComputeService();
+    final ClusterSpec clusterSpec = eventMap.values().iterator().next().getClusterSpec();
+    final ComputeServiceContext computeServiceContext = getCompute().apply(clusterSpec);
+    final ComputeService computeService = computeServiceContext.getComputeService();
 
     final Credentials credentials = new Credentials(
         clusterSpec.getClusterUser(), clusterSpec.getPrivateKey());
 
-    for (Entry<InstanceTemplate, ClusterActionEvent> entry : eventMap
-        .entrySet()) {
+    for (Entry<InstanceTemplate, ClusterActionEvent> entry : eventMap.entrySet()) {
+      if (shouldIgnoreInstanceTemplate(entry.getKey())) {
+        continue; // skip if not in the target
+      }
 
       eventSpecificActions(entry);
 
       Cluster cluster = entry.getValue().getCluster();
-
-      StatementBuilder statementBuilder = entry.getValue()
-          .getStatementBuilder();
+      StatementBuilder statementBuilder = entry.getValue().getStatementBuilder();
       if (statementBuilder.isEmpty()) {
         continue; // skip execution if we have an empty list
       }
 
-      Set<Instance> instances = cluster.getInstancesMatching(onlyRolesIn(entry
-          .getKey().getRoles()));
-
-      String instanceIds = Joiner.on(", ").join(
-          Iterables.transform(instances, new Function<Instance, String>() {
-            @Override
-            public String apply(@Nullable Instance instance) {
-              return instance == null ? "<null>" : instance.getId();
-            }
-          }));
-
-      LOG.info("Starting to run scripts on cluster for phase {}"
-          + "instances: {}", phaseName, instanceIds);
+      Set<Instance> instances = cluster.getInstancesMatching(onlyRolesIn(entry.getKey().getRoles()));
+      LOG.info("Starting to run scripts on cluster for phase {} "
+          + "on instances: {}", phaseName, asString(instances));
 
       for (final Instance instance : instances) {
+        if (instanceIsNotInTarget(instance)) {
+          continue; // skip the script execution
+        }
         final Statement statement = statementBuilder.build(clusterSpec, instance);
 
         futures.add(executorService.submit(new Callable<ExecResponse>() {
           @Override
           public ExecResponse call() {
 
-            LOG.info("Running {} phase script on: {}", phaseName,
-                instance.getId());
+            LOG.info("Running {} phase script on: {}", phaseName, instance.getId());
             if (LOG.isDebugEnabled()) {
-              LOG.debug("{} phase script on: {}\n{}", new Object[] { phaseName,
-                  instance.getId(), statement.render(OsFamily.UNIX) });
+              LOG.debug("{} phase script on: {}\n{}", new Object[]{phaseName,
+                  instance.getId(), statement.render(OsFamily.UNIX)});
             }
 
             try {
@@ -223,6 +233,43 @@ public abstract class ScriptBasedCluster
         phaseName);
   }
 
+  private String asString(Set<Instance> instances) {
+    return Joiner.on(", ").join(
+        Iterables.transform(instances, new Function<Instance, String>() {
+          @Override
+          public String apply(@Nullable Instance instance) {
+            return instance == null ? "<null>" : instance.getId();
+          }
+        }));
+  }
+
+  protected boolean shouldIgnoreInstanceTemplate(InstanceTemplate template) {
+    return targetRoles.size() != 0 && containsNoneOf(template.getRoles(), targetRoles);
+  }
+
+  protected boolean roleIsInTarget(String role) {
+    return targetRoles.size() == 0 || targetRoles.contains(role);
+  }
+
+  protected boolean instanceIsNotInTarget(Instance instance) {
+    if (targetInstanceIds.size() != 0) {
+      return ! targetInstanceIds.contains(instance.getId());
+    }
+    if (targetRoles.size() != 0) {
+      return containsNoneOf(instance.getRoles(), targetRoles);
+    }
+    return false;
+  }
+
+  private boolean containsNoneOf(Set<String> querySet, final Set<String> target) {
+    return !Iterables.any(querySet, new Predicate<String>() {
+      @Override
+      public boolean apply(@Nullable String role) {
+        return target.contains(role);
+      }
+    });
+  }
+
   protected void eventSpecificActions(
       Entry<InstanceTemplate, ClusterActionEvent> entry) throws IOException {
   }

Added: whirr/trunk/core/src/main/java/org/apache/whirr/actions/StartServicesAction.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/StartServicesAction.java?rev=1235735&view=auto
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/actions/StartServicesAction.java (added)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/actions/StartServicesAction.java Wed Jan 25 13:22:35 2012
@@ -0,0 +1,58 @@
+/**
+ * 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.actions;
+
+import com.google.common.base.Function;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.service.ClusterActionHandler;
+import org.jclouds.compute.ComputeServiceContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link ClusterAction} for starting the cluster services
+ */
+public class StartServicesAction extends ScriptBasedClusterAction {
+
+  private static final Logger LOG = LoggerFactory.getLogger(StartServicesAction.class);
+
+  public StartServicesAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap
+  ) {
+    super(getCompute, handlerMap);
+  }
+
+  public StartServicesAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap,
+      Set<String> targetRoles,
+      Set<String> targetInstanceIds
+  ) {
+    super(getCompute, handlerMap, targetRoles, targetInstanceIds);
+  }
+
+  @Override
+  protected String getAction() {
+    return ClusterActionHandler.START_ACTION;
+  }
+}

Added: whirr/trunk/core/src/main/java/org/apache/whirr/actions/StopServicesAction.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/StopServicesAction.java?rev=1235735&view=auto
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/actions/StopServicesAction.java (added)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/actions/StopServicesAction.java Wed Jan 25 13:22:35 2012
@@ -0,0 +1,58 @@
+/**
+ * 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.actions;
+
+import com.google.common.base.Function;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.service.ClusterActionHandler;
+import org.jclouds.compute.ComputeServiceContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link ClusterAction} for stopping the cluster services
+ */
+public class StopServicesAction extends ScriptBasedClusterAction {
+
+  private static final Logger LOG = LoggerFactory.getLogger(StopServicesAction.class);
+
+  public StopServicesAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap
+  ) {
+    super(getCompute, handlerMap);
+  }
+
+  public StopServicesAction(
+      Function<ClusterSpec, ComputeServiceContext> getCompute,
+      Map<String, ClusterActionHandler> handlerMap,
+      Set<String> targetRoles,
+      Set<String> targetInstanceIds
+  ) {
+    super(getCompute, handlerMap, targetRoles, targetInstanceIds);
+  }
+
+  @Override
+  protected String getAction() {
+    return ClusterActionHandler.STOP_ACTION;
+  }
+}

Modified: whirr/trunk/core/src/main/java/org/apache/whirr/command/AbstractClusterCommand.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/command/AbstractClusterCommand.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/command/AbstractClusterCommand.java (original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/command/AbstractClusterCommand.java Wed Jan 25 13:22:35 2012
@@ -24,6 +24,8 @@ import static org.apache.whirr.ClusterSp
 
 import com.google.common.collect.Maps;
 
+import java.io.IOException;
+import java.io.PrintStream;
 import java.util.EnumSet;
 import java.util.Map;
 
@@ -36,6 +38,7 @@ import org.apache.commons.configuration.
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.configuration.ConfigurationException;
 import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.whirr.Cluster;
 import org.apache.whirr.ClusterController;
 import org.apache.whirr.ClusterControllerFactory;
 import org.apache.whirr.ClusterSpec;
@@ -65,14 +68,12 @@ public abstract class AbstractClusterCom
     .describedAs("config.properties")
     .ofType(String.class);
 
-  public AbstractClusterCommand(String name, String description,
-                                ClusterControllerFactory factory) {
+  public AbstractClusterCommand(String name, String description, ClusterControllerFactory factory) {
     this(name, description, factory, new ClusterStateStoreFactory());
   }
 
-  public AbstractClusterCommand(String name, String description,
-                                ClusterControllerFactory factory,
-                                ClusterStateStoreFactory stateStoreFactory) {
+  public AbstractClusterCommand(String name, String description, ClusterControllerFactory factory,
+        ClusterStateStoreFactory stateStoreFactory) {
     super(name, description);
 
     this.factory = factory;
@@ -91,6 +92,9 @@ public abstract class AbstractClusterCom
     }
   }
 
+  /**
+   * Load the cluster spec by parsing the command line option set
+   */
   protected ClusterSpec getClusterSpec(OptionSet optionSet) throws ConfigurationException {
     Configuration optionsConfig = new PropertiesConfiguration();
     for (Map.Entry<Property, OptionSpec<?>> entry : optionSpecs.entrySet()) {
@@ -121,6 +125,15 @@ public abstract class AbstractClusterCom
   }
 
   /**
+   * Get the cluster instance together with NodeMetadata (through API calls)
+   */
+  protected Cluster getCluster(ClusterSpec clusterSpec, ClusterController controller)
+      throws IOException, InterruptedException {
+    return new Cluster(controller.getInstances(
+        clusterSpec, createClusterStateStore(clusterSpec)));
+  }
+
+  /**
    * Create the specified service
    */
   protected ClusterController createClusterController(String serviceName) {
@@ -132,8 +145,28 @@ public abstract class AbstractClusterCom
     return controller;
   }
 
+  /**
+   * Create the cluster state store object
+   */
   protected ClusterStateStore createClusterStateStore(ClusterSpec spec) {
     return stateStoreFactory.create(spec);
   }
 
+  /**
+   * Print command execution error and a hint to help the user get more help
+   */
+  protected void printErrorAndHelpHint(PrintStream stream, Throwable e) {
+    stream.println(e.getMessage());
+    stream.println("Help: whirr help " + getName());
+  }
+
+  /**
+   * Print a generic usage indication for commands
+   */
+  @Override
+  public void printUsage(PrintStream stream) throws IOException {
+    stream.println("Usage: whirr " + getName() + " [OPTIONS]");
+    stream.println();
+    parser.printHelpOn(stream);
+  }
 }

Modified: whirr/trunk/core/src/main/java/org/apache/whirr/command/Command.java
URL: http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/command/Command.java?rev=1235735&r1=1235734&r2=1235735&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/command/Command.java (original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/command/Command.java Wed Jan 25 13:22:35 2012
@@ -18,6 +18,7 @@
 
 package org.apache.whirr.command;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.List;
@@ -46,4 +47,5 @@ public abstract class Command {
   public abstract int run(InputStream in, PrintStream out, PrintStream err,
       List<String> args) throws Exception;
 
+  public abstract void printUsage(PrintStream stream) throws IOException;
 }