You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2017/09/19 15:07:30 UTC

[4/5] brooklyn-server git commit: Add/test VanillaSoftwareProcess ‘sshMonitoring.enabled’

Add/test VanillaSoftwareProcess ‘sshMonitoring.enabled’


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/509747f9
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/509747f9
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/509747f9

Branch: refs/heads/master
Commit: 509747f95fa10a319549b9e2065f86119c0324cc
Parents: d10282c
Author: Aled Sage <al...@gmail.com>
Authored: Fri Sep 15 09:29:28 2017 +0100
Committer: Aled Sage <al...@gmail.com>
Committed: Fri Sep 15 18:58:57 2017 +0100

----------------------------------------------------------------------
 .../camp/brooklyn/AbstractYamlTest.java         |  31 +-
 .../VanillaSoftwareProcessYamlTest.java         | 299 +++++++++++++++++++
 .../entity/RecordingSensorEventListener.java    |  12 +
 .../util/core/internal/ssh/ExecCmdAsserts.java  |  12 +-
 .../software/base/VanillaSoftwareProcess.java   |   6 +
 .../base/VanillaSoftwareProcessImpl.java        |  16 +-
 6 files changed, 366 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/509747f9/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
index 6c269d2..3d4bbd2 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
@@ -27,6 +27,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.zip.ZipEntry;
 
+import javax.annotation.Nullable;
+
 import org.apache.brooklyn.api.catalog.BrooklynCatalog;
 import org.apache.brooklyn.api.entity.Application;
 import org.apache.brooklyn.api.entity.Entity;
@@ -58,6 +60,7 @@ import org.apache.brooklyn.util.exceptions.ReferenceWithError;
 import org.apache.brooklyn.util.net.Urls;
 import org.apache.brooklyn.util.osgi.VersionedName;
 import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.time.Duration;
 import org.osgi.framework.Constants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -138,10 +141,15 @@ public abstract class AbstractYamlTest {
     }
 
     protected void waitForApplicationTasks(Entity app) {
+        waitForApplicationTasks(app, null);
+    }
+    
+    protected void waitForApplicationTasks(Entity app, @Nullable Duration timeout) {
         Set<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(brooklynMgmt.getExecutionManager(), app);
         getLogger().info("Waiting on " + tasks.size() + " task(s)");
         for (Task<?> t : tasks) {
-            t.blockUntilEnded();
+            boolean done = t.blockUntilEnded(timeout);
+            if (!done) throw new RuntimeException("Timeout waiting for task to complete: " + t);
         }
     }
 
@@ -174,10 +182,7 @@ public abstract class AbstractYamlTest {
         return createAndStartApplication(input, MutableMap.<String,String>of());
     }
     protected Entity createAndStartApplication(String input, Map<String,?> startParameters) throws Exception {
-        EntitySpec<?> spec = 
-            mgmt().getTypeRegistry().createSpecFromPlan(CampTypePlanTransformer.FORMAT, input, RegisteredTypeLoadingContexts.spec(Application.class), EntitySpec.class);
-        final Entity app = brooklynMgmt.getEntityManager().createEntity(spec);
-        // start the app (happens automatically if we use camp to instantiate, but not if we use crate spec approach)
+        final Entity app = createApplicationUnstarted(input);
         app.invoke(Startable.START, startParameters).get();
         return app;
     }
@@ -191,12 +196,22 @@ public abstract class AbstractYamlTest {
     }
     
     protected Entity createAndStartApplicationAsync(String yaml, Map<String,?> startParameters) throws Exception {
+        final Entity app = createApplicationUnstarted(yaml);
+        // Not calling .get() on task, so this is non-blocking.
+        app.invoke(Startable.START, startParameters);
+        return app;
+    }
+
+    protected Entity createApplicationUnstarted(String... multiLineYaml) throws Exception {
+        return createApplicationUnstarted(joinLines(multiLineYaml));
+    }
+    
+    protected Entity createApplicationUnstarted(String yaml) throws Exception {
+        // not starting the app (would have happened automatically if we use camp to instantiate, 
+        // but not if we use create spec approach).
         EntitySpec<?> spec = 
             mgmt().getTypeRegistry().createSpecFromPlan(CampTypePlanTransformer.FORMAT, yaml, RegisteredTypeLoadingContexts.spec(Application.class), EntitySpec.class);
         final Entity app = brooklynMgmt.getEntityManager().createEntity(spec);
-        // start the app (happens automatically if we use camp to instantiate, but not if we use create spec approach).
-        // Note calling .get() on task, so this is non-blocking.
-        app.invoke(Startable.START, startParameters);
         return app;
     }
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/509747f9/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/VanillaSoftwareProcessYamlTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/VanillaSoftwareProcessYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/VanillaSoftwareProcessYamlTest.java
new file mode 100644
index 0000000..27c941a
--- /dev/null
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/VanillaSoftwareProcessYamlTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.brooklyn.camp.brooklyn;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityAsserts;
+import org.apache.brooklyn.core.entity.RecordingSensorEventListener;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.core.sensor.function.FunctionSensor;
+import org.apache.brooklyn.enricher.stock.UpdatingMap;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.internal.ssh.ExecCmdAsserts;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.CustomResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+@Test
+public class VanillaSoftwareProcessYamlTest extends AbstractYamlTest {
+    private static final Logger log = LoggerFactory.getLogger(VanillaSoftwareProcessYamlTest.class);
+
+    public static class MyCallable implements Callable<Object> {
+        public static AtomicReference<Object> val = new AtomicReference<>();
+        public static AtomicReference<CountDownLatch> latch = new AtomicReference<>();
+
+        public static void clear() {
+            val.set(null);
+            latch.set(null);
+        }
+        @Override public Object call() throws Exception {
+            if (latch.get() != null) latch.get().await();
+            return val.get();
+        }
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MyCallable.clear();
+        RecordingSshTool.clear();
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        MyCallable.clear();
+        RecordingSshTool.clear();
+    }
+    
+    @Test
+    public void testSshPolling() throws Exception {
+        Entity app = createAndStartApplication(
+            "location:",
+            "  localhost:",
+            "    sshToolClass: "+RecordingSshTool.class.getName(),
+            "services:",
+            "- type: "+VanillaSoftwareProcess.class.getName(),
+            "  brooklyn.config:",
+            "    softwareProcess.serviceProcessIsRunningPollPeriod: 10ms",
+            "    checkRunning.command: myCheckRunning",
+            "    launch.command: myLaunch");
+        waitForApplicationTasks(app);
+
+        log.info("App started:");
+        Entities.dumpInfo(app);
+        
+        VanillaSoftwareProcess entity = (VanillaSoftwareProcess) Iterables.getOnlyElement(app.getChildren());
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, true);
+        
+        RecordingSshTool.setCustomResponse(".*myCheckRunning.*", new CustomResponse(1, "simulating not running", ""));
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, false);
+        
+        RecordingSshTool.clear();
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, true);
+    }
+    
+    @Test
+    public void testDisableSshPolling() throws Exception {
+        // driver.isRunning will report failure
+        RecordingSshTool.setCustomResponse(".*myCheckRunning.*", new CustomResponse(1, "simulating not running", ""));
+        
+        Entity app = createApplicationUnstarted(
+            "location:",
+            "  localhost:",
+            "    sshToolClass: "+RecordingSshTool.class.getName(),
+            "services:",
+            "- type: "+VanillaSoftwareProcess.class.getName(),
+            "  brooklyn.config:",
+            "    softwareProcess.serviceProcessIsRunningPollPeriod: 10ms",
+            "    sshMonitoring.enabled: false",
+            "    checkRunning.command: myCheckRunning",
+            "    launch.command: myLaunch");
+        
+        VanillaSoftwareProcess entity = (VanillaSoftwareProcess) Iterables.getOnlyElement(app.getChildren());
+        
+        RecordingSensorEventListener<Object> serviceUpListener = subscribe(entity, Attributes.SERVICE_UP);
+        RecordingSensorEventListener<Object> serviceStateListener = subscribe(entity, Attributes.SERVICE_STATE_ACTUAL);
+
+        Task<Void> task = app.invoke(Startable.START, ImmutableMap.of());
+        
+        // Should eventually poll for 'checkRunning', before reporting 'up'
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                ExecCmdAsserts.assertExecHasAtLeastOnce(RecordingSshTool.getExecCmds(), "myCheckRunning");
+            }});
+        
+        assertFalse(task.isDone());
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, false);
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+
+        // Let startup complete
+        RecordingSshTool.setCustomResponse(".*myCheckRunning.*", new CustomResponse(0, "", ""));
+        waitForApplicationTasks(app);
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, true);
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+
+        // Should never again do ssh-poll of checkRunning
+        RecordingSshTool.clear();
+        Asserts.succeedsContinually(new Runnable() {
+            public void run() {
+                ExecCmdAsserts.assertExecHasNever(RecordingSshTool.getExecCmds(), "myCheckRunning");
+            }});
+        
+        // Should not have transitioned through wrong states (e.g. never "on-fire"!)
+        assertEventsEqualEventually(serviceUpListener, ImmutableList.of(false, true), true);
+        assertEventsEqualEventually(serviceStateListener, ImmutableList.of(Lifecycle.CREATED, Lifecycle.STARTING, Lifecycle.RUNNING), true);
+    }
+   
+    @Test
+    public void testAlternativeServiceUpPolling() throws Exception {
+        AttributeSensor<Boolean> alternativeUpIndicator = Sensors.newBooleanSensor("myAlternativeUpIndicator");
+        MyCallable.latch.set(new CountDownLatch(1));
+        
+        Entity app = createApplicationUnstarted(
+            "location:",
+            "  localhost:",
+            "    sshToolClass: "+RecordingSshTool.class.getName(),
+            "services:",
+            "- type: "+VanillaSoftwareProcess.class.getName(),
+            "  brooklyn.config:",
+            "    softwareProcess.serviceProcessIsRunningPollPeriod: 10ms",
+            "    sshMonitoring.enabled: false",
+            "    checkRunning.command: myCheckRunning",
+            "    launch.command: myLaunch",
+            "  brooklyn.initializers:",
+            "  - type: "+FunctionSensor.class.getName(),
+            "    brooklyn.config:",
+            "      "+FunctionSensor.SENSOR_PERIOD.getName()+": 10ms",
+            "      "+FunctionSensor.SENSOR_NAME.getName()+": " + alternativeUpIndicator.getName(),
+            "      "+FunctionSensor.SENSOR_TYPE.getName()+": boolean",
+            "      "+FunctionSensor.FUNCTION.getName()+":",
+            "        $brooklyn:object:",
+            "          type: "+MyCallable.class.getName(),
+            "  brooklyn.enrichers:",
+            "  - type: " + UpdatingMap.class.getName(),
+            "    brooklyn.config:",
+            "      enricher.sourceSensor: $brooklyn:sensor(\"" + alternativeUpIndicator.getName() + "\")",
+            "      enricher.targetSensor: $brooklyn:sensor(\"service.notUp.indicators\")",
+            "      enricher.updatingMap.computing:",
+            "        $brooklyn:object:",
+            "          type: \"" + Functions.class.getName() + "\"",
+            "          factoryMethod.name: \"forMap\"",
+            "          factoryMethod.args:",
+            "          - false: \"false\"",
+            "            true: null",
+            "          - \"no value\"");
+        
+        VanillaSoftwareProcess entity = (VanillaSoftwareProcess) Iterables.getOnlyElement(app.getChildren());
+        
+        RecordingSensorEventListener<Object> serviceUpListener = subscribe(entity, Attributes.SERVICE_UP);
+        RecordingSensorEventListener<Object> serviceStateListener = subscribe(entity, Attributes.SERVICE_STATE_ACTUAL);
+
+        Task<Void> task = app.invoke(Startable.START, ImmutableMap.of());
+        
+        // Should eventually poll for 'checkRunning', but just once immediately after doing launch etc
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                ExecCmdAsserts.assertExecHasOnlyOnce(RecordingSshTool.getExecCmds(), "myCheckRunning");
+            }});
+        RecordingSshTool.clear();
+
+        assertFalse(task.isDone());
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_NOT_UP_INDICATORS, ImmutableMap.of(alternativeUpIndicator.getName(), "no value"));
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, false);
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+
+        // Let the function return 'false'
+        MyCallable.val.set(false);
+        MyCallable.latch.get().countDown();
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_NOT_UP_INDICATORS, ImmutableMap.of(alternativeUpIndicator.getName(), "false"));
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, false);
+        assertFalse(task.isDone());
+
+        // Let startup complete, by the function returning 'true'
+        MyCallable.val.set(true);
+        waitForApplicationTasks(app, Asserts.DEFAULT_LONG_TIMEOUT);
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, true);
+        EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+
+        // Should not have transitioned through wrong states (e.g. never "on-fire"!)
+        assertEventsEqualEventually(serviceUpListener, ImmutableList.of(false, true), true);
+        assertEventsEqualEventually(serviceStateListener, ImmutableList.of(Lifecycle.CREATED, Lifecycle.STARTING, Lifecycle.RUNNING), true);
+        
+        ExecCmdAsserts.assertExecHasNever(RecordingSshTool.getExecCmds(), "myCheckRunning");
+    }
+   
+    private RecordingSensorEventListener<Object> subscribe(Entity entity, Sensor<?> sensor) {
+        RecordingSensorEventListener<Object> listener = new RecordingSensorEventListener<>();
+        mgmt().getSubscriptionManager().subscribe(MutableMap.of("notifyOfInitialValue", true), entity, sensor, listener);
+        return listener;
+    }
+    
+    private void assertEventsEqualEventually(RecordingSensorEventListener<?> listener, Iterable<?> expected, boolean stripLeadingNulls) {
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                assertIterablesEqual(listener.getEventValues(), (stripLeadingNulls ? leadingNullsStripper() : Functions.identity()), expected);
+            }});
+    }
+
+    private void assertIterablesEqualEventually(Supplier<? extends Iterable<?>> actual, Function<? super List<?>, List<?>> transformer, Iterable<?> expected) {
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                assertIterablesEqual(actual.get(), transformer, expected);
+            }});
+    }
+    
+    private void assertIterablesEqual(Iterable<?> actual, Function<? super List<?>, List<?>> transformer, Iterable<?> expected) {
+        List<?> actualList = (actual instanceof List) ? (List<?>) actual : MutableList.copyOf(actual);
+        List<?> expectedList = (expected instanceof List) ? (List<?>) expected : MutableList.copyOf(expected);
+        String errMsg = "actual="+actualList+"; expected="+expectedList;
+        assertEquals(transformer.apply(actualList), expectedList, errMsg);
+    }
+    
+    private Function<List<?>, List<?>> leadingNullsStripper() {
+        return new Function<List<?>, List<?>>() {
+            @Override public List<?> apply(List<?> input) {
+                if (input == null || input.isEmpty() || input.get(0) != null) {
+                    return input;
+                }
+                List<Object> result = new ArrayList<>();
+                boolean foundNonNull = false;
+                for (Object element : input) {
+                    if (foundNonNull || input != null) {
+                        result.add(element);
+                        foundNonNull = true;
+                    }
+                }
+                return result;
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/509747f9/core/src/test/java/org/apache/brooklyn/core/entity/RecordingSensorEventListener.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/entity/RecordingSensorEventListener.java b/core/src/test/java/org/apache/brooklyn/core/entity/RecordingSensorEventListener.java
index 30d40a2..2c00382 100644
--- a/core/src/test/java/org/apache/brooklyn/core/entity/RecordingSensorEventListener.java
+++ b/core/src/test/java/org/apache/brooklyn/core/entity/RecordingSensorEventListener.java
@@ -35,6 +35,7 @@ import org.testng.Assert;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
@@ -92,6 +93,17 @@ public class RecordingSensorEventListener<T> implements SensorEventListener<T>,
     }
 
     /**
+     * @return A supplier that returns the latest live read-only view of recorded events.
+     */
+    public Supplier<Iterable<T>> getEventValuesSupplier() {
+        return new Supplier<Iterable<T>>() {
+            @Override public Iterable<T> get() {
+                return getEventValues();
+            }
+        };
+    }
+
+    /**
      * @return A static read-only view of event values sorted by the time at which they occurred.
      */
     public Iterable<T> getEventValuesSortedByTimestamp() {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/509747f9/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/ExecCmdAsserts.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/ExecCmdAsserts.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/ExecCmdAsserts.java
index 8aab3b0..db2a191 100644
--- a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/ExecCmdAsserts.java
+++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/ExecCmdAsserts.java
@@ -23,8 +23,10 @@ import java.util.List;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
 
 import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.ExecCmd;
+import org.apache.brooklyn.util.math.MathPredicates;
 
 @Beta
 public class ExecCmdAsserts {
@@ -92,7 +94,15 @@ public class ExecCmdAsserts {
         assertExecHasExactly(actuals, expectedCmd, 1);
     }
 
+    public static void assertExecHasAtLeastOnce(List<ExecCmd> actuals, String expectedCmd) {
+        assertExecHasExactly(actuals, expectedCmd, MathPredicates.greaterThanOrEqual(1));
+    }
+
     public static void assertExecHasExactly(List<ExecCmd> actuals, String expectedCmd, int expectedCount) {
+        assertExecHasExactly(actuals, expectedCmd, Predicates.equalTo(expectedCount));
+    }
+
+    public static void assertExecHasExactly(List<ExecCmd> actuals, String expectedCmd, Predicate<Integer> countChecker) {
         String errMsg = "actuals="+actuals+"; expected="+expectedCmd;
         int count = 0;
         for (ExecCmd actual : actuals) {
@@ -103,7 +113,7 @@ public class ExecCmdAsserts {
                 }
             }
         }
-        assertEquals(count, expectedCount, errMsg);
+        assertTrue(countChecker.apply(count), "actualCount="+count+"; expectedCount="+countChecker+"; "+errMsg);
     }
 
     public static ExecCmd findExecContaining(List<ExecCmd> actuals, String cmdRegex) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/509747f9/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
index a979064..1e41c98 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
@@ -20,6 +20,8 @@ package org.apache.brooklyn.entity.software.base;
 
 import org.apache.brooklyn.api.catalog.Catalog;
 import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
 
 /** 
  * A {@link SoftwareProcess} entity that runs commands from an archive.
@@ -56,4 +58,8 @@ import org.apache.brooklyn.api.entity.ImplementedBy;
 @Catalog(name="Vanilla Software Process", description="A software process configured with scripts, e.g. for launch, check-running and stop")
 @ImplementedBy(VanillaSoftwareProcessImpl.class)
 public interface VanillaSoftwareProcess extends AbstractVanillaProcess {
+    ConfigKey<Boolean> USE_SSH_MONITORING = ConfigKeys.newConfigKey(
+            "sshMonitoring.enabled", 
+            "SSH monitoring enabled", 
+            Boolean.TRUE);
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/509747f9/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
index 6f1aec0..5fa7b66 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
@@ -18,20 +18,34 @@
  */
 package org.apache.brooklyn.entity.software.base;
 
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic;
 
 public class VanillaSoftwareProcessImpl extends SoftwareProcessImpl implements VanillaSoftwareProcess {
+    
     @Override
     public Class<?> getDriverInterface() {
         return VanillaSoftwareProcessDriver.class;
     }
+
     @Override
     protected void connectSensors() {
         super.connectSensors();
-        connectServiceUpIsRunning();
+        if (isSshMonitoringEnabled()) {
+            connectServiceUpIsRunning();
+        } else {
+            // See SoftwareProcessImpl.waitForEntityStart(). We will already have waited for driver.isRunning.
+            // We will not poll for that again.
+            ServiceNotUpLogic.clearNotUpIndicator(this, SERVICE_PROCESS_IS_RUNNING);
+        }
     }
+    
     @Override
     protected void disconnectSensors() {
         disconnectServiceUpIsRunning();
         super.disconnectSensors();
     }
+    
+    protected boolean isSshMonitoringEnabled() {
+        return Boolean.TRUE.equals(getConfig(USE_SSH_MONITORING));
+    }
 }
\ No newline at end of file