Author: asavu
Date: Wed Oct 19 12:42:00 2011
New Revision: 1186173

URL: http://svn.apache.org/viewvc?rev=1186173&view=rev
Log:
WHIRR-243. Allow to run component tests in memory (David Alves, Adrian Cole via 
asavu)

Added:
    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
    
whirr/trunk/core/src/main/java/org/apache/whirr/actions/BootstrapClusterAction.java
    
whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureClusterAction.java
    whirr/trunk/core/src/main/java/org/apache/whirr/service/ComputeCache.java

Modified: whirr/trunk/CHANGES.txt
URL: 
http://svn.apache.org/viewvc/whirr/trunk/CHANGES.txt?rev=1186173&r1=1186172&r2=1186173&view=diff
==============================================================================
--- whirr/trunk/CHANGES.txt (original)
+++ whirr/trunk/CHANGES.txt Wed Oct 19 12:42:00 2011
@@ -37,6 +37,9 @@ Trunk (unreleased changes)
     WHIRR-214. First iteration on refactoring the core to support the 
     addition of nodes to running clusters (asavu)
 
+    WHIRR-243. Allow to run component tests in memory 
+    (David Alves, Adrian Cole via asavu)
+
   BUG FIXES
 
     WHIRR-377. Fix broken CLI logging config. (asavu via tomwhite)

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=1186173&r1=1186172&r2=1186173&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 19 12:42:00 2011
@@ -19,6 +19,7 @@
 package org.apache.whirr.actions;
 
 import com.google.common.base.Function;
+import com.google.common.base.Joiner;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
@@ -98,7 +99,12 @@ public class BootstrapClusterAction exte
 
       final Template template = BootstrapTemplate.build(clusterSpec, 
computeService,
         statementBuilder, entry.getValue().getTemplateBuilderStrategy());
-      
+
+      if (template.getOptions() != null) {
+        template.getOptions()
+          .nameTask("bootstrap-" + 
Joiner.on('_').join(entry.getKey().getRoles()));
+      }
+
       Future<Set<? extends NodeMetadata>> nodesFuture = executorService.submit(
           new StartupProcess(
               clusterSpec.getClusterName(),

Modified: 
whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureClusterAction.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureClusterAction.java?rev=1186173&r1=1186172&r2=1186173&view=diff
==============================================================================
--- 
whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureClusterAction.java
 (original)
+++ 
whirr/trunk/core/src/main/java/org/apache/whirr/actions/ConfigureClusterAction.java
 Wed Oct 19 12:42:00 2011
@@ -58,6 +58,7 @@ import java.util.concurrent.Future;
 import static org.apache.whirr.RolePredicates.onlyRolesIn;
 import static 
org.jclouds.compute.options.RunScriptOptions.Builder.overrideCredentialsWith;
 
+
 /**
  * A {@link org.apache.whirr.ClusterAction} for running a configuration script 
on instances
  * in the cluster after it has been bootstrapped.
@@ -133,6 +134,7 @@ public class ConfigureClusterAction exte
                 instance.getId(),
                 statement,
                 overrideCredentialsWith(credentials).runAsRoot(true)
+                .nameTask("configure-" + 
Joiner.on('_').join(instance.getRoles()))
               );
 
             } finally {

Modified: 
whirr/trunk/core/src/main/java/org/apache/whirr/service/ComputeCache.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/service/ComputeCache.java?rev=1186173&r1=1186172&r2=1186173&view=diff
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/service/ComputeCache.java 
(original)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/service/ComputeCache.java 
Wed Oct 19 12:42:00 2011
@@ -21,6 +21,7 @@ package org.apache.whirr.service;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Objects;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ForwardingObject;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -71,13 +72,27 @@ public enum ComputeCache implements Func
   final Map<Key, ComputeServiceContext> cache = new 
MapMaker().makeComputingMap(
       new Function<Key, ComputeServiceContext>(){
         private final ComputeServiceContextFactory factory =  new 
ComputeServiceContextFactory();
-        private final Set<AbstractModule> wiring = ImmutableSet.of(
-              new SshjSshClientModule(),
-              new SLF4JLoggingModule(), 
-              new EnterpriseConfigurationModule(),
-              new BindLoginCredentialsPatchForEC2());        
+        private Set<AbstractModule> wiring;
+               
         @Override
         public ComputeServiceContext apply(Key arg0) {
+          if (wiring == null) {
+            if (arg0.provider.equals("stub")) {
+              try {
+                wiring = ImmutableSet.of(
+                    new SLF4JLoggingModule(),
+                    (AbstractModule) 
Class.forName("org.apache.whirr.service.DryRunModule").newInstance());
+              } catch (Exception e) {
+                Throwables.propagate(e);
+              }
+            } else {
+              wiring = ImmutableSet.of(
+                  new SshjSshClientModule(),
+                  new SLF4JLoggingModule(), 
+                  new EnterpriseConfigurationModule(),
+                  new BindLoginCredentialsPatchForEC2());  
+            }
+          }
           LOG.debug("creating new ComputeServiceContext {}", arg0);
           ComputeServiceContext context = new 
IgnoreCloseComputeServiceContext(factory.createContext(
             arg0.provider, arg0.identity, arg0.credential,

Added: whirr/trunk/core/src/main/java/org/apache/whirr/service/DryRunModule.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/main/java/org/apache/whirr/service/DryRunModule.java?rev=1186173&view=auto
==============================================================================
--- whirr/trunk/core/src/main/java/org/apache/whirr/service/DryRunModule.java 
(added)
+++ whirr/trunk/core/src/main/java/org/apache/whirr/service/DryRunModule.java 
Wed Oct 19 12:42:00 2011
@@ -0,0 +1,353 @@
+/**
+ * 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.service;
+
+import static com.google.common.collect.Iterables.concat;
+import static com.google.common.collect.Iterables.contains;
+import static com.google.common.collect.Iterables.find;
+import static com.google.common.collect.Multimaps.synchronizedListMultimap;
+import static com.google.common.io.ByteStreams.newInputStreamSupplier;
+import static com.google.inject.matcher.Matchers.identicalTo;
+import static com.google.inject.matcher.Matchers.returns;
+import static com.google.inject.matcher.Matchers.subclassesOf;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.jclouds.compute.callables.RunScriptOnNode;
+import org.jclouds.compute.domain.ExecResponse;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.crypto.CryptoStreams;
+import org.jclouds.domain.Credentials;
+import org.jclouds.io.Payload;
+import org.jclouds.io.payloads.StringPayload;
+import org.jclouds.net.IPSocket;
+import org.jclouds.ssh.SshClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+import com.google.common.io.InputSupplier;
+import com.google.inject.AbstractModule;
+
+/**
+ * Outputs orchestration jclouds does to INFO logging and saves an ordered list
+ * of all scripts executed on the cluster that can be used to make assertions.
+ * Use in tests by setting whirr.provider to "stub" and make sure you do not
+ * specify a hardware, image, or location id.
+ * 
+ */
+// note that most of this logic will be pulled into jclouds 1.0-beta-10 per
+// http://code.google.com/p/jclouds/issues/detail?id=490
+public class DryRunModule extends AbstractModule {
+    private static final Logger LOG = LoggerFactory
+            .getLogger(DryRunModule.class);
+
+    // an example showing how to intercept any internal method for logging
+    // purposes
+    public class LogCallToRunScriptOnNode implements MethodInterceptor {
+
+        public Object invoke(MethodInvocation i) throws Throwable {
+            if (i.getMethod().getName().equals("call")) {
+                RunScriptOnNode runScriptOnNode = RunScriptOnNode.class.cast(i
+                        .getThis());
+                String nodeName = runScriptOnNode.getNode().getName();
+                LOG.info(nodeName + " >> running script");
+                Object returnVal = i.proceed();
+                LOG.info(nodeName + " << " + returnVal);
+                return returnVal;
+            } else {
+                return i.proceed();
+            }
+        }
+    }
+
+    public static synchronized DryRun getDryRun() {
+        return DryRun.INSTANCE;
+    }
+
+    public static void resetDryRun() {
+        DryRun.INSTANCE.executedScripts.clear();
+    }
+
+    // enum singleton pattern
+    public static enum DryRun {
+        INSTANCE;
+        // allow duplicate mappings and use deterministic ordering
+        private final ListMultimap<NodeMetadata, RunScriptOnNode> 
executedScripts = synchronizedListMultimap(LinkedListMultimap
+                .<NodeMetadata, RunScriptOnNode> create());
+
+        DryRun() {
+        }
+
+        void newExecution(RunScriptOnNode runScript) {
+            executedScripts.put(runScript.getNode(), runScript);
+        }
+
+        public synchronized ListMultimap<NodeMetadata, RunScriptOnNode> 
getExecutions() {
+            return ImmutableListMultimap.copyOf(executedScripts);
+        }
+
+    }
+
+    public class SaveDryRunsByInterceptingRunScriptOnNodeCreation implements
+            MethodInterceptor {
+
+        public Object invoke(MethodInvocation i) throws Throwable {
+            if (i.getMethod().getName().equals("create")) {
+                Object returnVal = i.proceed();
+                
getDryRun().newExecution(RunScriptOnNode.class.cast(returnVal));
+                return returnVal;
+            } else {
+                return i.proceed();
+            }
+        }
+    }
+
+    @Override
+    protected void configure() {
+        bind(SshClient.Factory.class).to(LogSshClient.Factory.class);
+        bindInterceptor(subclassesOf(RunScriptOnNode.class),
+                returns(identicalTo(ExecResponse.class)),
+                new LogCallToRunScriptOnNode());
+        bindInterceptor(subclassesOf(RunScriptOnNode.Factory.class),
+                returns(identicalTo(RunScriptOnNode.class)),
+                new SaveDryRunsByInterceptingRunScriptOnNodeCreation());
+    }
+
+    private static class Key {
+        private final IPSocket socket;
+        private final Credentials creds;
+        private final NodeMetadata node;
+
+        Key(IPSocket socket, Credentials creds, @Nullable NodeMetadata node) {
+            this.socket = socket;
+            this.creds = creds;
+            this.node = node;
+        }
+
+        // only the user, not password should be used to identify this
+        // connection
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(socket, creds.identity);
+        }
+
+        @Override
+        public boolean equals(Object that) {
+            if (that == null)
+                return false;
+            return Objects.equal(this.toString(), that.toString());
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%s#%s@%s:%d", node.getName(), creds.identity,
+                    socket.getAddress(), socket.getPort());
+        }
+    }
+
+    @Singleton
+    private static class LogSshClient implements SshClient {
+        private final Key key;
+
+        public LogSshClient(Key key) {
+            this.key = key;
+        }
+
+        private static class NodeHasAddress implements Predicate<NodeMetadata> 
{
+            private final String address;
+
+            private NodeHasAddress(String address) {
+                this.address = address;
+            }
+
+            @Override
+            public boolean apply(NodeMetadata arg0) {
+                return contains(
+                        concat(arg0.getPrivateAddresses(),
+                                arg0.getPublicAddresses()), address);
+            }
+        }
+
+        @Singleton
+        public static class Factory implements SshClient.Factory {
+
+            // this will ensure only one state per ip socket/user
+            private final Map<Key, SshClient> clientMap;
+            // easy access to node metadata
+            private final ConcurrentMap<String, NodeMetadata> nodes;
+
+            @SuppressWarnings("unused")
+            @Inject
+            public Factory(final ConcurrentMap<String, NodeMetadata> nodes) {
+                this.clientMap = new MapMaker()
+                        .makeComputingMap(new Function<Key, SshClient>() {
+
+                            @Override
+                            public SshClient apply(Key key) {
+                                return new LogSshClient(key);
+                            }
+
+                        });
+                this.nodes = nodes;
+            }
+
+            @Override
+            public SshClient create(final IPSocket socket,
+                    Credentials loginCreds) {
+                return clientMap.get(new Key(socket, loginCreds,
+                        find(nodes.values(),
+                                new NodeHasAddress(socket.getAddress()))));
+            }
+
+            @Override
+            public SshClient create(IPSocket socket, String username,
+                    String password) {
+                return create(socket, new Credentials(username, password));
+            }
+
+            @Override
+            public SshClient create(IPSocket socket, String username,
+                    byte[] privateKey) {
+                return create(socket, new Credentials(username, new String(
+                        privateKey)));
+            }
+        }
+
+        private final Map<String, Payload> contents = Maps.newConcurrentMap();
+
+        @Override
+        public void connect() {
+            LOG.info(toString() + " >> connect()");
+        }
+
+        @Override
+        public void disconnect() {
+            LOG.info(toString() + " >> disconnect()");
+        }
+
+        public ThreadLocal<AtomicInteger> delay = new 
ThreadLocal<AtomicInteger>();
+        public static int callDelay = 5;
+
+        @Override
+        public ExecResponse exec(String script) {
+            LOG.info(toString() + " >> exec(" + script + ")");
+            // jclouds checks the status code, but only when seeing if a job
+            // completed. to emulate real scripts all calls are delayed by
+            // forcing
+            // jclouds to call status multiple times (5 by default) before
+            // returning exitCode 1.
+            if (delay.get() == null) {
+                delay.set(new AtomicInteger(0));
+            }
+            ExecResponse exec;
+            if (script.endsWith(" status")) {
+                if (delay.get().get() >= callDelay) {
+                    exec = new ExecResponse("", "", 1);
+                } else {
+                    exec = new ExecResponse("", "", 0);
+                }
+            } else {
+                exec = new ExecResponse("", "", 0);
+            }
+
+            LOG.info(toString() + " << " + exec);
+
+            delay.get().getAndIncrement();
+            return exec;
+        }
+
+        @Override
+        public Payload get(String path) {
+            LOG.info(toString() + " >> get(" + path + ")");
+            Payload returnVal = contents.get(path);
+            LOG.info(toString() + " << md5[" + md5Hex(returnVal) + "]");
+            return returnVal;
+        }
+
+        @Override
+        public String getHostAddress() {
+            return key.socket.getAddress();
+        }
+
+        @Override
+        public String getUsername() {
+            return key.creds.identity;
+        }
+
+        @Override
+        public void put(String path, Payload payload) {
+            LOG.info(toString() + " >> put(" + path + ", md5["
+                    + md5Hex(payload) + "])");
+            contents.put(path, payload);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null)
+                return false;
+            return toString().equals(obj.toString());
+        }
+
+        @Override
+        public int hashCode() {
+            return key.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return key.toString();
+        }
+
+        @Override
+        public void put(String path, String text) {
+            put(path, new StringPayload(text));
+        }
+    }
+
+    public static String md5Hex(String in) {
+        return md5Hex(newInputStreamSupplier(in.getBytes()));
+    }
+
+    public static String md5Hex(InputSupplier<? extends InputStream> supplier) 
{
+        try {
+            return CryptoStreams.md5Hex(supplier);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

Added: 
whirr/trunk/core/src/test/java/org/apache/whirr/service/DryRunModuleTest.java
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/test/java/org/apache/whirr/service/DryRunModuleTest.java?rev=1186173&view=auto
==============================================================================
--- 
whirr/trunk/core/src/test/java/org/apache/whirr/service/DryRunModuleTest.java 
(added)
+++ 
whirr/trunk/core/src/test/java/org/apache/whirr/service/DryRunModuleTest.java 
Wed Oct 19 12:42:00 2011
@@ -0,0 +1,110 @@
+/**
+ * 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.service;
+
+import com.google.common.collect.ListMultimap;
+import com.jcraft.jsch.JSchException;
+import org.apache.commons.configuration.CompositeConfiguration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.whirr.ClusterController;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.service.DryRunModule.DryRun;
+import org.jclouds.compute.callables.RunScriptOnNode;
+import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSsh;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.scriptbuilder.InitBuilder;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static junit.framework.Assert.assertFalse;
+
+public class DryRunModuleTest {
+
+    public static class Noop2ClusterActionHandler extends
+            ClusterActionHandlerSupport {
+
+        @Override
+        public String getRole() {
+            return "noop2";
+        }
+
+    }
+
+    public static class Noop3ClusterActionHandler extends
+            ClusterActionHandlerSupport {
+
+        @Override
+        public String getRole() {
+            return "noop3";
+        }
+    }
+
+    /**
+     * Simple test that asserts that a 1 node cluster was launched and 
bootstrap
+     * and configure scripts were executed.
+     * 
+     * @throws ConfigurationException
+     * @throws IOException
+     * @throws JSchException
+     * @throws InterruptedException
+     */
+    @Test
+    public void testNoInitScriptsAfterConfigurationStarted()
+            throws ConfigurationException, JSchException, IOException,
+            InterruptedException {
+
+        CompositeConfiguration config = new CompositeConfiguration();
+        config.setProperty("whirr.provider", "stub");
+        config.setProperty("whirr.cluster-name", "stub-test");
+        config.setProperty("whirr.instance-templates",
+                "10 noop+noop3,10 noop2+noop,10 noop3+noop2");
+
+        ClusterSpec clusterSpec = ClusterSpec.withTemporaryKeys(config);
+        ClusterController controller = new ClusterController();
+
+        controller.launchCluster(clusterSpec);
+
+        DryRun dryRun = DryRunModule.getDryRun();
+        ListMultimap<NodeMetadata, RunScriptOnNode> list = dryRun
+                .getExecutions();
+
+        // this tests the barrier by making sure that once a configure
+        // script is executed no more setup scripts are executed.
+
+        boolean configStarted = false;
+        for (RunScriptOnNode script : list.values()) {
+            if (!configStarted && 
getScriptName(script).startsWith("configure")) {
+                configStarted = true;
+                continue;
+            }
+            if (configStarted) {
+                assertFalse(
+                  "A setup script was executed after the first configure 
script.",
+                  getScriptName(script).startsWith("setup"));
+            }
+        }
+    }
+
+    private String getScriptName(RunScriptOnNode script) {
+        return ((InitBuilder) ((RunScriptOnNodeAsInitScriptUsingSsh) script)
+                .getStatement()).getInstanceName();
+    }
+
+}

Added: 
whirr/trunk/core/src/test/resources/META-INF/services/org.apache.whirr.service.ClusterActionHandler
URL: 
http://svn.apache.org/viewvc/whirr/trunk/core/src/test/resources/META-INF/services/org.apache.whirr.service.ClusterActionHandler?rev=1186173&view=auto
==============================================================================
--- 
whirr/trunk/core/src/test/resources/META-INF/services/org.apache.whirr.service.ClusterActionHandler
 (added)
+++ 
whirr/trunk/core/src/test/resources/META-INF/services/org.apache.whirr.service.ClusterActionHandler
 Wed Oct 19 12:42:00 2011
@@ -0,0 +1,14 @@
+#   Licensed 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.
+org.apache.whirr.service.NoopClusterActionHandler
+org.apache.whirr.service.DryRunModuleTest$Noop2ClusterActionHandler
+org.apache.whirr.service.DryRunModuleTest$Noop3ClusterActionHandler
\ No newline at end of file


Reply via email to