This closes #72

Fix test to work on any combination of OS & eol type.
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 0000000..af5fbd1
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+-Xmx1024m -Xms512m -XX:MaxPermSize=256m
diff --git a/LICENSE b/LICENSE
index ccac2ba..ba06157 100644
--- a/LICENSE
+++ b/LICENSE
@@ -235,10 +235,15 @@
   Used under the following license: SIL OFL 1.1 (http://scripts.sil.org/OFL)
   Copyright (c) Dave Gandy (2016)
 
-This project includes the software: github.com/codegangsta/cli
-  Available at: https://github.com/codegangsta/cli
+This project includes the software: github.com/NodePrime/jsonpath/cli/jsonpath
+  Available at: github.com/NodePrime/jsonpath/
   Used under the following license: The MIT License (http://opensource.org/licenses/MIT)
-  Copyright (C) 2013 Jeremy Saenz
+  Copyright (c) 2015 NodePrime Inc.
+
+This project includes the software: github.com/urfave/cli
+  Available at: https://github.com/urfave/cli
+  Used under the following license: The MIT License (http://opensource.org/licenses/MIT)
+  Copyright (c) 2016 Jeremy Saenz & Contributors
 
 This project includes the software: golang.org/x/crypto/ssh
   Available at: https://godoc.org/golang.org/x/crypto/ssh
@@ -273,6 +278,14 @@
   Used under the following license: The MIT License (http://opensource.org/licenses/MIT)
   Copyright (c) "Cowboy" Ben Alman (2010)"
 
+This project includes the software: jQuery hashchange event
+  Available at: http://benalman.com/projects/jquery-hashchange-plugin/
+  Developed by: "Cowboy" Ben Alman (http://benalman.com/)
+  Inclusive of: jquery.ba-bbq*.js
+  Version used: 1.2
+  Used under the following license: The MIT License (http://opensource.org/licenses/MIT)
+  Copyright (c) "Cowboy" Ben Alman (2010)"
+
 This project includes the software: DataTables Table plug-in for jQuery
   Available at: http://www.datatables.net/
   Developed by: SpryMedia Ltd (http://sprymedia.co.uk/)
@@ -283,7 +296,7 @@
 
 This project includes the software: jquery.easy-autocomplete.js
   Available at: https://github.com/pawelczak/EasyAutocomplete
-  Version used: 1.3.1
+  Version used: 1.3.3
   Used under the following license: The MIT License (http://opensource.org/licenses/MIT)
   Copyright (c) Łukasz Pawełczak (2015)
 
diff --git a/examples/global-web-fabric/pom.xml b/examples/global-web-fabric/pom.xml
index c91290d..e44d249 100644
--- a/examples/global-web-fabric/pom.xml
+++ b/examples/global-web-fabric/pom.xml
@@ -26,7 +26,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/global-web-fabric/src/main/resources/catalog.bom b/examples/global-web-fabric/src/main/resources/catalog.bom
index 4197271..4818153 100644
--- a/examples/global-web-fabric/src/main/resources/catalog.bom
+++ b/examples/global-web-fabric/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: template
     items:
     - id: org.apache.brooklyn.demo.GlobalWebFabricExample
diff --git a/examples/pom.xml b/examples/pom.xml
index 110258b..3b14be3 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -32,7 +32,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/simple-messaging-pubsub/pom.xml b/examples/simple-messaging-pubsub/pom.xml
index 13dc86d..af81ec1 100644
--- a/examples/simple-messaging-pubsub/pom.xml
+++ b/examples/simple-messaging-pubsub/pom.xml
@@ -26,7 +26,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/simple-nosql-cluster/pom.xml b/examples/simple-nosql-cluster/pom.xml
index 7065cab..a3b3085 100644
--- a/examples/simple-nosql-cluster/pom.xml
+++ b/examples/simple-nosql-cluster/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/simple-nosql-cluster/src/main/resources/catalog.bom b/examples/simple-nosql-cluster/src/main/resources/catalog.bom
index f61f9cf..daacaaf 100644
--- a/examples/simple-nosql-cluster/src/main/resources/catalog.bom
+++ b/examples/simple-nosql-cluster/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: template
     items:
     - id: org.apache.brooklyn.demo.RiakClusterExample
diff --git a/examples/simple-web-cluster/pom.xml b/examples/simple-web-cluster/pom.xml
index 6e25fd0..f51df0b 100644
--- a/examples/simple-web-cluster/pom.xml
+++ b/examples/simple-web-cluster/pom.xml
@@ -28,7 +28,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/simple-web-cluster/src/main/resources/catalog.bom b/examples/simple-web-cluster/src/main/resources/catalog.bom
index 901c185..ebe3fa1 100644
--- a/examples/simple-web-cluster/src/main/resources/catalog.bom
+++ b/examples/simple-web-cluster/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: template
     items:
     - id: org.apache.brooklyn.demo.NodeJsTodoApplication
diff --git a/examples/webapps/hello-world-sql/pom.xml b/examples/webapps/hello-world-sql/pom.xml
index 969a1be..c55d0c7 100644
--- a/examples/webapps/hello-world-sql/pom.xml
+++ b/examples/webapps/hello-world-sql/pom.xml
@@ -26,7 +26,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-webapps-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/webapps/hello-world-webapp/pom.xml b/examples/webapps/hello-world-webapp/pom.xml
index ac2fe36..98ddce1 100644
--- a/examples/webapps/hello-world-webapp/pom.xml
+++ b/examples/webapps/hello-world-webapp/pom.xml
@@ -26,7 +26,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-webapps-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/examples/webapps/hello-world-webapp/src/main/webapp/styles/main.css b/examples/webapps/hello-world-webapp/src/main/webapp/styles/main.css
index 27606d3..b47cd5b 100644
--- a/examples/webapps/hello-world-webapp/src/main/webapp/styles/main.css
+++ b/examples/webapps/hello-world-webapp/src/main/webapp/styles/main.css
@@ -24,7 +24,7 @@
 	margin: 0;
 }
 #main{
-	background: url('../images/BrooklynBridge3Large.png')fixed;
+	background: url('../images/BrooklynBridge3Large.png');
 	-webkit-background-size: cover;
 	-moz-background-size: cover;
 	-o-background-size: cover;
diff --git a/examples/webapps/pom.xml b/examples/webapps/pom.xml
index dc67e86..92d4b01 100644
--- a/examples/webapps/pom.xml
+++ b/examples/webapps/pom.xml
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.apache.brooklyn.example</groupId>
         <artifactId>brooklyn-examples-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>   <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/karaf/catalog/pom.xml b/karaf/catalog/pom.xml
index 2748c47..fe9a1d2 100644
--- a/karaf/catalog/pom.xml
+++ b/karaf/catalog/pom.xml
@@ -24,7 +24,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library-karaf</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/karaf/catalog/src/main/resources/library-catalog-classes.bom b/karaf/catalog/src/main/resources/library-catalog-classes.bom
index eb46825..525a7d5 100644
--- a/karaf/catalog/src/main/resources/library-catalog-classes.bom
+++ b/karaf/catalog/src/main/resources/library-catalog-classes.bom
@@ -16,7 +16,7 @@
 # under the License.
 #
 brooklyn.catalog:
-  version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+  version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
   include: classpath://catalog-classes.bom
 
   items:
diff --git a/karaf/features/pom.xml b/karaf/features/pom.xml
index c4627bd..390e546 100644
--- a/karaf/features/pom.xml
+++ b/karaf/features/pom.xml
@@ -24,7 +24,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library-karaf</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
     </parent>
 
     <artifactId>brooklyn-library-features</artifactId>
@@ -38,7 +38,7 @@
                 <plugin>
                     <groupId>org.apache.karaf.tooling</groupId>
                     <artifactId>karaf-maven-plugin</artifactId>
-                    <version>4.0.1</version>
+                    <version>${karaf.plugin.version}</version>
                     <extensions>true</extensions>
                 </plugin>
             </plugins>
diff --git a/karaf/pom.xml b/karaf/pom.xml
index e158869..95ee864 100644
--- a/karaf/pom.xml
+++ b/karaf/pom.xml
@@ -24,7 +24,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/pom.xml b/pom.xml
index 473feaa..de1452d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,12 +24,11 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../brooklyn-server/parent/</relativePath>
     </parent>
 
     <artifactId>brooklyn-library</artifactId>
-    <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
     <packaging>pom</packaging>
 
     <name>Brooklyn Library Root</name>
@@ -45,9 +44,9 @@
     </developers>
 
     <scm>
-        <connection>scm:git:https://git-wip-us.apache.org/repos/asf/incubator-brooklyn.git</connection>
-        <developerConnection>scm:git:https://git-wip-us.apache.org/repos/asf/incubator-brooklyn.git</developerConnection>
-        <url>https://git-wip-us.apache.org/repos/asf?p=incubator-brooklyn.git</url>
+        <connection>scm:git:https://git-wip-us.apache.org/repos/asf/brooklyn-library.git</connection>
+        <developerConnection>scm:git:https://git-wip-us.apache.org/repos/asf/brooklyn-library.git</developerConnection>
+        <url>https://git-wip-us.apache.org/repos/asf?p=brooklyn-library.git</url>
         <tag>HEAD</tag>
     </scm>
 
@@ -57,7 +56,7 @@
     </issueManagement>
     <ciManagement>
         <system>Jenkins</system>
-        <url>https://builds.apache.org/job/incubator-brooklyn-master-build/</url>
+        <url>https://builds.apache.org/view/Brooklyn/job/brooklyn-library-master/</url>
     </ciManagement>
     <mailingLists>
         <mailingList>
diff --git a/qa/pom.xml b/qa/pom.xml
index 5a7e255..cbef5e6 100644
--- a/qa/pom.xml
+++ b/qa/pom.xml
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedExternalMonitor.java b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedExternalMonitor.java
new file mode 100644
index 0000000..3424842
--- /dev/null
+++ b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedExternalMonitor.java
@@ -0,0 +1,57 @@
+/*
+ * 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.qa.load;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.reflect.TypeToken;
+
+@ImplementedBy(SimulatedExternalMonitorImpl.class)
+public interface SimulatedExternalMonitor extends Entity, Startable {
+
+    @SuppressWarnings("serial")
+    ConfigKey<Predicate<? super Entity>> ENTITY_FILTER = ConfigKeys.newConfigKey(
+            new TypeToken<Predicate<? super Entity>>() {},
+            "entityFilter",
+            "Entities to set the sensors on",
+            Predicates.instanceOf(VanillaSoftwareProcess.class));
+
+    ConfigKey<Integer> NUM_SENSORS = ConfigKeys.newIntegerConfigKey(
+            "numSensors",
+            "Number of attribute sensors to set on each entity",
+            1);
+    
+    ConfigKey<Duration> POLL_PERIOD = ConfigKeys.newConfigKey(
+            Duration.class,
+            "pollPeriod",
+            "Period for polling to get the sensors (delay between polls)",
+            Duration.ONE_SECOND);
+    
+    AttributeSensor<Boolean> SERVICE_UP = Attributes.SERVICE_UP;
+}
diff --git a/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedExternalMonitorImpl.java b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedExternalMonitorImpl.java
new file mode 100644
index 0000000..a80c5a5
--- /dev/null
+++ b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedExternalMonitorImpl.java
@@ -0,0 +1,101 @@
+/*
+ * 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.qa.load;
+
+import java.util.Collection;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class SimulatedExternalMonitorImpl extends AbstractEntity implements SimulatedExternalMonitor {
+
+    private ScheduledExecutorService executor;
+    private Future<?> future;
+    
+    @Override
+    public void rebind() {
+        super.rebind();
+        if (Boolean.TRUE.equals(sensors().get(SERVICE_UP))) {
+            startPolling();
+        }
+    }
+    
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        if (Boolean.TRUE.equals(sensors().get(SERVICE_UP))) {
+            // already up; no-op
+        }
+        sensors().set(SERVICE_UP, true);
+        startPolling();
+    }
+
+    @Override
+    public void stop() {
+        sensors().set(SERVICE_UP, false);
+        stopPolling();
+    }
+
+    @Override
+    public void restart() {
+        stop();
+        start(ImmutableList.<Location>of());
+    }
+    
+    protected void startPolling() {
+        Duration pollPeriod = config().get(POLL_PERIOD);
+        executor = Executors.newScheduledThreadPool(1);
+        executor.scheduleWithFixedDelay(new Runnable() {
+            public void run() {
+                simulatePoll();
+            }}, 
+            0, pollPeriod.toMilliseconds(), TimeUnit.MILLISECONDS);
+    }
+    
+    protected void stopPolling() {
+        if (executor != null) {
+            executor.shutdownNow();
+            executor = null;
+        }
+    }
+    
+    protected void simulatePoll() {
+        String val = "val-" + System.currentTimeMillis();
+        Predicate<? super Entity> filter = config().get(ENTITY_FILTER);
+        Integer numSensors = config().get(NUM_SENSORS);
+        Iterable<Entity> entities = Iterables.filter(getManagementContext().getEntityManager().getEntities(), filter);
+        for (Entity entity : entities) {
+            for (int i = 0; i < numSensors; i++) {
+                AttributeSensor<String> sensor = Sensors.newStringSensor("externalSensor"+i);
+                entity.sensors().set(sensor, val);
+            }
+        }
+    }
+}
diff --git a/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
index 0e41beb..b0178e5 100644
--- a/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
+++ b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
@@ -33,6 +33,7 @@
 import org.apache.brooklyn.core.entity.StartableApplication;
 import org.apache.brooklyn.core.entity.trait.Startable;
 import org.apache.brooklyn.core.location.PortRanges;
+import org.apache.brooklyn.core.sensor.DependentConfiguration;
 import org.apache.brooklyn.enricher.stock.Enrichers;
 import org.apache.brooklyn.entity.database.mysql.MySqlNode;
 import org.apache.brooklyn.entity.group.DynamicCluster;
@@ -95,7 +96,8 @@
                         .configure(JavaWebAppService.ROOT_WAR, WAR_PATH)
                         .configure(JavaEntityMethods.javaSysProp("brooklyn.example.db.url"), 
                                 formatString("jdbc:%s%s?user=%s\\&password=%s", 
-                                        attributeWhenReady(mysql, MySqlNode.DATASTORE_URL), DB_TABLE, DB_USERNAME, DB_PASSWORD))
+                                        DependentConfiguration.builder().attributeWhenReady(mysql, MySqlNode.DATASTORE_URL).build(), 
+                                        DB_TABLE, DB_USERNAME, DB_PASSWORD))
                         .configure(DynamicCluster.INITIAL_SIZE, 2)
                         .configure(WebAppService.ENABLED_PROTOCOLS, ImmutableSet.of(USE_HTTPS ? "https" : "http")) );
 
diff --git a/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedVanillaSoftwareProcessImpl.java b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedVanillaSoftwareProcessImpl.java
new file mode 100644
index 0000000..e5aea80
--- /dev/null
+++ b/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedVanillaSoftwareProcessImpl.java
@@ -0,0 +1,220 @@
+/*
+ * 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.qa.load;
+
+import java.net.URI;
+import java.util.concurrent.Callable;
+
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcessImpl;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcessSshDriver;
+import org.apache.brooklyn.feed.function.FunctionFeed;
+import org.apache.brooklyn.feed.function.FunctionPollConfig;
+import org.apache.brooklyn.feed.http.HttpFeed;
+import org.apache.brooklyn.feed.http.HttpPollConfig;
+import org.apache.brooklyn.feed.http.HttpValueFunctions;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.time.Duration;
+
+/**
+ * For simulating various aspects of the {@link VanillaSoftwareProcess} entity.
+ *  
+ * It is assumed that the ssh commands for install, launch, etc will be written with testability in mind.
+ * For example, they might just be {@code echo} statements, because there is insufficient resources to 
+ * run 100s of processes.
+ * 
+ * It is thus possible to simulate aspects of the behaviour, for performance and load testing purposes. 
+ * 
+ * There is configuration for:
+ * <ul>
+ *   <li>{@code skipSshOnStart}
+ *     <ul>
+ *       <li>If true, then no ssh commands will be executed at deploy-time. 
+ *           This is useful for speeding up load testing, to get to the desired number of entities.
+ *       <li>If false, the ssh commands will be executed.
+ *     </ul>
+ *   <li>{@code simulateEntity}
+ *     <ul>
+ *       <li>if true, no underlying entity will be started. Instead a sleep 100000 job will be run and monitored.
+ *       <li>if false, the underlying entity (i.e. a JBoss app-server) will be started as normal.
+ *     </ul>
+ *   <li>{@code simulateExternalMonitoring}
+ *     <ul>
+ *       <li>if true, disables the default monitoring mechanism. Instead, a function will periodically execute 
+ *           to set the entity's sensors (as though the values had been obtained from the external monitoring tool).
+ *       <li>if false, then:
+ *         <ul>
+ *           <li>If {@code simulateEntity==true} it will execute comparable commands (e.g. execute a command of the same 
+ *               size over ssh or do a comparable number of http GET requests).
+ *           <li>If {@code simulateEntity==false} then normal monitoring will be done.
+ *         </ul>
+ *     </ul>
+ * </ul>
+ */
+public class SimulatedVanillaSoftwareProcessImpl extends VanillaSoftwareProcessImpl {
+
+    public static final ConfigKey<Boolean> EXEC_SSH_ON_START = ConfigKeys.newBooleanConfigKey(
+            "execSshOnStart", 
+            "If true, will execute the ssh commands on install/launch; if false, will skip them", 
+            true);
+
+    public static final ConfigKey<URI> HTTP_FEED_URI = ConfigKeys.newConfigKey(
+            URI.class, 
+            "httpFeed.uri", 
+            "If non-null, the URI to poll periodically using a HttpFeed", null);
+
+    public static final ConfigKey<Duration> HTTP_FEED_POLL_PERIOD = ConfigKeys.newConfigKey(
+            Duration.class, 
+            "httpFeed.pollPeriod", 
+            "The poll priod for the HttpFeed (if 'httpFeed.uri' was non-null)", 
+            Duration.ONE_SECOND);
+
+    public static final ConfigKey<Duration> FUNCTION_FEED_POLL_PERIOD = ConfigKeys.newConfigKey(
+            Duration.class, 
+            "functionFeed.pollPeriod", 
+            "The poll priod for a function that increments 'counter' periodically (if null, then no-op)", 
+            Duration.ONE_SECOND);
+
+    // see SERVICE_PROCESS_IS_RUNNING_POLL_PERIOD
+    // Inspired by EmptySoftwareProcess.USE_SSH_MONITORING
+    public static final ConfigKey<Boolean> USE_SSH_MONITORING = ConfigKeys.newConfigKey(
+            "sshMonitoring.enabled", 
+            "Whether to poll periodically over ssh, using the driver.isRunning check", 
+            Boolean.TRUE);
+
+    private static final AttributeSensor<String> HTTP_STRING_ATTRIBUTE = Sensors.newStringSensor("httpStringAttribute");
+
+    private static final AttributeSensor<Integer> HTTP_INT_ATTRIBUTE = Sensors.newIntegerSensor("httpIntAttribute");
+
+    private static final AttributeSensor<Long> FUNCTION_COUNTER = Sensors.newLongSensor("functionCounter");
+
+    private FunctionFeed functionFeed;
+    private HttpFeed httpFeed;
+    
+    @Override
+    public void init() {
+        super.init();
+        if (Boolean.FALSE.equals(config().get(EXEC_SSH_ON_START))) {
+            config().set(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true);
+        }
+    }
+    
+    @Override
+    public Class<?> getDriverInterface() {
+        return SimulatedVanillaSoftwareProcessSshDriver.class;
+    }
+
+    @Override
+    protected void connectServiceUpIsRunning() {
+        boolean useSshMonitoring = Boolean.TRUE.equals(config().get(USE_SSH_MONITORING));
+        if (useSshMonitoring) {
+            super.connectServiceUpIsRunning();
+        }
+    }
+    
+    @Override
+    protected void initEnrichers() {
+        super.initEnrichers();
+    }
+    
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        
+        boolean useSshMonitoring = Boolean.TRUE.equals(config().get(USE_SSH_MONITORING));
+        Duration functionFeedPeriod = config().get(FUNCTION_FEED_POLL_PERIOD);
+        URI httpFeedUri = config().get(HTTP_FEED_URI);
+        
+        if (!useSshMonitoring) {
+            ServiceNotUpLogic.clearNotUpIndicator(this, SERVICE_PROCESS_IS_RUNNING);
+        }
+
+        if (functionFeedPeriod != null) {
+            functionFeed = feeds().add(FunctionFeed.builder()
+                    .entity(this)
+                    .period(functionFeedPeriod)
+                    .poll(FunctionPollConfig.forSensor(FUNCTION_COUNTER)
+                            .callable(new Callable<Long>() {
+                                @Override public Long call() throws Exception {
+                                    Long oldVal = sensors().get(FUNCTION_COUNTER);
+                                    return (oldVal == null) ? 1 : oldVal + 1;
+                                }
+                            }))
+                    .build());
+        }
+        
+        if (httpFeedUri != null) {
+            httpFeed = feeds().add(HttpFeed.builder()
+                    .entity(this)
+                    .period(config().get(HTTP_FEED_POLL_PERIOD))
+                    .baseUri(httpFeedUri)
+                    .poll(new HttpPollConfig<Integer>(HTTP_INT_ATTRIBUTE)
+                            .onSuccess(HttpValueFunctions.responseCode()))
+                    .poll(new HttpPollConfig<String>(HTTP_STRING_ATTRIBUTE)
+                            .onSuccess(HttpValueFunctions.stringContentsFunction()))
+                    .build());
+        }
+    }
+
+    @Override
+    protected void disconnectSensors() {
+        super.disconnectSensors();
+        if (functionFeed != null) functionFeed.stop();
+        if (httpFeed != null) httpFeed.stop();
+    }
+    
+    public static class SimulatedVanillaSoftwareProcessSshDriver extends VanillaSoftwareProcessSshDriver {
+        public SimulatedVanillaSoftwareProcessSshDriver(SimulatedVanillaSoftwareProcessImpl entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        
+        @Override
+        public void install() {
+            if (Boolean.TRUE.equals(entity.getConfig(EXEC_SSH_ON_START))) {
+                super.install();
+            } else {
+                // no-op
+            }
+        }
+        
+        @Override
+        public void customize() {
+            if (Boolean.TRUE.equals(entity.getConfig(EXEC_SSH_ON_START))) {
+                super.customize();
+            } else {
+                // no-op
+            }
+        }
+        
+        @Override
+        public void launch() {
+            if (Boolean.TRUE.equals(entity.getConfig(EXEC_SSH_ON_START))) {
+                super.launch();
+            } else {
+                // no-op
+            }
+        }
+    }
+}
diff --git a/qa/src/test/java/org/apache/brooklyn/qa/load/AbstractLoadTest.java b/qa/src/test/java/org/apache/brooklyn/qa/load/AbstractLoadTest.java
new file mode 100644
index 0000000..c07f329
--- /dev/null
+++ b/qa/src/test/java/org/apache/brooklyn/qa/load/AbstractLoadTest.java
@@ -0,0 +1,378 @@
+/*
+ * 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.qa.load;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest;
+import org.apache.brooklyn.core.entity.StartableApplication;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.location.PortRanges;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.persist.PersistMode;
+import org.apache.brooklyn.core.test.HttpService;
+import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.launcher.BrooklynLauncher;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.test.performance.PerformanceTestUtils;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+/**
+ * Customers ask about the scalability of Brooklyn. These load tests investigate how many 
+ * concurrent apps can be deployed and managed by a single Brooklyn management node.
+ * 
+ * The apps are "simulated" in that they don't create the underlying resources 
+ * (we are not checking if the test machine can run 100s of app-servers simultaneously!) 
+ * 
+ * See the configuration options on {@link SimulatedVanillaSoftwareProcessImpl}.
+ * 
+ * The {@link SimulatedExternalMonitor} is used to simulate us not polling the entities directly
+ * (over ssh, http or whatever). Instead we simulate the metrics being retrieved from some external
+ * source, and injected directly into the entities by calling {@code sensors().set()}. For example,
+ * this could be collected from a Graphite server.
+ * 
+ * If using {@link TestConfig#simulateExternalMonitor(Predicate, int, Duration)}, it will 
+ * automatically turn off {@code useSshMonitoring}, {@code useHttpMonitoring} and 
+ * {@code useFunctionMonitoring} for <em>all</em> entities (not just for those that match 
+ * the predicate passed to simulateExternalMonitor).
+ */
+public class AbstractLoadTest extends AbstractYamlTest {
+
+    // TODO Could/should issue provisioning request through REST api, rather than programmatically; 
+    // and poll to detect completion.
+    
+    /*
+     * Useful commands when investigating:
+     *     LOG_FILE=usage/qa/brooklyn-camp-tests.log
+     *     grep -E "OutOfMemoryError|[P|p]rovisioning time|sleeping before|CPU fraction|LoadTest using" $LOG_FILE | less
+     *     grep -E "OutOfMemoryError|[P|p]rovisioning time" $LOG_FILE; grep "CPU fraction" $LOG_FILE | tail -1; grep "LoadTest using" $LOG_FILE | tail -1
+     *     grep -E "OutOfMemoryError|LoadTest using" $LOG_FILE
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractLoadTest.class);
+
+    private File persistenceDir;
+    private BrooklynLauncher launcher;
+    private ListeningExecutorService executor;
+    private Future<?> cpuFuture;
+    
+    private Location localhost;
+    
+    List<Duration> provisioningTimes;
+
+    private HttpService httpService;
+    private URI httpServiceUri;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        super.setUp();
+        
+        localhost = mgmt().getLocationRegistry().getLocationManaged("localhost");
+        
+        provisioningTimes = Collections.synchronizedList(Lists.<Duration>newArrayList());
+
+        // Create executors
+        executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+
+        // Monitor utilisation (memory/CPU) while tests run
+        executor.submit(new Callable<Void>() {
+            public Void call() {
+                try {
+                    mgmt().getExecutionManager(); // force GC to be instantiated
+                    while (true) {
+                        String usage = ((LocalManagementContext)mgmt()).getGarbageCollector().getUsageString();
+                        LOG.info("LoadTest using "+usage);
+                        Thread.sleep(1000);
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt(); // exit gracefully
+                } catch (Exception e) {
+                    LOG.error("Error getting usage info", e);
+                }
+                return null;
+            }});
+        
+        cpuFuture = PerformanceTestUtils.sampleProcessCpuTime(Duration.ONE_SECOND, "during LoadTest");
+        
+        httpService = new HttpService(PortRanges.fromString("9000+"), true).start();
+        httpServiceUri = new URI(httpService.getUrl());
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        try {
+            if (httpService != null) httpService.shutdown();
+            if (cpuFuture != null) cpuFuture.cancel(true);
+            if (executor != null) executor.shutdownNow();
+        } finally {
+            super.tearDown();
+        }
+    }
+
+    @Override
+    protected ManagementContext setUpPlatform() {
+        // Create management node
+        persistenceDir = Files.createTempDir();
+        launcher = BrooklynLauncher.newInstance()
+                .persistMode(PersistMode.CLEAN)
+                .highAvailabilityMode(HighAvailabilityMode.MASTER)
+                .persistenceDir(persistenceDir)
+                .start();
+        
+        String webServerUrl = launcher.getServerDetails().getWebServerUrl();
+        LOG.info("Brooklyn web-console running at " + webServerUrl);
+        
+        return launcher.getServerDetails().getManagementContext();
+    }
+    
+    @Override
+    protected void tearDownPlatform() {
+        if (launcher != null) launcher.terminate();
+        if (persistenceDir != null) Os.deleteRecursively(persistenceDir);
+    }
+    
+    public static class TestConfig {
+        public int totalApps = 1;
+        public int numAppsPerBatch = 1;
+        public Duration sleepBetweenBatch = Duration.ZERO;
+        
+        int clusterSize = 2;
+        
+        boolean simulateExternalMonitor = false;
+        Predicate<? super Entity> externalMonitorFilter;
+        int externalMonitorNumSensors;
+        Duration externalMonitorPollPeriod;
+        
+        
+        boolean execSshOnStart = SimulatedVanillaSoftwareProcessImpl.EXEC_SSH_ON_START.getDefaultValue();
+        Duration functionFeedPollPeriod = SimulatedVanillaSoftwareProcessImpl.FUNCTION_FEED_POLL_PERIOD.getDefaultValue();
+        boolean useSshMonitoring = SimulatedVanillaSoftwareProcessImpl.USE_SSH_MONITORING.getDefaultValue();
+        Duration httpFeedPollPeriod = SimulatedVanillaSoftwareProcessImpl.HTTP_FEED_POLL_PERIOD.getDefaultValue();
+        URI httpFeedUri;
+        
+        public TestConfig(AbstractLoadTest tester) {
+            httpFeedUri = tester.httpServiceUri;
+        }
+        public TestConfig simulateExternalMonitor(Predicate<? super Entity> filter, int numSensors, Duration pollPeriod) {
+            simulateExternalMonitor = true;
+            externalMonitorFilter = filter;
+            externalMonitorNumSensors = numSensors;
+            externalMonitorPollPeriod = pollPeriod;
+            useSshMonitoring(false);
+            useHttpMonitoring(false);
+            useFunctionMonitoring(false);
+            return this;
+        }
+        public TestConfig totalApps(int totalApps) {
+            return totalApps(totalApps, totalApps);
+        }
+        public TestConfig totalApps(int totalApps, int numAppsPerBatch) {
+            this.totalApps = totalApps;
+            this.numAppsPerBatch = numAppsPerBatch;
+            return this;
+        }
+        public TestConfig sleepBetweenBatch(Duration val) {
+            sleepBetweenBatch = val;
+            return this;
+        }
+        public TestConfig clusterSize(int val) {
+            clusterSize = val;
+            return this;
+        }
+        public TestConfig execSshOnStart(boolean val) {
+            execSshOnStart = val;
+            return this;
+        }
+        public TestConfig useSshMonitoring(boolean val) {
+            useSshMonitoring = val;
+            return this;
+        }
+        public TestConfig useHttpMonitoring(boolean val) {
+            if (val) {
+                if (httpFeedUri == null) {
+                    throw new IllegalStateException("No HTTP URI; expected to be set by AbstractLoadTest.httpServiceUri");
+                }
+            } else {
+                httpFeedUri = null;
+            }
+            return this;
+        }
+        public TestConfig useFunctionMonitoring(boolean val) {
+            if (val) {
+                functionFeedPollPeriod = SimulatedVanillaSoftwareProcessImpl.FUNCTION_FEED_POLL_PERIOD.getDefaultValue(); 
+            } else {
+                functionFeedPollPeriod = null;
+            }
+            return this;
+        }
+    }
+    
+    protected void runLocalhostManyApps(TestConfig config) throws Exception {
+        final int totalApps = config.totalApps;
+        final int numAppsPerBatch = config.numAppsPerBatch;
+        final int numCycles = (totalApps / numAppsPerBatch);
+        final Duration sleepBetweenBatch = config.sleepBetweenBatch;
+        
+        int counter = 0;
+        
+        if (config.simulateExternalMonitor) {
+            SimulatedExternalMonitor externalMonitor = mgmt().getEntityManager().createEntity(EntitySpec.create(SimulatedExternalMonitor.class)
+                    .configure(SimulatedExternalMonitor.ENTITY_FILTER, config.externalMonitorFilter)
+                    .configure(SimulatedExternalMonitor.NUM_SENSORS, config.externalMonitorNumSensors)
+                    .configure(SimulatedExternalMonitor.POLL_PERIOD, config.externalMonitorPollPeriod));
+            externalMonitor.start(ImmutableList.<Location>of());
+        }
+        for (int i = 0; i < numCycles; i++) {
+            List<ListenableFuture<? extends Entity>> futures = Lists.newArrayList();
+            for (int j = 0; j < numAppsPerBatch; j++) {
+                String yamlApp = newYamlApp("Simulated App " + i, config);
+                
+                ListenableFuture<? extends Entity> future = executor.submit(newProvisionAppTask(yamlApp));
+                futures.add(future);
+                counter++;
+            }
+            
+            List<? extends Entity> apps = Futures.allAsList(futures).get();
+            
+            for (Entity app : apps) {
+                assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
+            }
+
+            synchronized (provisioningTimes) {
+                LOG.info("cycle="+i+"; numApps="+counter+": provisioning times: "+provisioningTimes);
+                provisioningTimes.clear();
+            }
+
+            LOG.info("cycle="+i+"; numApps="+counter+": sleeping for "+sleepBetweenBatch+" before next batch of apps");
+            Time.sleep(sleepBetweenBatch);
+        }
+    }
+    
+    protected String newYamlApp(String appName, TestConfig config) {
+        return Joiner.on("\n").join(
+                "name: " + appName,
+                "location: localhost",
+
+                "services:",
+                "- type: " + DynamicCluster.class.getName(),
+                "  id: cluster",
+                "  brooklyn.config:",
+                "    cluster.initial.size: " + config.clusterSize,
+                "    memberSpec:",
+                "      $brooklyn:entitySpec:",
+                "        type: " + SimulatedVanillaSoftwareProcessImpl.class.getName(),
+                "        brooklyn.config:",
+                "          shell.env:",
+                "            ENV1: val1",
+                "            ENV2: val2",
+                "        install.command: echo myInstallCommand",
+                "        customize.command: echo myCustomizeCommand",
+                "        launch.command: echo myLaunchCommand",
+                "        checkRunning.command: echo myCheckRunningCommand",
+                "        " + SimulatedVanillaSoftwareProcessImpl.EXEC_SSH_ON_START.getName() + ": " + config.execSshOnStart,
+                "        " + SimulatedVanillaSoftwareProcessImpl.USE_SSH_MONITORING.getName() + ": " + config.useSshMonitoring,
+                "        " + SimulatedVanillaSoftwareProcessImpl.FUNCTION_FEED_POLL_PERIOD.getName() + ": " + config.functionFeedPollPeriod,
+                "        " + SimulatedVanillaSoftwareProcessImpl.HTTP_FEED_POLL_PERIOD.getName() + ": " + config.httpFeedPollPeriod,
+                "        " + (config.httpFeedUri != null ? SimulatedVanillaSoftwareProcessImpl.HTTP_FEED_URI.getName() + ": " + config.httpFeedUri : ""),
+                "  brooklyn.enrichers:",
+                "  - type: org.apache.brooklyn.enricher.stock.Aggregator",
+                "    brooklyn.config:",
+                "      enricher.sourceSensor: counter",
+                "      enricher.targetSensor: counter",
+                "      transformation: sum",
+                "  brooklyn.policies:",
+                "  - type: " + AutoScalerPolicy.class.getName(),
+                "    brooklyn.config:",
+                "      metric: sensorDoesNotExist",
+                "      metricLowerBound: 1",
+                "      metricUpperBound: 3",
+                "      minPoolSize: " + config.clusterSize,
+                "      maxPoolSize: " + (config.clusterSize + 3));
+    }
+    
+    protected Callable<Entity> newProvisionAppTask(final String yaml) {
+        return new Callable<Entity>() {
+            public Entity call() throws Exception {
+                try {
+                    Stopwatch stopwatch = Stopwatch.createStarted();
+                    Entity app = createAndStartApplication(yaml);
+                    Duration duration = Duration.of(stopwatch.elapsed(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
+                    LOG.info("Provisioning time: "+duration);
+                    provisioningTimes.add(duration);
+    
+                    return app;
+                } catch (Throwable t) {
+                    LOG.error("Error deploying app (rethrowing)", t);
+                    throw Exceptions.propagate(t);
+                }
+            }
+        };
+    }
+    
+    protected <T extends StartableApplication> Callable<T> newProvisionAppTask(final EntitySpec<T> appSpec) {
+        return new Callable<T>() {
+            public T call() {
+                try {
+                    Stopwatch stopwatch = Stopwatch.createStarted();
+                    T app = mgmt().getEntityManager().createEntity(appSpec);
+                    app.start(ImmutableList.of(localhost));
+                    Duration duration = Duration.of(stopwatch.elapsed(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
+                    LOG.info("Provisioning time: "+duration);
+                    provisioningTimes.add(duration);
+    
+                    return app;
+                } catch (Throwable t) {
+                    LOG.error("Error deploying app (rethrowing)", t);
+                    throw Exceptions.propagate(t);
+                }
+            }
+        };
+    }
+}
diff --git a/qa/src/test/java/org/apache/brooklyn/qa/load/LoadSanityTest.java b/qa/src/test/java/org/apache/brooklyn/qa/load/LoadSanityTest.java
new file mode 100644
index 0000000..5f082f2
--- /dev/null
+++ b/qa/src/test/java/org/apache/brooklyn/qa/load/LoadSanityTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.qa.load;
+
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import org.apache.brooklyn.util.time.Duration;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicates;
+
+/**
+ * A trivially small "load" test, which just checks that our test is actually working.
+ * It deploys just one app.
+ */
+public class LoadSanityTest extends AbstractLoadTest {
+
+    @Test(groups="Integration")
+    public void testApp() throws Exception {
+        super.runLocalhostManyApps(new TestConfig(this)
+                .execSshOnStart(true) // default is true, but be explicit
+                .useSshMonitoring(true) // default is true, but be explicit
+                .useHttpMonitoring(true) // default is true, but be explicit
+                .useFunctionMonitoring(true) // default is true, but be explicit
+                .totalApps(1));
+    }
+    
+    @Test(groups="Integration")
+    public void testAppExternallyMonitored() throws Exception {
+        super.runLocalhostManyApps(new TestConfig(this)
+                .simulateExternalMonitor(Predicates.instanceOf(VanillaSoftwareProcess.class), 5, Duration.ONE_SECOND)
+                .useSshMonitoring(false)
+                .useHttpMonitoring(false)
+                .useFunctionMonitoring(false)
+                .totalApps(1));
+    }
+}
diff --git a/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java b/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java
index cc779a9..50ffb6b 100644
--- a/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java
+++ b/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java
@@ -18,135 +18,15 @@
  */
 package org.apache.brooklyn.qa.load;
 
-import static org.testng.Assert.assertEquals;
-
-import java.io.File;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.mgmt.ManagementContext;
-import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
-import org.apache.brooklyn.core.entity.Entities;
-import org.apache.brooklyn.core.entity.StartableApplication;
-import org.apache.brooklyn.core.entity.trait.Startable;
-import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
-import org.apache.brooklyn.core.mgmt.persist.PersistMode;
-import org.apache.brooklyn.launcher.BrooklynLauncher;
-import org.apache.brooklyn.test.PerformanceTestUtils;
-import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import org.apache.brooklyn.qa.load.AbstractLoadTest.TestConfig;
 import org.apache.brooklyn.util.time.Duration;
-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.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.io.Files;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.base.Predicates;
 
-/**
- * Customers ask about the scalability of Brooklyn. These load tests investigate how many 
- * concurrent apps can be deployed and managed by a single Brooklyn management node.
- * 
- * The apps are "simulated" in that they don't create the underlying resources 
- * (we are not checking if the test machine can run 100s of app-servers simultaneously!) 
- * The install/customize/launch will instead execute ssh commands of comparable length,
- * but that just echo rather than execute the actual commands.
- * 
- * "SIMULATE_EXTERNAL_MONITORING" means that we do not poll the entities directly (over ssh, http or 
- * whatever). Instead we simulate the metrics being injected directly to be set on the entity (e.g. 
- * having been collected from a Graphite server).
- * 
- * "SKIP_SSH_ON_START" means don't do the normal install+customize+launch ssh commands. Instead, just
- * startup the entities so we can monitor their resource usage.
- */
-public class LoadTest {
+public class LoadTest extends AbstractLoadTest {
 
-    // TODO Could/should issue provisioning request through REST api, rather than programmatically; 
-    // and poll to detect completion.
-    
-    /*
-     * Useful commands when investigating:
-     *     LOG_FILE=usage/qa/brooklyn-camp-tests.log
-     *     grep -E "OutOfMemoryError|[P|p]rovisioning time|sleeping before|CPU fraction|LoadTest using" $LOG_FILE | less
-     *     grep -E "OutOfMemoryError|[P|p]rovisioning time" $LOG_FILE; grep "CPU fraction" $LOG_FILE | tail -1; grep "LoadTest using" $LOG_FILE | tail -1
-     *     grep -E "OutOfMemoryError|LoadTest using" $LOG_FILE
-     */
-    private static final Logger LOG = LoggerFactory.getLogger(LoadTest.class);
-
-    private File persistenceDir;
-    private BrooklynLauncher launcher;
-    private String webServerUrl;
-    private ManagementContext managementContext;
-    private ListeningExecutorService executor;
-    private Future<?> cpuFuture;
-    
-    private Location localhost;
-    
-    List<Duration> provisioningTimes;
-
-
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        // Create management node
-        persistenceDir = Files.createTempDir();
-        launcher = BrooklynLauncher.newInstance()
-                .persistMode(PersistMode.CLEAN)
-                .highAvailabilityMode(HighAvailabilityMode.MASTER)
-                .persistenceDir(persistenceDir)
-                .start();
-        webServerUrl = launcher.getServerDetails().getWebServerUrl();
-        managementContext = launcher.getServerDetails().getManagementContext();
-
-        localhost = managementContext.getLocationRegistry().getLocationManaged("localhost");
-        
-        provisioningTimes = Collections.synchronizedList(Lists.<Duration>newArrayList());
-
-        // Create executors
-        executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
-
-        // Monitor utilisation (memory/CPU) while tests run
-        executor.submit(new Callable<Void>() {
-            public Void call() {
-                try {
-                    while (true) {
-                        managementContext.getExecutionManager(); // force GC to be instantiated
-                        String usage = ((LocalManagementContext)managementContext).getGarbageCollector().getUsageString();
-                        LOG.info("LoadTest using "+usage);
-                        Thread.sleep(1000);
-                    }
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt(); // exit gracefully
-                } catch (Exception e) {
-                    LOG.error("Error getting usage info", e);
-                }
-                return null;
-            }});
-        
-        cpuFuture = PerformanceTestUtils.sampleProcessCpuTime(Duration.ONE_SECOND, "during testProvisionAppsConcurrently");
-
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (cpuFuture != null) cpuFuture.cancel(true);
-        if (executor != null) executor.shutdownNow();
-        if (launcher != null) launcher.terminate();
-        if (persistenceDir != null) Os.deleteRecursively(persistenceDir);
-    }
-    
     /**
      * Creates multiple apps simultaneously. 
      * 
@@ -158,84 +38,35 @@
      * TODO Does not measure the cost of jclouds for creating all the VMs/containers.
      */
     @Test(groups="Acceptance")
-    public void testLocalhostProvisioningAppsConcurrently() throws Exception {
-        final int NUM_CONCURRENT_APPS_PROVISIONING = 20; 
-        
-        List<ListenableFuture<StartableApplication>> futures = Lists.newArrayList();
-        for (int i = 0; i < NUM_CONCURRENT_APPS_PROVISIONING; i++) {
-            ListenableFuture<StartableApplication> future = executor.submit(newProvisionAppTask(managementContext, 
-                    EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
-                            .configure(SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING, true)
-                            .displayName("Simulated app "+i)));
-            futures.add(future);
-        }
-        
-        List<StartableApplication> apps = Futures.allAsList(futures).get();
-        
-        for (StartableApplication app : apps) {
-            assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
-        }
+    public void testProvisioningConcurrently() throws Exception {
+        // TODO Getting ssh error (SocketException: Connection reset) with 10 entities, if don't disable ssh-on-start.
+        // Will still execute checkRunning to wait for process to start (even if execSshOnStart is false).
+        super.runLocalhostManyApps(new TestConfig(this)
+                .useSshMonitoring(false)
+                .execSshOnStart(false) // getting ssh errors otherwise!
+                .totalApps(10));
     }
-    
+
     /**
      * Creates many apps, to monitor resource usage etc.
      * 
-     * "SIMULATE_EXTERNAL_MONITORING" means that we do not poll the entities directly (over ssh, http or 
-     * whatever). Instead we simulate the metrics being injected directly to be set on the entity (e.g. 
-     * having been collected from a Graphite server).
-     * 
      * Long-term target is 2500 VMs under management.
      * Until we reach that point, we can partition the load across multiple (separate) brooklyn management nodes.
      */
     @Test(groups="Acceptance")
-    public void testLocalhostManyApps() throws Exception {
-        final int NUM_APPS = 630; // target is 2500 VMs; each blueprint has 4 (rounding up)
+    public void testManyAppsExternallyMonitored() throws Exception {
+        // TODO Getting ssh error ("Server closed connection during identification exchange") 
+        // with only two cycles (i.e. 20 entities).
+        //
+        // The ssh activity is from `SoftwareProcessImpl.waitForEntityStart`, which calls
+        // `VanillaSoftwareProcessSshDriver.isRunning`.
+        final int TOTAL_APPS = 600; // target is 2500 VMs; each blueprint has 2 VanillaSoftwareProcess
         final int NUM_APPS_PER_BATCH = 10;
-        final int SLEEP_BETWEEN_BATCHES = 10*1000;
-        final boolean SKIP_SSH_ON_START = true; // getting ssh errors otherwise!
-        
-        int counter = 0;
-        
-        for (int i = 0; i < NUM_APPS / NUM_APPS_PER_BATCH; i++) {
-            List<ListenableFuture<StartableApplication>> futures = Lists.newArrayList();
-            for (int j = 0; j < NUM_APPS_PER_BATCH; j++) {
-                ListenableFuture<StartableApplication> future = executor.submit(newProvisionAppTask(
-                        managementContext, 
-                        EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
-                                .configure(SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING, true)
-                                .configure(SimulatedTheeTierApp.SKIP_SSH_ON_START, SKIP_SSH_ON_START)
-                                .displayName("Simulated app "+(++counter))));
-                futures.add(future);
-            }
-            
-            List<StartableApplication> apps = Futures.allAsList(futures).get();
-            
-            for (StartableApplication app : apps) {
-                assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
-            }
-
-            synchronized (provisioningTimes) {
-                LOG.info("cycle="+i+"; numApps="+counter+": provisioning times: "+provisioningTimes);
-                provisioningTimes.clear();
-            }
-
-            LOG.info("cycle="+i+"; numApps="+counter+": sleeping before next batch of apps");
-            Thread.sleep(SLEEP_BETWEEN_BATCHES);
-        }
-    }
-    
-    protected <T extends StartableApplication> Callable<T> newProvisionAppTask(final ManagementContext managementContext, final EntitySpec<T> appSpec) {
-        return new Callable<T>() {
-            public T call() {
-                Stopwatch stopwatch = Stopwatch.createStarted();
-                T app = managementContext.getEntityManager().createEntity(appSpec);
-                app.start(ImmutableList.of(localhost));
-                Duration duration = Duration.of(stopwatch.elapsed(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
-                LOG.info("Provisioning time: "+duration);
-                provisioningTimes.add(duration);
-
-                return app;
-            }
-        };
+        super.runLocalhostManyApps(new TestConfig(this)
+                .execSshOnStart(false) // getting ssh errors otherwise!
+                .simulateExternalMonitor(Predicates.instanceOf(VanillaSoftwareProcess.class), 5, Duration.ONE_SECOND)
+                .clusterSize(2)
+                .totalApps(TOTAL_APPS, NUM_APPS_PER_BATCH)
+                .sleepBetweenBatch(Duration.TEN_SECONDS));
     }
 }
diff --git a/qa/src/test/projects/downstream-parent-test/pom.xml b/qa/src/test/projects/downstream-parent-test/pom.xml
index a679026..801ad7b 100644
--- a/qa/src/test/projects/downstream-parent-test/pom.xml
+++ b/qa/src/test/projects/downstream-parent-test/pom.xml
@@ -23,7 +23,7 @@
 
     <groupId>org.apache.brooklyn.downstream-parent-test</groupId>
     <artifactId>catalogue-load-test</artifactId>
-    <version>0.10.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
+    <version>0.11.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
     <packaging>jar</packaging>
 
     <name>Downstream parent catalogue load test test</name>
@@ -31,7 +31,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-downstream-parent</artifactId>
-        <version>0.10.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
     </parent>
 
     <repositories>
@@ -62,7 +62,7 @@
         <dependency>
             <groupId>org.apache.brooklyn</groupId>
             <artifactId>brooklyn-all</artifactId>
-            <version>0.10.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
+            <version>0.11.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
             <scope>provided</scope>
         </dependency>
     </dependencies>
diff --git a/qa/src/test/projects/downstream-parent-test/src/main/resources/catalog.bom b/qa/src/test/projects/downstream-parent-test/src/main/resources/catalog.bom
index 49510be..ac976c9 100644
--- a/qa/src/test/projects/downstream-parent-test/src/main/resources/catalog.bom
+++ b/qa/src/test/projects/downstream-parent-test/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: com.example.HelloEntity
diff --git a/qa/src/test/resources/java-web-app-and-db-with-policy.bom b/qa/src/test/resources/java-web-app-and-db-with-policy.bom
index 8568f8b..f29757b 100644
--- a/qa/src/test/resources/java-web-app-and-db-with-policy.bom
+++ b/qa/src/test/resources/java-web-app-and-db-with-policy.bom
@@ -20,7 +20,7 @@
 # standalone catalog file
 
 brooklyn.catalog:
-  version: "0.10.0-SNAPSHOT"   # BROOKLYN_VERSION
+  version: "0.11.0-SNAPSHOT"   # BROOKLYN_VERSION
   items:
   - id: java-cluster-db-policy-example
     itemType: template
diff --git a/qa/start-monitor.sh b/qa/start-monitor.sh
index 3340b09..e5852fd 100755
--- a/qa/start-monitor.sh
+++ b/qa/start-monitor.sh
@@ -25,7 +25,7 @@
 #set -x # debug
 
 CLASS=org.apache.brooklyn.qa.longevity.Monitor
-VERSION=0.10.0-SNAPSHOT # BROOKLYN_VERSION
+VERSION=0.11.0-SNAPSHOT # BROOKLYN_VERSION
 
 ROOT=$(cd $(dirname $0) && pwd)
 cd $ROOT
diff --git a/qa/start-webcluster.sh b/qa/start-webcluster.sh
index c7eda34..ec2a4fa 100755
--- a/qa/start-webcluster.sh
+++ b/qa/start-webcluster.sh
@@ -25,7 +25,7 @@
 #set -x # debug
 
 CLASS=org.apache.brooklyn.qa.longevity.webcluster.WebClusterApp
-VERSION=0.10.0-SNAPSHOT # BROOKLYN_VERSION
+VERSION=0.11.0-SNAPSHOT # BROOKLYN_VERSION
 
 ROOT=$(cd $(dirname $0) && pwd)
 cd $ROOT
diff --git a/sandbox/cassandra-multicloud-snitch/pom.xml b/sandbox/cassandra-multicloud-snitch/pom.xml
index f1a49f4..62f5d9f 100644
--- a/sandbox/cassandra-multicloud-snitch/pom.xml
+++ b/sandbox/cassandra-multicloud-snitch/pom.xml
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/sandbox/database/pom.xml b/sandbox/database/pom.xml
index cdd5f02..54a328f 100644
--- a/sandbox/database/pom.xml
+++ b/sandbox/database/pom.xml
@@ -31,7 +31,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/sandbox/database/src/main/java/org/apache/brooklyn/entity/database/derby/DerbySchema.java b/sandbox/database/src/main/java/org/apache/brooklyn/entity/database/derby/DerbySchema.java
index 07417a5..67b2a40 100644
--- a/sandbox/database/src/main/java/org/apache/brooklyn/entity/database/derby/DerbySchema.java
+++ b/sandbox/database/src/main/java/org/apache/brooklyn/entity/database/derby/DerbySchema.java
@@ -31,7 +31,6 @@
 import org.apache.brooklyn.entity.database.Schema;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.entity.java.UsesJmx;
 import org.apache.brooklyn.feed.jmx.JmxAttributePollConfig;
 import org.apache.brooklyn.feed.jmx.JmxFeed;
@@ -104,7 +103,7 @@
             exchange = new ObjectName(format("org.apache.derby:type=VirtualHost.Exchange,VirtualHost=\"%s\",name=\"amq.direct\",ExchangeType=direct", virtualHost));
             create();
 
-            jmxHelper = new JmxHelper((EntityLocal)getParent());
+            jmxHelper = new JmxHelper(getParent());
 
             ObjectName schemaMBeanName = new ObjectName(format("org.apache.derby:type=VirtualHost.Schema,VirtualHost=\"%s\",name=\"%s\"", virtualHost, name));
 
diff --git a/sandbox/extra/pom.xml b/sandbox/extra/pom.xml
index d308833..eeebd44 100644
--- a/sandbox/extra/pom.xml
+++ b/sandbox/extra/pom.xml
@@ -31,7 +31,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/sandbox/mobile-app/pom.xml b/sandbox/mobile-app/pom.xml
index 60f26ad..3a40f13 100644
--- a/sandbox/mobile-app/pom.xml
+++ b/sandbox/mobile-app/pom.xml
@@ -35,7 +35,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version><!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version><!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/sandbox/monitoring/pom.xml b/sandbox/monitoring/pom.xml
index 0298913..b9dd2ce 100644
--- a/sandbox/monitoring/pom.xml
+++ b/sandbox/monitoring/pom.xml
@@ -31,7 +31,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/sandbox/monitoring/src/main/resources/catalog.bom b/sandbox/monitoring/src/main/resources/catalog.bom
index 97704d3..6a8f064 100644
--- a/sandbox/monitoring/src/main/resources/catalog.bom
+++ b/sandbox/monitoring/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     items:
     - id: org.apache.brooklyn.entity.monitoring.zabbix.ZabbixServer
       item:
diff --git a/sandbox/nosql/pom.xml b/sandbox/nosql/pom.xml
index c821cb9..ae97aa8 100644
--- a/sandbox/nosql/pom.xml
+++ b/sandbox/nosql/pom.xml
@@ -32,7 +32,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/cm/ansible/pom.xml b/software/cm/ansible/pom.xml
index 6d55478..7054b39 100644
--- a/software/cm/ansible/pom.xml
+++ b/software/cm/ansible/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java
index fd73c61..b2a104a 100644
--- a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java
+++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java
@@ -18,7 +18,8 @@
  */
 package org.apache.brooklyn.entity.cm.ansible;
 
-import com.google.common.base.Supplier;
+import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
+
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.MachineLocation;
 import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
@@ -35,14 +36,14 @@
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.net.Urls;
-
-import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.apache.brooklyn.util.time.Time;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Supplier;
+
 public class AnsibleLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements AnsibleConfig {
 
     private static final Logger LOG = LoggerFactory.getLogger(AnsibleLifecycleEffectorTasks.class);
@@ -135,6 +136,7 @@
     }
 
 
+    @Override
     protected void postStartCustom() {
         boolean result = false;
         result |= tryCheckStartService();
@@ -173,11 +175,12 @@
                             .setOnFailureOrException(false))
                     .build();
                     
-             entity().feeds().addFeed(serviceSshFeed);
+             entity().feeds().add(serviceSshFeed);
         } else {
             LOG.warn("Location(s) {} not an ssh-machine location, so not polling for status; "
                     + "setting serviceUp immediately", entity().getLocations());
         }
+        super.postStartCustom();
     }
 
     protected boolean tryCheckStartService() {
diff --git a/software/cm/ansible/src/main/resources/catalog.bom b/software/cm/ansible/src/main/resources/catalog.bom
index 68f453d..e4f6a06 100644
--- a/software/cm/ansible/src/main/resources/catalog.bom
+++ b/software/cm/ansible/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.cm.ansible.AnsibleEntity
diff --git a/software/cm/pom.xml b/software/cm/pom.xml
index 0bfde48..3cfbe2b 100644
--- a/software/cm/pom.xml
+++ b/software/cm/pom.xml
@@ -25,13 +25,13 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
     <groupId>org.apache.brooklyn</groupId>
     <artifactId>brooklyn-software-cm</artifactId>
-    <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+    <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
     <packaging>pom</packaging>
 
     <name>Brooklyn CM Integration Root</name>
diff --git a/software/cm/salt/pom.xml b/software/cm/salt/pom.xml
index a5ba926..8e95954 100644
--- a/software/cm/salt/pom.xml
+++ b/software/cm/salt/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java
index e48c0b2..fdae93b 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java
@@ -73,7 +73,7 @@
 
 
     private static String adaptForSaltYamlTypes(String description) {
-        return description.replaceAll("!!python/unicode", "!!java.lang.String");
+        return description.replaceAll("!!python/unicode\\s+", "");
     }
 
     @SuppressWarnings("unchecked")
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java
index 871caea..49fd336 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java
@@ -18,21 +18,26 @@
  */
 package org.apache.brooklyn.entity.cm.salt.impl;
 
-import com.google.common.annotations.Beta;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Supplier;
-import com.google.common.collect.ImmutableMap;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.MULTILINE;
+import static org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode.ALWAYS;
+import static org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode.NEVER;
+
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.MachineLocation;
 import org.apache.brooklyn.api.mgmt.TaskAdaptable;
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
-import org.apache.brooklyn.entity.cm.salt.SaltConfig;
 import org.apache.brooklyn.core.effector.Effectors;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
 import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.entity.cm.salt.SaltConfig;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
 import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
@@ -46,14 +51,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static java.util.regex.Pattern.DOTALL;
-import static java.util.regex.Pattern.MULTILINE;
-import static org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode.ALWAYS;
-import static org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode.NEVER;
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
 
 @Beta
 public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements SaltConfig {
@@ -165,10 +166,11 @@
         SaltHighstate.applyHighstate(stateDescription, entity());
     }
 
-
+    @Override
     protected void postStartCustom() {
         // TODO: check for package installed?
         entity().sensors().set(SoftwareProcess.SERVICE_UP, true);
+        super.postStartCustom();
     }
 
 
diff --git a/software/cm/salt/src/main/resources/catalog.bom b/software/cm/salt/src/main/resources/catalog.bom
index afe09ab..acd9556 100644
--- a/software/cm/salt/src/main/resources/catalog.bom
+++ b/software/cm/salt/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.cm.salt.SaltEntity
diff --git a/software/database/pom.xml b/software/database/pom.xml
index faf5a8d..b2818d6 100644
--- a/software/database/pom.xml
+++ b/software/database/pom.xml
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNode.java b/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNode.java
index 3c78d99..bd06c6f 100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNode.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNode.java
@@ -42,7 +42,7 @@
 
     @SetFromFlag("version")
     public static final ConfigKey<String> SUGGESTED_VERSION =
-        ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "5.5.40");
+        ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "5.5.53");
 
     // https://downloads.mariadb.org/interstitial/mariadb-5.5.33a/kvm-bintar-hardy-amd64/mariadb-5.5.33a-linux-x86_64.tar.gz/from/http://mirrors.coreix.net/mariadb
     // above redirects to download the artifactd from the URLs below.
@@ -57,7 +57,7 @@
     /** download mirror, if desired */
     @SetFromFlag("mirrorUrl")
     public static final ConfigKey<String> MIRROR_URL = ConfigKeys.newStringConfigKey("mariadb.install.mirror.url", "URL of mirror",
-        "http://mirrors.coreix.net/mariadb/"
+        "http://mirrors.coreix.net/mariadb"
      );
 
     @SetFromFlag("port")
diff --git a/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNodeImpl.java b/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNodeImpl.java
index 86a8bfb..4c960a6 100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNodeImpl.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/mariadb/MariaDbNodeImpl.java
@@ -78,7 +78,7 @@
             String cmd = getDriver().getStatusCmd();
             feed = SshFeed.builder()
                     .entity(this)
-                    .period(Duration.FIVE_SECONDS)
+                    .period(config().get(SERVICE_PROCESS_IS_RUNNING_POLL_PERIOD))
                     .machine(machine.get())
                     .poll(new SshPollConfig<Boolean>(SERVICE_UP)
                             .command(cmd)
diff --git a/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlClusterImpl.java b/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlClusterImpl.java
index d356fc5..cbc0272 100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlClusterImpl.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlClusterImpl.java
@@ -29,7 +29,6 @@
 import javax.annotation.Nullable;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
@@ -205,11 +204,10 @@
     protected Entity createNode(Location loc, Map<?, ?> flags) {
         MySqlNode node = (MySqlNode) super.createNode(loc, flags);
         if (!MySqlClusterUtils.IS_MASTER.apply(node)) {
-            EntityLocal localNode = (EntityLocal) node;
-            ServiceNotUpLogic.updateNotUpIndicator(localNode, MySqlSlave.SLAVE_HEALTHY, "Replication not started");
+            ServiceNotUpLogic.updateNotUpIndicator(node, MySqlSlave.SLAVE_HEALTHY, "Replication not started");
 
             addFeed(FunctionFeed.builder()
-                .entity(localNode)
+                .entity(node)
                 .period(Duration.FIVE_SECONDS)
                 .poll(FunctionPollConfig.forSensor(MySqlSlave.SLAVE_HEALTHY)
                         .callable(new SlaveStateCallable(node))
@@ -310,7 +308,6 @@
                 DynamicTasks.submitTopLevelTask(TaskBuilder.builder()
                         .displayName("setup master-slave replication")
                         .body(nodeInitTaskBody)
-                        .tag(BrooklynTaskTags.tagForContextEntity(node))
                         .tag(BrooklynTaskTags.NON_TRANSIENT_TASK_TAG)
                         .build(),
                         node);
diff --git a/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlNodeImpl.java b/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlNodeImpl.java
index 8a4c177..d71d5e1 100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlNodeImpl.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/mysql/MySqlNodeImpl.java
@@ -107,7 +107,7 @@
             String cmd = getDriver().getStatusCmd();
             feed = SshFeed.builder()
                     .entity(this)
-                    .period(Duration.FIVE_SECONDS)
+                    .period(config().get(SERVICE_PROCESS_IS_RUNNING_POLL_PERIOD))
                     .machine(machine.get())
                     .poll(new SshPollConfig<Double>(QUERIES_PER_SECOND_FROM_MYSQL)
                             .command(cmd)
diff --git a/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeChefImplFromScratch.java b/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeChefImplFromScratch.java
index 96e65a5..0c52f20 100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeChefImplFromScratch.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeChefImplFromScratch.java
@@ -32,8 +32,6 @@
 import org.apache.brooklyn.entity.stock.EffectorStartableImpl;
 import org.apache.brooklyn.feed.ssh.SshFeed;
 import org.apache.brooklyn.feed.ssh.SshPollConfig;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.util.collections.Jsonya;
 import org.apache.brooklyn.util.core.ResourceUtils;
@@ -41,6 +39,8 @@
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.ssh.BashCommands;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class PostgreSqlNodeChefImplFromScratch extends EffectorStartableImpl implements PostgreSqlNode {
 
@@ -77,6 +77,7 @@
             usePidFile("/var/run/postgresql/*.pid");
             useService("postgresql");
         }
+        @Override
         protected void startWithKnifeAsync() {
             Entities.warnOnIgnoringConfig(entity(), ChefConfig.CHEF_LAUNCH_RUN_LIST);
             Entities.warnOnIgnoringConfig(entity(), ChefConfig.CHEF_LAUNCH_ATTRIBUTES);
@@ -95,9 +96,8 @@
                         // no other arguments currenty supported; chef will pick a password for us
                 );
         }
+        @Override
         protected void postStartCustom() {
-            super.postStartCustom();
-
             // now run the creation script
             String creationScript;
             String creationScriptUrl = entity().getConfig(PostgreSqlNode.CREATION_SCRIPT_URL);
@@ -110,11 +110,14 @@
 
             // and finally connect sensors
             entity().connectSensors();
+            super.postStartCustom();
         }
+        @Override
         protected void preStopCustom() {
             entity().disconnectSensors();
             super.preStopCustom();
         }
+        @Override
         protected PostgreSqlNodeChefImplFromScratch entity() {
             return (PostgreSqlNodeChefImplFromScratch) super.entity();
         }
diff --git a/software/database/src/main/resources/catalog.bom b/software/database/src/main/resources/catalog.bom
index 7d65e6b..6f24a9e 100644
--- a/software/database/src/main/resources/catalog.bom
+++ b/software/database/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.database.crate.CrateNode
diff --git a/software/messaging/pom.xml b/software/messaging/pom.xml
index d9984be..bf80c61 100644
--- a/software/messaging/pom.xml
+++ b/software/messaging/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>org.apache.brooklyn</groupId>
 		<artifactId>brooklyn-library</artifactId>
-		<version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+		<version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
 		<relativePath>../../pom.xml</relativePath>
 	</parent>
 
@@ -219,9 +219,22 @@
                     <groupId>storm</groupId>
                     <artifactId>carbonite</artifactId>
                 </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>log4j-over-slf4j</artifactId>
+                </exclusion>
             </exclusions>
         </dependency>
 
+        <dependency>
+            <!-- We exclude jsr311-api transitive dependency from jclouds (for javax.ws.rs) due to version conflict.
+                 Therefore explicitly bring this one in.
+            -->
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>javax.ws.rs-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <!-- Transitive dependencies, declared explicitly due to version mismatch -->
         <dependency>
             <groupId>log4j</groupId>
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/activemq/ActiveMQDestinationImpl.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/activemq/ActiveMQDestinationImpl.java
index 6a27030..559a193 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/activemq/ActiveMQDestinationImpl.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/activemq/ActiveMQDestinationImpl.java
@@ -21,9 +21,6 @@
 import javax.management.MalformedObjectNameException;
 import javax.management.ObjectName;
 
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
-
 import com.google.common.base.Preconditions;
 
 import org.apache.brooklyn.entity.messaging.jms.JMSDestinationImpl;
@@ -48,7 +45,7 @@
 
         try {
             brokerMBeanName = new ObjectName("org.apache.activemq:type=Broker,brokerName=" + brokerName);
-            jmxHelper = new JmxHelper((EntityLocal) getParent());
+            jmxHelper = new JmxHelper(getParent());
         } catch (MalformedObjectNameException e) {
             throw Exceptions.propagate(e);
         }
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/qpid/QpidDestinationImpl.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/qpid/QpidDestinationImpl.java
index d475c5e..b514321 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/qpid/QpidDestinationImpl.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/messaging/qpid/QpidDestinationImpl.java
@@ -25,7 +25,6 @@
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.entity.java.UsesJmx;
 import org.apache.brooklyn.entity.messaging.amqp.AmqpServer;
 import org.apache.brooklyn.entity.messaging.jms.JMSDestinationImpl;
@@ -63,7 +62,7 @@
             if (virtualHost == null) virtualHost = getConfig(QpidBroker.VIRTUAL_HOST_NAME);
             sensors().set(QpidBroker.VIRTUAL_HOST_NAME, virtualHost);
             virtualHostManager = new ObjectName(format("org.apache.qpid:type=VirtualHost.VirtualHostManager,VirtualHost=\"%s\"", virtualHost));
-            jmxHelper = new JmxHelper((EntityLocal)getParent());
+            jmxHelper = new JmxHelper(getParent());
         } catch (MalformedObjectNameException e) {
             throw Exceptions.propagate(e);
         }
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/AbstractZooKeeperImpl.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/AbstractZooKeeperImpl.java
index 60175c9..b3ced27 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/AbstractZooKeeperImpl.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/AbstractZooKeeperImpl.java
@@ -94,9 +94,9 @@
 
     @Override
     public void disconnectSensors() {
-        super.disconnectSensors();
-        disconnectServiceUpIsRunning();
         if (jmxFeed != null) jmxFeed.stop();
+        disconnectServiceUpIsRunning();
+        super.disconnectSensors();
     }
 
     @Override
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsemble.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsemble.java
index a5ba570..487d639 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsemble.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsemble.java
@@ -29,7 +29,10 @@
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.entity.group.DynamicCluster;
 import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.apache.brooklyn.util.guava.Suppliers;
 
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
 import com.google.common.reflect.TypeToken;
 
 @Catalog(name="ZooKeeper ensemble", description="A cluster of ZooKeeper servers. "
@@ -38,15 +41,26 @@
 public interface ZooKeeperEnsemble extends DynamicCluster {
 
     @SetFromFlag("clusterName")
-    BasicAttributeSensorAndConfigKey<String> CLUSTER_NAME = new BasicAttributeSensorAndConfigKey<String>(String
-            .class, "zookeeper.cluster.name", "Name of the Zookeeper cluster", "BrooklynZookeeperCluster");
+    BasicAttributeSensorAndConfigKey<String> CLUSTER_NAME = new BasicAttributeSensorAndConfigKey<String>(String.class,
+            "zookeeper.cluster.name", "Name of the Zookeeper cluster", "BrooklynZookeeperCluster");
 
     @SetFromFlag("initialSize")
-    public static final ConfigKey<Integer> INITIAL_SIZE = ConfigKeys.newConfigKeyWithDefault(DynamicCluster.INITIAL_SIZE, 3);
+    ConfigKey<Integer> INITIAL_SIZE = ConfigKeys.newConfigKeyWithDefault(DynamicCluster.INITIAL_SIZE, 3);
 
-    @SuppressWarnings("serial")
+    ConfigKey<Supplier<Integer>> NODE_ID_SUPPLIER = ConfigKeys.builder(new TypeToken<Supplier<Integer>>() {})
+            .name("zookeeper.nodeId.supplier")
+            .description("Supplies values for members id in zoo.cfg")
+            .defaultValue(Suppliers.incrementing())
+            .constraint(Predicates.notNull())
+            .build();
+
     AttributeSensor<List<String>> ZOOKEEPER_SERVERS = Sensors.newSensor(new TypeToken<List<String>>() { },
             "zookeeper.servers", "Hostnames to connect to cluster with");
 
+    AttributeSensor<String> ZOOKEEPER_ENDPOINTS = Sensors.newStringSensor(
+            "zookeeper.endpoints", "A comma-separated host:port list of members of the ensemble");
+
+    /** @deprecated since 0.10.0 use <code>sensors().get(ZooKeeperEnsemble.CLUSTER_NAME)</code> instead */
+    @Deprecated
     String getClusterName();
 }
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsembleImpl.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsembleImpl.java
index c2c3e3f..2588921 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsembleImpl.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperEnsembleImpl.java
@@ -18,38 +18,31 @@
  */
 package org.apache.brooklyn.entity.zookeeper;
 
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.api.sensor.EnricherSpec;
 import org.apache.brooklyn.core.entity.Attributes;
-import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.enricher.stock.Enrichers;
 import org.apache.brooklyn.entity.group.AbstractMembershipTrackingPolicy;
 import org.apache.brooklyn.entity.group.DynamicClusterImpl;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.util.guava.Suppliers;
 
-import com.google.common.collect.Lists;
+import com.google.common.base.Supplier;
 
 public class ZooKeeperEnsembleImpl extends DynamicClusterImpl implements ZooKeeperEnsemble {
 
-    private static final Logger log = LoggerFactory.getLogger(ZooKeeperEnsembleImpl.class);
-    private static final AtomicInteger myId = new AtomicInteger();
-    
-    private MemberTrackingPolicy policy;
-
     public ZooKeeperEnsembleImpl() {}
 
     /**
      * Sets the default {@link #MEMBER_SPEC} to describe the ZooKeeper nodes.
+     * Overwrites any value configured for {@link ZooKeeperNode#MY_ID} to use
+     * the value given by {@link ZooKeeperEnsemble#NODE_ID_SUPPLIER}.
      */
     @Override
     protected EntitySpec<?> getMemberSpec() {
-        return getConfig(MEMBER_SPEC, EntitySpec.create(ZooKeeperNode.class));
+        EntitySpec<?> spec = getConfig(MEMBER_SPEC, EntitySpec.create(ZooKeeperNode.class));
+        spec.configure(ZooKeeperNode.MY_ID, config().get(ZooKeeperEnsemble.NODE_ID_SUPPLIER).get());
+        return spec;
     }
 
     @Override
@@ -58,47 +51,48 @@
     }
 
     @Override
-    public void init() {
-        log.info("Initializing the ZooKeeper Ensemble");
-        super.init();
-
-        policy = policies().add(PolicySpec.create(MemberTrackingPolicy.class)
-                .displayName("Members tracker")
-                .configure("group", this));
+    protected void initEnrichers() {
+        super.initEnrichers();
+        EnricherSpec<?> zks = Enrichers.builder()
+                .aggregating(ZooKeeperNode.ZOOKEEPER_ENDPOINT)
+                .publishing(ZOOKEEPER_SERVERS)
+                .fromMembers()
+                .build();
+        EnricherSpec<?> zke = Enrichers.builder()
+                .joining(ZOOKEEPER_SERVERS)
+                .publishing(ZOOKEEPER_ENDPOINTS)
+                .quote(false)
+                .separator(",")
+                .build();
+        enrichers().add(zks);
+        enrichers().add(zke);
     }
 
-    public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy {
-        @Override
-        protected void onEntityChange(Entity member) {
-        }
+    /**
+     * @deprecated since 0.10.0 class is unused but kept for persistence backwards compatibility
+     */
+    @Deprecated
+    private static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy {
+        private final Object[] mutex = new Object[0];
 
         @Override
         protected void onEntityAdded(Entity member) {
-            if (member.getAttribute(ZooKeeperNode.MY_ID) == null) {
-                ((EntityInternal) member).sensors().set(ZooKeeperNode.MY_ID, myId.incrementAndGet());
+            if (member.config().get(ZooKeeperNode.MY_ID) == null) {
+                Supplier<Integer> id;
+                synchronized (mutex) {
+                    // Entities may not have been created with NODE_ID_SUPPLIER, so create it if
+                    // it's not there. We can't provide any good guarantees about what number to
+                    // start with, but then again the previous version of the entity gave no
+                    // guarantee either.
+                    id = entity.config().get(ZooKeeperEnsemble.NODE_ID_SUPPLIER);
+                    if (id == null) {
+                        id = Suppliers.incrementing();
+                        entity.config().set(ZooKeeperEnsemble.NODE_ID_SUPPLIER, id);
+                    }
+                }
+                member.config().set(ZooKeeperNode.MY_ID, id.get());
             }
         }
-
-        @Override
-        protected void onEntityRemoved(Entity member) {
-        }
-    };
-
-    @Override
-    protected void initEnrichers() {
-        super.initEnrichers();
-        
-    }
-    
-    @Override
-    public void start(Collection<? extends Location> locations) {
-        super.start(locations);
-        
-        List<String> zookeeperServers = Lists.newArrayList();
-        for (Entity zookeeper : getMembers()) {
-            zookeeperServers.add(zookeeper.getAttribute(Attributes.HOSTNAME));
-        }
-        sensors().set(ZOOKEEPER_SERVERS, zookeeperServers);
     }
 
 }
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNode.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNode.java
index 0dce3b7..54ed432 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNode.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNode.java
@@ -24,6 +24,7 @@
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.core.sensor.BasicAttributeSensorAndConfigKey;
 import org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
@@ -38,14 +39,14 @@
 public interface ZooKeeperNode extends SoftwareProcess {
 
     @SetFromFlag("version")
-    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "3.4.5");
+    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "3.4.9");
 
     @SetFromFlag("archiveNameFormat")
     ConfigKey<String> ARCHIVE_DIRECTORY_NAME_FORMAT = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.ARCHIVE_DIRECTORY_NAME_FORMAT, "zookeeper-%s");
 
     @SetFromFlag("downloadUrl")
     AttributeSensorAndConfigKey<String, String> DOWNLOAD_URL = ConfigKeys.newSensorAndConfigKeyWithDefault(SoftwareProcess.DOWNLOAD_URL,
-            "http://apache.fastbull.org/zookeeper/zookeeper-${version}/zookeeper-${version}.tar.gz");
+            "http://apache.org/dyn/closer.cgi?action=download&filename=zookeeper/zookeeper-${version}/zookeeper-${version}.tar.gz");
 
     @SetFromFlag("zookeeperPort")
     PortAttributeSensorAndConfigKey ZOOKEEPER_PORT = ConfigKeys.newPortSensorAndConfigKey("zookeeper.port", "Zookeeper port", "2181+");
@@ -64,12 +65,22 @@
             "zookeeper.configTemplate", "Zookeeper configuration template (in freemarker format)",
             "classpath://org/apache/brooklyn/entity/messaging/zookeeper/zoo.cfg");
 
+    @SetFromFlag("zookeeperId")
+    BasicAttributeSensorAndConfigKey<Integer> MY_ID = new BasicAttributeSensorAndConfigKey<>(Integer.class,
+            "zookeeper.myid", "ZooKeeper node's myId", 1);
+
     AttributeSensor<Long> OUTSTANDING_REQUESTS = Sensors.newLongSensor("zookeeper.outstandingRequests", "Outstanding request count");
     AttributeSensor<Long> PACKETS_RECEIVED = Sensors.newLongSensor("zookeeper.packets.received", "Total packets received");
     AttributeSensor<Long> PACKETS_SENT = Sensors.newLongSensor("zookeeper.packets.sent", "Total packets sent");
-    AttributeSensor<Integer> MY_ID = Sensors.newIntegerSensor("zookeeper.myid", "ZooKeeper node's myId");
 
+    AttributeSensor<String> ZOOKEEPER_ENDPOINT = Sensors.newStringSensor(
+            "zookeeper.endpoint", "The host:port of the node");
+
+    /** @deprecated since 0.10.0 use <code>sensors().get(ZooKeeperNode.ZOOKEEPER_PORT)</code> instead */
+    @Deprecated
     Integer getZookeeperPort();
 
+    /** @deprecated since 0.10.0 use <code>sensors().get(ZooKeeperNode.HOSTNAME)</code> instead */
+    @Deprecated
     String getHostname();
 }
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNodeImpl.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNodeImpl.java
index 275e101..718e360 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNodeImpl.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperNodeImpl.java
@@ -18,6 +18,13 @@
  */
 package org.apache.brooklyn.entity.zookeeper;
 
+import java.net.URI;
+
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.location.access.BrooklynAccessUtils;
+
+import com.google.common.net.HostAndPort;
+
 /**
  * An {@link org.apache.brooklyn.api.entity.Entity} that represents a single standalone zookeeper instance.
  */
@@ -26,8 +33,27 @@
     public ZooKeeperNodeImpl() {}
 
     @Override
+    public void init() {
+        super.init();
+        // MY_ID was changed from a sensor to config. Publish it as a sensor to maintain
+        // compatibility with any blueprints that reference it.
+        Integer myId = config().get(MY_ID);
+        if (myId == null) {
+            throw new NullPointerException("Require value for " + MY_ID.getName());
+        }
+        sensors().set(MY_ID, myId);
+    }
+
+    @Override
     public Class<?> getDriverInterface() {
         return ZooKeeperDriver.class;
     }
 
+    @Override
+    protected void postStart() {
+        super.postStart();
+        HostAndPort hap = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, sensors().get(ZOOKEEPER_PORT));
+        sensors().set(ZooKeeperNode.ZOOKEEPER_ENDPOINT, hap.toString());
+        sensors().set(Attributes.MAIN_URI, URI.create("zk://" +hap.toString()));
+    }
 }
diff --git a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperSshDriver.java b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperSshDriver.java
index 87bf3ff..c7b1dc9 100644
--- a/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperSshDriver.java
+++ b/software/messaging/src/main/java/org/apache/brooklyn/entity/zookeeper/ZooKeeperSshDriver.java
@@ -54,25 +54,32 @@
         return entity.getConfig(ZooKeeperNode.ZOOKEEPER_CONFIG_TEMPLATE);
     }
 
-    protected int getMyId() {
-        return entity.getAttribute(ZooKeeperNode.MY_ID);
+    protected Integer getMyId() {
+        return entity.config().get(ZooKeeperNode.MY_ID);
     }
 
     // FIXME All for one, and one for all! If any node fails then we're stuck waiting for its hostname/port forever.
     // Need a way to terminate the wait based on the entity going on-fire etc.
     // FIXME Race in getMemebers. Should we change DynamicCluster.grow to create the members and only then call start on them all?
     public List<ZooKeeperServerConfig> getZookeeperServers() throws ExecutionException, InterruptedException {
-        ZooKeeperEnsemble ensemble = (ZooKeeperEnsemble) entity.getParent();
         List<ZooKeeperServerConfig> result = Lists.newArrayList();
 
-        for (Entity member : ensemble.getMembers()) {
-            Integer myid = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.MY_ID).get();
-            String hostname = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.HOSTNAME).get();
-            Integer port = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.ZOOKEEPER_PORT).get();
-            Integer leaderPort = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.ZOOKEEPER_LEADER_PORT).get();
-            Integer electionPort = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.ZOOKEEPER_ELECTION_PORT).get();
-            result.add(new ZooKeeperServerConfig(myid, hostname, port, leaderPort, electionPort));
+        if (entity.getParent() instanceof ZooKeeperEnsemble) {
+            ZooKeeperEnsemble ensemble = (ZooKeeperEnsemble) entity.getParent();
+
+            for (Entity member : ensemble.getMembers()) {
+                Integer memberId = member.config().get(ZooKeeperNode.MY_ID);
+                if (memberId == null) {
+                    throw new IllegalStateException(member + " has null value for " + ZooKeeperNode.MY_ID);
+                }
+                String hostname = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.HOSTNAME).get();
+                Integer port = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.ZOOKEEPER_PORT).get();
+                Integer leaderPort = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.ZOOKEEPER_LEADER_PORT).get();
+                Integer electionPort = Entities.attributeSupplierWhenReady(member, ZooKeeperNode.ZOOKEEPER_ELECTION_PORT).get();
+                result.add(new ZooKeeperServerConfig(memberId, hostname, port, leaderPort, electionPort));
+            }
         }
+
         return result;
     }
 
diff --git a/software/messaging/src/main/resources/catalog.bom b/software/messaging/src/main/resources/catalog.bom
index ec884ca..8209cc6 100644
--- a/software/messaging/src/main/resources/catalog.bom
+++ b/software/messaging/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.messaging.activemq.ActiveMQBroker
diff --git a/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEc2LiveTest.java b/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEc2LiveTest.java
index f3557d1..5a435f0 100644
--- a/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEc2LiveTest.java
+++ b/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEc2LiveTest.java
@@ -18,16 +18,18 @@
  */
 package org.apache.brooklyn.entity.messaging.zookeeper;
 
+import static org.testng.Assert.assertEquals;
+
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.entity.trait.Startable;
-import org.testng.annotations.Test;
 import org.apache.brooklyn.entity.AbstractEc2LiveTest;
 import org.apache.brooklyn.entity.zookeeper.ZooKeeperNode;
+import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.net.HostAndPort;
 
 public class ZooKeeperEc2LiveTest extends AbstractEc2LiveTest {
 
@@ -36,10 +38,17 @@
      */
     @Override
     protected void doTest(Location loc) throws Exception {
-        ZooKeeperNode zookeeper = app.createAndManageChild(EntitySpec.create(ZooKeeperNode.class).configure("jmxPort", "31001+"));
+        ZooKeeperNode zookeeper = app.createAndManageChild(EntitySpec.create(ZooKeeperNode.class)
+                .configure("jmxPort", "31001+"));
         app.start(ImmutableList.of(loc));
-        Entities.dumpInfo(zookeeper);
         EntityAsserts.assertAttributeEqualsEventually(zookeeper, Startable.SERVICE_UP, true);
+        HostAndPort conn = HostAndPort.fromParts(
+                zookeeper.sensors().get(ZooKeeperNode.HOSTNAME),
+                zookeeper.sensors().get(ZooKeeperNode.ZOOKEEPER_PORT));
+        try (ZooKeeperTestSupport zkts = new ZooKeeperTestSupport(conn)) {
+            zkts.create("/ec2livetest", "data".getBytes());
+            assertEquals(new String(zkts.get("/ec2livetest")), "data");
+        }
     }
     
     @Test(enabled=false)
diff --git a/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEnsembleLiveTest.java b/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEnsembleLiveTest.java
index 2721976..52b4d08 100644
--- a/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEnsembleLiveTest.java
+++ b/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperEnsembleLiveTest.java
@@ -18,33 +18,39 @@
  */
 package org.apache.brooklyn.entity.messaging.zookeeper;
 
-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.factory.ApplicationBuilder;
-import org.apache.brooklyn.core.entity.trait.Startable;
-import org.apache.brooklyn.core.test.entity.TestApplication;
-import org.apache.brooklyn.entity.zookeeper.ZooKeeperEnsemble;
-import org.apache.brooklyn.entity.zookeeper.ZooKeeperNode;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
 
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Uninterruptibles;
+import java.util.List;
+import java.util.Set;
 
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityAsserts;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
+import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.entity.zookeeper.ZooKeeperEnsemble;
+import org.apache.brooklyn.entity.zookeeper.ZooKeeperNode;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Optional;
+import org.testng.annotations.Parameters;
 import org.testng.annotations.Test;
 
-import java.net.Socket;
-import java.util.concurrent.TimeUnit;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.net.HostAndPort;
 
 /**
  * A live test of the {@link org.apache.brooklyn.entity.zookeeper.ZooKeeperEnsemble} entity.
@@ -52,29 +58,28 @@
  * Tests that a 3 node cluster can be started on Amazon EC2 and data written on one {@link org.apache.brooklyn.entity.zookeeper.ZooKeeperEnsemble}
  * can be read from another, using the Astyanax API.
  */
-public class ZooKeeperEnsembleLiveTest {
+public class ZooKeeperEnsembleLiveTest extends BrooklynAppLiveTestSupport {
 
     private static final Logger log = LoggerFactory.getLogger(ZooKeeperEnsembleLiveTest.class);
-    
-    private String provider = 
-            "gce-europe-west1";
-//            "aws-ec2:eu-west-1";
-//            "named:hpcloud-compute-at";
-//            "localhost";
+    private static final String DEFAULT_LOCATION = "jclouds:aws-ec2:eu-west-1";
 
-    protected TestApplication app;
-    protected Location testLocation;
-    protected ZooKeeperEnsemble cluster;
+    private Location testLocation;
+    private String locationSpec;
 
-    @BeforeMethod(alwaysRun = true)
-    public void setup() {
-        app = ApplicationBuilder.newManagedApp(TestApplication.class);
-        testLocation = app.getManagementContext().getLocationRegistry().getLocationManaged(provider);
+    @BeforeClass(alwaysRun = true)
+    @Parameters({"locationSpec"})
+    public void setLocationSpec(@Optional String locationSpec) {
+        this.locationSpec = !Strings.isBlank(locationSpec)
+                            ? locationSpec
+                            : DEFAULT_LOCATION;
+        log.info("Running {} with in {}", this, this.locationSpec);
     }
 
-    @AfterMethod(alwaysRun = true)
-    public void shutdown() {
-        Entities.destroyAll(app.getManagementContext());
+    @Override
+    @BeforeMethod(alwaysRun = true)
+    public void setUp() throws Exception {
+        super.setUp();
+        testLocation = app.getManagementContext().getLocationRegistry().getLocationManaged(locationSpec);
     }
 
     /**
@@ -82,46 +87,61 @@
      */
     @Test(groups = "Live")
     public void testStartUpConnectAndResize() throws Exception {
-        try {
-            cluster = app.createAndManageChild(EntitySpec.create(ZooKeeperEnsemble.class)
-                    .configure("initialSize", 3)
-                    .configure("clusterName", "ZooKeeperEnsembleLiveTest"));
-            assertEquals(cluster.getCurrentSize().intValue(), 0);
+        final String zkDataPath = "/ensembletest";
+        final int initialSize = 3;
+        ZooKeeperEnsemble ensemble = app.createAndManageChild(EntitySpec.create(ZooKeeperEnsemble.class)
+                .configure(DynamicCluster.INITIAL_SIZE, initialSize)
+                .configure(ZooKeeperEnsemble.CLUSTER_NAME, "ZooKeeperEnsembleLiveTest"));
 
-            app.start(ImmutableList.of(testLocation));
+        app.start(ImmutableList.of(testLocation));
+        Entities.dumpInfo(app);
 
-            EntityAsserts.assertAttributeEqualsEventually(cluster, ZooKeeperEnsemble.GROUP_SIZE, 3);
-            Entities.dumpInfo(app);
+        EntityAsserts.assertAttributeEqualsEventually(ensemble, ZooKeeperEnsemble.GROUP_SIZE, 3);
+        EntityAsserts.assertAttributeEqualsEventually(ensemble, Startable.SERVICE_UP, true);
+        assertNotNull(ensemble.sensors().get(ZooKeeperEnsemble.ZOOKEEPER_ENDPOINTS),
+                "expected value for " + ZooKeeperEnsemble.ZOOKEEPER_ENDPOINTS + " on " + ensemble + ", was null");
+        Set<Integer> nodeIds = Sets.newHashSet();
+        for (Entity zkNode : ensemble.getMembers()) {
+            nodeIds.add(zkNode.config().get(ZooKeeperNode.MY_ID));
+        }
+        assertEquals(nodeIds.size(), initialSize, "expected " + initialSize + " node ids, found " + Iterables.toString(nodeIds));
 
-            EntityAsserts.assertAttributeEqualsEventually(cluster, Startable.SERVICE_UP, true);
-            for(Entity zkNode : cluster.getMembers()) {
-                assertTrue(isSocketOpen((ZooKeeperNode) zkNode));
-            }
-            cluster.resize(1);
-            EntityAsserts.assertAttributeEqualsEventually(cluster, ZooKeeperEnsemble.GROUP_SIZE, 1);
-            Entities.dumpInfo(app);
-            EntityAsserts.assertAttributeEqualsEventually(cluster, Startable.SERVICE_UP, true);
-            for (Entity zkNode : cluster.getMembers()) {
-                assertTrue(isSocketOpen((ZooKeeperNode) zkNode));
-            }
-        } catch (Throwable e) {
-            throw Throwables.propagate(e);
+        // Write data to one and read from the others.
+        List<String> servers = ensemble.sensors().get(ZooKeeperEnsemble.ZOOKEEPER_SERVERS);
+        assertNotNull(servers, "value for sensor should not be null: " + ZooKeeperEnsemble.ZOOKEEPER_SERVERS);
+        assertEquals(servers.size(), initialSize, "expected " + initialSize + " entries in " + servers);
+
+        // Write to one
+        String firstServer = servers.get(0);
+        HostAndPort conn = HostAndPort.fromString(firstServer);
+        log.info("Writing data to {}", conn);
+        try (ZooKeeperTestSupport zkts = new ZooKeeperTestSupport(conn)) {
+            zkts.create(zkDataPath, "data".getBytes());
+            assertEquals(new String(zkts.get(zkDataPath)), "data");
+        }
+
+        // And read from the others.
+        for (int i = 1; i < servers.size(); i++) {
+            conn = HostAndPort.fromString(servers.get(i));
+            log.info("Asserting that data can be read from {}", conn);
+            assertPathDataEventually(conn, zkDataPath, "data");
         }
     }
 
-    protected static boolean isSocketOpen(ZooKeeperNode node) {
-        int attempt = 0, maxAttempts = 20;
-        while(attempt < maxAttempts) {
-            try {
-                Socket s = new Socket(node.getAttribute(Attributes.HOSTNAME), node.getZookeeperPort());
-                s.close();
-                return true;
-            } catch (Exception e) {
-                attempt++;
-            }
-            Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+    protected void assertPathDataEventually(HostAndPort hostAndPort, final String path, String expected) throws Exception {
+        try (ZooKeeperTestSupport zkts = new ZooKeeperTestSupport(hostAndPort)) {
+            final Supplier<String> dataSupplier = new Supplier<String>() {
+                @Override
+                public String get() {
+                    try {
+                        return new String(zkts.get(path));
+                    } catch (Exception e) {
+                        throw Exceptions.propagate(e);
+                    }
+                }
+            };
+            Asserts.eventually(dataSupplier, Predicates.equalTo(expected));
         }
-        return false;
     }
-    
+
 }
diff --git a/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperTestSupport.java b/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperTestSupport.java
new file mode 100644
index 0000000..f1987d2
--- /dev/null
+++ b/software/messaging/src/test/java/org/apache/brooklyn/entity/messaging/zookeeper/ZooKeeperTestSupport.java
@@ -0,0 +1,88 @@
+/*
+ * 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.entity.messaging.zookeeper;
+
+import java.io.Closeable;
+import java.util.concurrent.CountDownLatch;
+
+import org.apache.brooklyn.location.paas.PaasLocation;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.ZooKeeper;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.net.HostAndPort;
+
+/**
+ * Useful methods for writing to and reading from ZooKeeper nodes.
+ */
+public class ZooKeeperTestSupport implements Closeable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperTestSupport.class);
+    private final ZooKeeper zk;
+    private final CountDownLatch connSignal = new CountDownLatch(1);
+
+    public ZooKeeperTestSupport(final HostAndPort hostAndPort) throws Exception {
+        final int sessionTimeout = 3000;
+        zk = new ZooKeeper(hostAndPort.toString(), sessionTimeout, new Watcher() {
+            @Override
+            public void process(WatchedEvent event) {
+                if (event.getState() == Event.KeeperState.SyncConnected) {
+                    LOG.debug("Connected to ZooKeeper at {}", hostAndPort);
+                    connSignal.countDown();
+                } else {
+                    LOG.info("WatchedEvent at {}: {}", hostAndPort, event.getState());
+                }
+            }
+        });
+        connSignal.await();
+    }
+
+    @Override
+    public void close() {
+        try {
+            zk.close();
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    public String create(String path, byte[] data) throws Exception {
+        return zk.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+    }
+
+    public Stat update(String path, byte[] data) throws Exception {
+        return zk.setData(path, data, zk.exists(path, true).getVersion());
+    }
+
+    public void delete(String path) throws Exception {
+        zk.delete(path, zk.exists(path, true).getVersion());
+    }
+
+    public byte[] get(String path) throws Exception {
+        return zk.getData(path, false, zk.exists(path, false));
+    }
+
+}
diff --git a/software/monitoring/pom.xml b/software/monitoring/pom.xml
index 931c79e..901df83 100644
--- a/software/monitoring/pom.xml
+++ b/software/monitoring/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/monitoring/src/main/java/org/apache/brooklyn/entity/monitoring/monit/MonitNodeImpl.java b/software/monitoring/src/main/java/org/apache/brooklyn/entity/monitoring/monit/MonitNodeImpl.java
index 2702994..f3b2863 100644
--- a/software/monitoring/src/main/java/org/apache/brooklyn/entity/monitoring/monit/MonitNodeImpl.java
+++ b/software/monitoring/src/main/java/org/apache/brooklyn/entity/monitoring/monit/MonitNodeImpl.java
@@ -71,7 +71,7 @@
             String cmd = getDriver().getStatusCmd();
             feed = SshFeed.builder()
                 .entity(this)
-                .period(Duration.FIVE_SECONDS)
+                .period(config().get(SERVICE_PROCESS_IS_RUNNING_POLL_PERIOD))
                 .machine((SshMachineLocation) machine)
                 .poll(new SshPollConfig<Boolean>(SERVICE_UP)
                     .command(cmd)
diff --git a/software/monitoring/src/main/resources/catalog.bom b/software/monitoring/src/main/resources/catalog.bom
index 5388ae0..4ecc2a6 100644
--- a/software/monitoring/src/main/resources/catalog.bom
+++ b/software/monitoring/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.monitoring.monit.MonitNode
diff --git a/software/network/pom.xml b/software/network/pom.xml
index 6f7a1bb..cc08b7a 100644
--- a/software/network/pom.xml
+++ b/software/network/pom.xml
@@ -28,7 +28,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerImpl.java b/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerImpl.java
index ef53bc4..a210e55 100644
--- a/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerImpl.java
+++ b/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerImpl.java
@@ -105,6 +105,7 @@
     public void init() {
         super.init();
         checkNotNull(getConfig(HOSTNAME_SENSOR), "%s requires value for %s", getClass().getName(), HOSTNAME_SENSOR);
+        checkNotNull(getConfig(ADDRESS_SENSOR), "%s requires value for %s", getClass().getName(), ADDRESS_SENSOR);
         DynamicGroup entities = addChild(EntitySpec.create(DynamicGroup.class)
                 .displayName("BIND-managed entities")
                 .configure(DynamicGroup.ENTITY_FILTER, getEntityFilter()));
diff --git a/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerSshDriver.java b/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerSshDriver.java
index ad0aaff..150cfb9 100644
--- a/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerSshDriver.java
+++ b/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindDnsServerSshDriver.java
@@ -76,7 +76,7 @@
 
         List<String> commands = Lists.newArrayList(
                 BashCommands.sudo("mkdir -p " + getDataDirectory() + " " + getDynamicDirectory() + " " + getOsSupport().getConfigDirectory()),
-                BashCommands.sudo("chown -R bind:bind " + getDataDirectory() + " " + getDynamicDirectory()),
+                BashCommands.sudo("chown -R " + getOsSupport().getUser() + ":" + getOsSupport().getUser() + " " + getDataDirectory() + " " + getDynamicDirectory()),
                 // TODO determine name of ethernet interface if not eth0?
                 IptablesCommands.insertIptablesRule(Chain.INPUT, "eth0", Protocol.UDP, dnsPort, Policy.ACCEPT),
                 IptablesCommands.insertIptablesRule(Chain.INPUT, "eth0", Protocol.TCP, dnsPort, Policy.ACCEPT),
diff --git a/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindOsSupport.java b/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindOsSupport.java
index c48f8a3..ad681c7 100644
--- a/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindOsSupport.java
+++ b/software/network/src/main/java/org/apache/brooklyn/entity/network/bind/BindOsSupport.java
@@ -30,6 +30,7 @@
     // Likewise would make these package-private and have no getters if Freemarker was ok with it.
     private final String packageName;
     private final String serviceName;
+    private final String user;
     private final String rootConfigFile;
     private final String configDirectory;
     private final String workingDirectory;
@@ -39,6 +40,7 @@
     private BindOsSupport(
             String packageName,
             String serviceName,
+            String user,
             String rootConfigFile,
             String configDirectory,
             String workingDirectory,
@@ -46,6 +48,7 @@
             String keysFile) {
         this.packageName = packageName;
         this.serviceName = serviceName;
+        this.user = user;
         this.rootConfigFile = rootConfigFile;
         this.configDirectory = configDirectory;
         this.workingDirectory = workingDirectory;
@@ -60,9 +63,10 @@
         return new BindOsSupport(
                 "bind",
                 "named",
+                "named",
                 "/etc/named.conf",
                 "/var/named",
-                "/var/named/data",
+                "/var/named",
                 "/var/named/named.ca",
                 "/etc/named.iscdlv.key");
     }
@@ -74,6 +78,7 @@
         return new BindOsSupport(
                 "bind9",
                 "bind9",
+                "bind",
                 "/etc/bind/named.conf",
                 "/etc/bind",
                 "/var/cache/bind",
@@ -110,4 +115,7 @@
         return keysFile;
     }
 
+    public String getUser() {
+        return user;
+    }
 }
diff --git a/software/network/src/main/resources/catalog.bom b/software/network/src/main/resources/catalog.bom
index ceead03..6fbeca9 100644
--- a/software/network/src/main/resources/catalog.bom
+++ b/software/network/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.network.bind.BindDnsServer
diff --git a/software/nosql/pom.xml b/software/nosql/pom.xml
index bc9dbc2..4c603f4 100644
--- a/software/nosql/pom.xml
+++ b/software/nosql/pom.xml
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
diff --git a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/cassandra/CassandraNodeSshDriver.java b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/cassandra/CassandraNodeSshDriver.java
index dbb743b..8831e81 100644
--- a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/cassandra/CassandraNodeSshDriver.java
+++ b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/cassandra/CassandraNodeSshDriver.java
@@ -28,7 +28,6 @@
 import java.util.Set;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.mgmt.TaskWrapper;
 import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
@@ -263,10 +262,10 @@
                 queuedStart = root.getAttribute(CassandraDatacenter.QUEUED_START_NODES);
                 if (queuedStart==null) {
                     queuedStart = new ArrayList<Entity>();
-                    ((EntityLocal)root).sensors().set(CassandraDatacenter.QUEUED_START_NODES, queuedStart);
+                    root.sensors().set(CassandraDatacenter.QUEUED_START_NODES, queuedStart);
                 }
                 queuedStart.add(getEntity());
-                ((EntityLocal)root).sensors().set(CassandraDatacenter.QUEUED_START_NODES, queuedStart);
+                root.sensors().set(CassandraDatacenter.QUEUED_START_NODES, queuedStart);
             }
             do {
                 // get it again in case it is backed by something external
@@ -306,7 +305,7 @@
             }
             if (isClustered() && isFirst) {
                 for (Entity ancestor: getCassandraAncestors()) {
-                    ((EntityLocal)ancestor).sensors().set(CassandraDatacenter.FIRST_NODE_STARTED_TIME_UTC, System.currentTimeMillis());
+                    ancestor.sensors().set(CassandraDatacenter.FIRST_NODE_STARTED_TIME_UTC, System.currentTimeMillis());
                 }
             }
         } finally {
diff --git a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/couchbase/CouchbaseNodeImpl.java b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/couchbase/CouchbaseNodeImpl.java
index 68d3a54..4dc9bbd 100644
--- a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/couchbase/CouchbaseNodeImpl.java
+++ b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/couchbase/CouchbaseNodeImpl.java
@@ -252,7 +252,7 @@
         if (toClusterO instanceof String) {
             toClusterO = getManagementContext().lookup((String)toClusterO);
         }
-        Entity toCluster = Tasks.resolving(toClusterO, Entity.class).context(getExecutionContext()).get();
+        Entity toCluster = Tasks.resolving(toClusterO, Entity.class).context(this).get();
 
         String fromBucket = Preconditions.checkNotNull( (String)ruleArgs.getStringKey("fromBucket"), "fromBucket must be specified" );
 
diff --git a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/elasticsearch/ElasticSearchNodeImpl.java b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/elasticsearch/ElasticSearchNodeImpl.java
index ccb09e8..6bc89df 100644
--- a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/elasticsearch/ElasticSearchNodeImpl.java
+++ b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/elasticsearch/ElasticSearchNodeImpl.java
@@ -72,9 +72,7 @@
         Integer rawPort = getAttribute(HTTP_PORT);
         String hostname = getAttribute(HOSTNAME);
         checkNotNull(rawPort, "HTTP_PORT sensors not set for %s; is an acceptable port available?", this);
-        
-        sensors().set(DATASTORE_URL, String.format("http://%s:%s", hostname, rawPort));
-        
+
         HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, rawPort);
         Function<Maybe<JsonElement>, String> getNodeId = new Function<Maybe<JsonElement>, String>() {
             @Override public String apply(Maybe<JsonElement> input) {
@@ -85,6 +83,7 @@
             }
         };
 
+        sensors().set(DATASTORE_URL, String.format("http://%s", hp));
         if (isHttpMonitoringEnabled()) {
             boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS);
 
diff --git a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/mongodb/MongoDBReplicaSetImpl.java b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/mongodb/MongoDBReplicaSetImpl.java
index 1a80c3a..63f0f86 100644
--- a/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/mongodb/MongoDBReplicaSetImpl.java
+++ b/software/nosql/src/main/java/org/apache/brooklyn/entity/nosql/mongodb/MongoDBReplicaSetImpl.java
@@ -35,7 +35,6 @@
 import javax.annotation.Nullable;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.policy.PolicySpec;
@@ -54,7 +53,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Sets;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
@@ -238,7 +236,7 @@
                 addSecondaryWhenPrimaryIsNonNull(server);
             }
         } catch (Exception e) {
-            ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator((EntityLocal)server, "Failed to update replicaset", e);
+            ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator(server, "Failed to update replicaset", e);
         }
     }
 
@@ -338,7 +336,7 @@
                 }
             });
         } catch (Exception e) {
-            ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator((EntityLocal)member, "Failed to update replicaset", e);
+            ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator(member, "Failed to update replicaset", e);
         }
     }
 
diff --git a/software/nosql/src/main/resources/catalog.bom b/software/nosql/src/main/resources/catalog.bom
index bbcb063..228442e 100644
--- a/software/nosql/src/main/resources/catalog.bom
+++ b/software/nosql/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.nosql.redis.RedisStore
diff --git a/software/osgi/pom.xml b/software/osgi/pom.xml
index 7293674..d52a040 100644
--- a/software/osgi/pom.xml
+++ b/software/osgi/pom.xml
@@ -30,7 +30,7 @@
 	<parent>
 		<groupId>org.apache.brooklyn</groupId>
 		<artifactId>brooklyn-library</artifactId>
-		<version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+		<version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
 		<relativePath>../../pom.xml</relativePath>
 	</parent>
 
diff --git a/software/osgi/src/main/resources/catalog.bom b/software/osgi/src/main/resources/catalog.bom
index 9cd81f1..eddddcb 100644
--- a/software/osgi/src/main/resources/catalog.bom
+++ b/software/osgi/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.osgi.karaf.KarafContainer
diff --git a/software/webapp/pom.xml b/software/webapp/pom.xml
index ad6359f..dc75279 100644
--- a/software/webapp/pom.xml
+++ b/software/webapp/pom.xml
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-library</artifactId>
-        <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
@@ -164,7 +164,7 @@
         <dependency>
             <groupId>org.apache.brooklyn</groupId>
             <artifactId>brooklyn-camp</artifactId>
-            <version>0.10.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+            <version>0.11.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java
index ec3d7a8..e76d89e 100644
--- a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java
@@ -18,12 +18,14 @@
  */
 package org.apache.brooklyn.entity.proxy;
 
+import java.net.URI;
 import java.util.Set;
 
 import org.apache.brooklyn.api.entity.ImplementedBy;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.sensor.BasicAttributeSensorAndConfigKey;
 import org.apache.brooklyn.entity.group.Cluster;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
@@ -48,6 +50,9 @@
     ConfigKey<String> SERVICE_UP_URL_PATH = ConfigKeys.newStringConfigKey(
             "controller.config.serviceUpUrlPath", "The path that will be appended to the root URL to determine SERVICE_UP", "");
 
+    AttributeSensor<URI> MAIN_URI_MAPPED_SUBNET = Attributes.MAIN_URI_MAPPED_SUBNET;
+    AttributeSensor<URI> MAIN_URI_MAPPED_PUBLIC = Attributes.MAIN_URI_MAPPED_PUBLIC;
+
     boolean isActive();
 
     ProxySslConfig getSslConfig();
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java
index 257882d..3277e38 100644
--- a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java
@@ -33,6 +33,7 @@
 import org.apache.brooklyn.api.policy.Policy;
 import org.apache.brooklyn.api.policy.PolicySpec;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
@@ -53,6 +54,7 @@
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Objects;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -229,7 +231,15 @@
     public String getDomain() {
         return getAttribute(DOMAIN_NAME);
     }
-    
+
+    protected String getDomainWithoutWildcard() {
+        String domain = getDomain();
+        if (domain != null && domain.startsWith("*.")) {
+            domain = domain.replace("*.", ""); // Strip wildcard
+        }
+        return domain;
+    }
+
     @Override
     public Integer getPort() {
         if (isSsl())
@@ -265,14 +275,22 @@
     protected String inferProtocol() {
         return isSsl() ? "https" : "http";
     }
-    
+
+    protected String inferUrlForSubnet() {
+        String domain = getDomainWithoutWildcard();
+        if (domain==null) domain = getAttribute(Attributes.SUBNET_ADDRESS);
+        return inferUrl(domain, Optional.<Integer>absent());
+    }
+
+    protected String inferUrlForPublic() {
+        String domain = getDomainWithoutWildcard();
+        if (domain==null) domain = getAttribute(Attributes.ADDRESS);
+        return inferUrl(domain, Optional.<Integer>absent());
+    }
+
     /** returns URL, if it can be inferred; null otherwise */
     protected String inferUrl(boolean requireManagementAccessible) {
-        String protocol = checkNotNull(getProtocol(), "no protocol configured");
-        String domain = getDomain();
-        if (domain != null && domain.startsWith("*.")) {
-            domain = domain.replace("*.", ""); // Strip wildcard
-        }
+        String domain = getDomainWithoutWildcard();
         Integer port = checkNotNull(getPort(), "no port configured (the requested port may be in use)");
         if (requireManagementAccessible) {
             HostAndPort accessible = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, port);
@@ -282,8 +300,15 @@
             }
         }
         if (domain==null) domain = Machines.findSubnetHostname(this).orNull();
-        if (domain==null) return null;
-        return protocol+"://"+domain+":"+port+"/"+getConfig(SERVICE_UP_URL_PATH);
+        return inferUrl(domain, Optional.of(port));
+    }
+
+    protected String inferUrl(String host, Optional<Integer> portOverride) {
+        if (host == null) return null;
+        String protocol = checkNotNull(getProtocol(), "no protocol configured");
+        int port = portOverride.isPresent() ? portOverride.get() : checkNotNull(getPort(), "no port configured (the requested port may be in use)");
+        String path = getConfig(SERVICE_UP_URL_PATH);
+        return protocol+"://"+host+":"+port+"/"+path;
     }
 
     protected String inferUrl() {
@@ -316,12 +341,26 @@
         ConfigToAttributes.apply(this);
 
         sensors().set(PROTOCOL, inferProtocol());
-        sensors().set(MAIN_URI, URI.create(inferUrl()));
+        sensors().set(MAIN_URI, createUriOrNull(inferUrl()));
+        sensors().set(MAIN_URI_MAPPED_SUBNET, createUriOrNull(inferUrlForSubnet()));
+        sensors().set(MAIN_URI_MAPPED_PUBLIC, createUriOrNull(inferUrlForPublic()));
         sensors().set(ROOT_URL, inferUrl());
  
         checkNotNull(getPortNumberSensor(), "no sensor configured to infer port number");
     }
     
+    private URI createUriOrNull(String val) {
+        if (val == null) {
+            return null;
+        }
+        try {
+            return URI.create(val);
+        } catch (IllegalArgumentException e) {
+            LOG.warn("Invalid URI for {}: {}", this, val);
+            return null;
+        }
+    }
+    
     @Override
     protected void connectSensors() {
         super.connectSensors();
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java
index 4e55780..4b3a46e 100644
--- a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java
@@ -27,7 +27,6 @@
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.Attributes;
-import org.apache.brooklyn.core.entity.factory.ConfigurableEntityFactory;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.trait.MemberReplaceable;
 import org.apache.brooklyn.core.entity.trait.Resizable;
@@ -78,12 +77,6 @@
     public static BasicAttributeSensorAndConfigKey<EntitySpec<? extends LoadBalancer>> CONTROLLER_SPEC = new BasicAttributeSensorAndConfigKey(
             EntitySpec.class, "controlleddynamicwebappcluster.controllerSpec", "Spec for creating the controller (if one not supplied explicitly); if null an NGINX instance will be created");
 
-    @SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
-    /** factory (or closure) to create the web server, given flags */
-    @SetFromFlag("factory")
-    public static BasicAttributeSensorAndConfigKey<ConfigurableEntityFactory<? extends WebAppService>> FACTORY = new BasicAttributeSensorAndConfigKey(
-            ConfigurableEntityFactory.class, DynamicCluster.FACTORY.getName(), "factory (or closure) to create the web server");
-
     @SuppressWarnings({ "unchecked", "rawtypes" })
     /** Spec for web server entiites to be created */
     @SetFromFlag("memberSpec")
@@ -105,8 +98,6 @@
     
     public LoadBalancer getController();
     
-    public ConfigurableEntityFactory<WebAppService> getFactory();
-    
     public DynamicWebAppCluster getCluster();
     
     public Group getControlledGroup();
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java
index 6ac61ce..dd658e0 100644
--- a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java
@@ -32,7 +32,6 @@
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityPredicates;
-import org.apache.brooklyn.core.entity.factory.ConfigurableEntityFactory;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
 import org.apache.brooklyn.core.entity.trait.Startable;
@@ -79,16 +78,14 @@
     public void init() {
         super.init();
         
-        ConfigToAttributes.apply(this, FACTORY);
         ConfigToAttributes.apply(this, MEMBER_SPEC);
         ConfigToAttributes.apply(this, CONTROLLER);
         ConfigToAttributes.apply(this, CONTROLLER_SPEC);
         ConfigToAttributes.apply(this, WEB_CLUSTER_SPEC);
         ConfigToAttributes.apply(this, CONTROLLED_GROUP);
         
-        ConfigurableEntityFactory<? extends WebAppService> webServerFactory = getAttribute(FACTORY);
         EntitySpec<? extends WebAppService> webServerSpec = getAttribute(MEMBER_SPEC);
-        if (webServerFactory == null && webServerSpec == null) {
+        if (webServerSpec == null) {
             log.debug("creating default web server spec for {}", this);
             webServerSpec = EntitySpec.create(TomcatServer.class);
             sensors().set(MEMBER_SPEC, webServerSpec);
@@ -97,20 +94,14 @@
         log.debug("creating cluster child for {}", this);
         // Note relies on initial_size being inherited by DynamicWebAppCluster, because key id is identical
         EntitySpec<? extends DynamicWebAppCluster> webClusterSpec = getAttribute(WEB_CLUSTER_SPEC);
-        Map<String,Object> webClusterFlags;
-        if (webServerSpec != null) {
-            webClusterFlags = MutableMap.<String,Object>of("memberSpec", webServerSpec);
-        } else {
-            webClusterFlags = MutableMap.<String,Object>of("factory", webServerFactory);
-        }
+        Map<String,Object> webClusterFlags = MutableMap.<String,Object>of("memberSpec", webServerSpec);
+
         if (webClusterSpec == null) {
             log.debug("creating default web cluster spec for {}", this);
             webClusterSpec = EntitySpec.create(DynamicWebAppCluster.class);
         }
         boolean hasMemberSpec = webClusterSpec.getConfig().containsKey(DynamicWebAppCluster.MEMBER_SPEC) || webClusterSpec.getFlags().containsKey("memberSpec");
-        @SuppressWarnings("deprecation")
-        boolean hasMemberFactory = webClusterSpec.getConfig().containsKey(DynamicWebAppCluster.FACTORY) || webClusterSpec.getFlags().containsKey("factory");
-        if (!(hasMemberSpec || hasMemberFactory)) {
+        if (!hasMemberSpec) {
             webClusterSpec.configure(webClusterFlags);
         } else {
             log.warn("In {}, not setting cluster's {} because already set on webClusterSpec", new Object[] {this, webClusterFlags.keySet()});
@@ -182,12 +173,6 @@
         return getAttribute(CONTROLLER);
     }
 
-    @SuppressWarnings("unchecked")
-    @Override
-    public ConfigurableEntityFactory<WebAppService> getFactory() {
-        return (ConfigurableEntityFactory<WebAppService>) getAttribute(FACTORY);
-    }
-    
     // TODO convert to an entity reference which is serializable
     @Override
     public DynamicWebAppCluster getCluster() {
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java
index 3ce748b..a25b4f7 100644
--- a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java
@@ -24,7 +24,6 @@
 import java.util.concurrent.TimeUnit;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.core.location.access.BrooklynAccessUtils;
 import org.apache.brooklyn.policy.enricher.RollingTimeWindowMeanEnricher;
 import org.apache.brooklyn.policy.enricher.TimeFractionDeltaEnricher;
@@ -37,11 +36,11 @@
 
     public static final Duration DEFAULT_WINDOW_DURATION = Duration.TEN_SECONDS;
 
-    public static void connectWebAppServerPolicies(EntityLocal entity) {
+    public static void connectWebAppServerPolicies(Entity entity) {
         connectWebAppServerPolicies(entity, DEFAULT_WINDOW_DURATION);
     }
 
-    public static void connectWebAppServerPolicies(EntityLocal entity, Duration windowPeriod) {
+    public static void connectWebAppServerPolicies(Entity entity, Duration windowPeriod) {
         entity.enrichers().add(TimeWeightedDeltaEnricher.<Integer>getPerSecondDeltaEnricher(entity, REQUEST_COUNT, REQUESTS_PER_SECOND_LAST));
 
         if (windowPeriod!=null) {
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java
index 7264698..aa2c9ac 100644
--- a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java
@@ -36,7 +36,7 @@
             Sensors.newIntegerSensor("webapp.reqs.processingTime.max", "Max processing time for any single request, reported by webserver (millis)");
 
     /** the fraction of time represented by the most recent delta to TOTAL_PROCESSING_TIME, ie 0.4 if 800 millis were accumulated in last 2s;
-     * easily configured with {@link WebAppServiceMethods#connectWebAppServerPolicies(org.apache.brooklyn.api.internal.EntityLocal, org.apache.brooklyn.util.time.Duration)} */
+     * easily configured with {@link WebAppServiceMethods#connectWebAppServerPolicies(org.apache.brooklyn.api.entity.Entity, org.apache.brooklyn.util.time.Duration)} */
     public static final AttributeSensor<Double> PROCESSING_TIME_FRACTION_LAST =
             Sensors.newDoubleSensor("webapp.reqs.processingTime.fraction.last", "Fraction of time spent processing, reported by webserver (percentage, last datapoint)");
     public static final AttributeSensor<Double> PROCESSING_TIME_FRACTION_IN_WINDOW =
@@ -52,7 +52,7 @@
             Sensors.newDoubleSensor("webapp.reqs.perSec.last", "Reqs/sec (last datapoint)");
 
     /** rolled-up req/second for a window, 
-     * easily configured with {@link WebAppServiceMethods#connectWebAppServerPolicies(org.apache.brooklyn.api.internal.EntityLocal, org.apache.brooklyn.util.time.Duration)} */
+     * easily configured with {@link WebAppServiceMethods#connectWebAppServerPolicies(org.apache.brooklyn.api.entity.Entity, org.apache.brooklyn.util.time.Duration)} */
     public static final AttributeSensor<Double> REQUESTS_PER_SECOND_IN_WINDOW =
             Sensors.newDoubleSensor("webapp.reqs.perSec.windowed", "Reqs/sec (over time window)");
 
diff --git a/software/webapp/src/main/resources/catalog.bom b/software/webapp/src/main/resources/catalog.bom
index cd42ead..d1349ef 100644
--- a/software/webapp/src/main/resources/catalog.bom
+++ b/software/webapp/src/main/resources/catalog.bom
@@ -16,7 +16,7 @@
 # under the License.
 
 brooklyn.catalog:
-    version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION
+    version: "0.11.0-SNAPSHOT" # BROOKLYN_VERSION
     itemType: entity
     items:
     - id: org.apache.brooklyn.entity.webapp.nodejs.NodeJsWebAppService
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java
index 6daae33..94221c4 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java
@@ -22,6 +22,7 @@
 import static org.testng.Assert.assertTrue;
 
 import java.net.Inet4Address;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -30,7 +31,6 @@
 import java.util.Set;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.location.LocationSpec;
@@ -39,8 +39,11 @@
 import org.apache.brooklyn.api.location.NoMachinesAvailableException;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
 import org.apache.brooklyn.core.entity.Attributes;
-import org.apache.brooklyn.core.entity.factory.EntityFactory;
+import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.location.HasSubnetHostname;
+import org.apache.brooklyn.core.location.Machines;
+import org.apache.brooklyn.core.location.PortRanges;
 import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
 import org.apache.brooklyn.core.test.entity.TestEntity;
 import org.apache.brooklyn.core.test.entity.TestEntityImpl;
@@ -53,6 +56,7 @@
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.flags.SetFromFlag;
 import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.annotations.BeforeMethod;
@@ -77,7 +81,7 @@
         super.setUp();
         
         List<SshMachineLocation> machines = new ArrayList<SshMachineLocation>();
-        for (int i=1; i<=10; i++) {
+        for (int i = 1; i <= 10; i++) {
             SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
                     .configure("address", Inet4Address.getByName("1.1.1."+i)));
             machines.add(machine);
@@ -85,13 +89,13 @@
         loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)
                 .configure("machines", machines));
         
-        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+        cluster = app.addChild(EntitySpec.create(DynamicCluster.class)
                 .configure("initialSize", 0)
-                .configure("factory", new ClusteredEntity.Factory()));
+                .configure("memberSpec", EntitySpec.create(TestEntity.class).impl(WebServerEntity.class)));
         
-        controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
+        controller = app.addChild(EntitySpec.create(TrackingAbstractController.class)
                 .configure("serverPool", cluster) 
-                .configure("portNumberSensor", ClusteredEntity.HTTP_PORT)
+                .configure("portNumberSensor", WebServerEntity.HTTP_PORT)
                 .configure("domain", "mydomain"));
         
         app.start(ImmutableList.of(loc));
@@ -114,9 +118,9 @@
         // above may trigger error logged about no hostname, but should update again with the settings below
         
         log.info("setting mymachine:1234");
-        child.sensors().set(ClusteredEntity.HOSTNAME, "mymachine");
+        child.sensors().set(WebServerEntity.HOSTNAME, "mymachine");
         child.sensors().set(Attributes.SUBNET_HOSTNAME, "mymachine");
-        child.sensors().set(ClusteredEntity.HTTP_PORT, 1234);
+        child.sensors().set(WebServerEntity.HTTP_PORT, 1234);
         assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine:1234"));
         
         /* a race failure has been observed, https://issues.apache.org/jira/browse/BROOKLYN-206
@@ -134,16 +138,16 @@
          */
         
         log.info("setting mymachine2:1234");
-        child.sensors().set(ClusteredEntity.HOSTNAME, "mymachine2");
+        child.sensors().set(WebServerEntity.HOSTNAME, "mymachine2");
         child.sensors().set(Attributes.SUBNET_HOSTNAME, "mymachine2");
         assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine2:1234"));
         
         log.info("setting mymachine2:1235");
-        child.sensors().set(ClusteredEntity.HTTP_PORT, 1235);
+        child.sensors().set(WebServerEntity.HTTP_PORT, 1235);
         assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine2:1235"));
         
         log.info("clearing");
-        child.sensors().set(ClusteredEntity.HOSTNAME, null);
+        child.sensors().set(WebServerEntity.HOSTNAME, null);
         child.sensors().set(Attributes.SUBNET_HOSTNAME, null);
         assertEventuallyExplicitAddressesMatch(ImmutableList.<String>of());
     }
@@ -152,12 +156,12 @@
     public void testUpdateCalledWithAddressesOfNewChildren() {
         // First child
         cluster.resize(1);
-        EntityLocal child = (EntityLocal) Iterables.getOnlyElement(cluster.getMembers());
+        Entity child = Iterables.getOnlyElement(cluster.getMembers());
         
         List<Collection<String>> u = Lists.newArrayList(controller.getUpdates());
         assertTrue(u.isEmpty(), "expected empty list but got "+u);
         
-        child.sensors().set(ClusteredEntity.HTTP_PORT, 1234);
+        child.sensors().set(WebServerEntity.HTTP_PORT, 1234);
         child.sensors().set(Startable.SERVICE_UP, true);
         assertEventuallyAddressesMatchCluster();
 
@@ -168,9 +172,9 @@
             public void run() {
                 assertEquals(cluster.getMembers().size(), 2);
             }});
-        EntityLocal child2 = (EntityLocal) Iterables.getOnlyElement(MutableSet.builder().addAll(cluster.getMembers()).remove(child).build());
+        Entity child2 = Iterables.getOnlyElement(MutableSet.<Entity>builder().addAll(cluster.getMembers()).remove(child).build());
         
-        child2.sensors().set(ClusteredEntity.HTTP_PORT, 1234);
+        child2.sensors().set(WebServerEntity.HTTP_PORT, 1234);
         child2.sensors().set(Startable.SERVICE_UP, true);
         assertEventuallyAddressesMatchCluster();
         
@@ -189,8 +193,8 @@
         // Get some children, so we can remove one...
         cluster.resize(2);
         for (Entity it: cluster.getMembers()) { 
-            ((EntityLocal)it).sensors().set(ClusteredEntity.HTTP_PORT, 1234);
-            ((EntityLocal)it).sensors().set(Startable.SERVICE_UP, true);
+            it.sensors().set(WebServerEntity.HTTP_PORT, 1234);
+            it.sensors().set(Startable.SERVICE_UP, true);
         }
         assertEventuallyAddressesMatchCluster();
 
@@ -205,17 +209,17 @@
         // Get some children, so we can remove one...
         cluster.resize(2);
         for (Entity it: cluster.getMembers()) { 
-            ((EntityLocal)it).sensors().set(ClusteredEntity.HTTP_PORT, 1234);
-            ((EntityLocal)it).sensors().set(Startable.SERVICE_UP, true);
+            it.sensors().set(WebServerEntity.HTTP_PORT, 1234);
+            it.sensors().set(Startable.SERVICE_UP, true);
         }
         assertEventuallyAddressesMatchCluster();
 
         // Now unset host/port, and remove children
         // Note the unsetting of hostname is done in SoftwareProcessImpl.stop(), so this is realistic
         for (Entity it : cluster.getMembers()) {
-            ((EntityLocal)it).sensors().set(ClusteredEntity.HTTP_PORT, null);
-            ((EntityLocal)it).sensors().set(ClusteredEntity.HOSTNAME, null);
-            ((EntityLocal)it).sensors().set(Startable.SERVICE_UP, false);
+            it.sensors().set(WebServerEntity.HTTP_PORT, null);
+            it.sensors().set(WebServerEntity.HOSTNAME, null);
+            it.sensors().set(Startable.SERVICE_UP, false);
         }
         assertEventuallyAddressesMatch(ImmutableList.<Entity>of());
     }
@@ -224,7 +228,7 @@
     public void testUsesHostAndPortSensor() throws Exception {
         controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
                 .configure("serverPool", cluster) 
-                .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT)
+                .configure("hostAndPortSensor", WebServerEntity.HOST_AND_PORT)
                 .configure("domain", "mydomain"));
         controller.start(Arrays.asList(loc));
         
@@ -239,7 +243,7 @@
         // TODO Ugly sleep to allow AbstractController to detect node having been added
         Thread.sleep(100);
         
-        child.sensors().set(ClusteredEntity.HOST_AND_PORT, "mymachine:1234");
+        child.sensors().set(WebServerEntity.HOST_AND_PORT, "mymachine:1234");
         assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine:1234"));
     }
 
@@ -248,8 +252,8 @@
         try {
             TrackingAbstractController controller2 = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
                     .configure("serverPool", cluster) 
-                    .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT)
-                    .configure("hostnameSensor", ClusteredEntity.HOSTNAME)
+                    .configure("hostAndPortSensor", WebServerEntity.HOST_AND_PORT)
+                    .configure("hostnameSensor", WebServerEntity.HOSTNAME)
                     .configure("domain", "mydomain"));
             controller2.start(Arrays.asList(loc));
         } catch (Exception e) {
@@ -264,8 +268,8 @@
         try {
             TrackingAbstractController controller3 = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
                     .configure("serverPool", cluster) 
-                    .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT)
-                    .configure("portNumberSensor", ClusteredEntity.HTTP_PORT)
+                    .configure("hostAndPortSensor", WebServerEntity.HOST_AND_PORT)
+                    .configure("portNumberSensor", WebServerEntity.HTTP_PORT)
                     .configure("domain", "mydomain"));
             controller3.start(Arrays.asList(loc));
         } catch (Exception e) {
@@ -300,19 +304,58 @@
         assertTrue(u.isEmpty(), "expected no updates, but got "+u);
     }
 
+    @Test
+    public void testMainUriSensorsCorrectlyComputedWithDomain() throws Exception {
+        URI expected = URI.create("http://mydomain:8000/");
+
+        EntityAsserts.assertAttributeEquals(controller, TrackingAbstractController.MAIN_URI, expected);
+        EntityAsserts.assertAttributeEquals(controller, TrackingAbstractController.MAIN_URI_MAPPED_SUBNET, expected);
+        EntityAsserts.assertAttributeEquals(controller, TrackingAbstractController.MAIN_URI_MAPPED_PUBLIC, expected);
+    }
+
+    @Test
+    public void testMainUriSensorsCorrectlyComputedWithoutDomain() throws Exception {
+        // The MachineLocation needs to implement HasSubnetHostname for the Attributes.SUBNET_HOSTNAME 
+        // to be set with the subnet addresss (otherwise it will fall back to using machine.getAddress()).
+        // See Machines.getSubnetHostname. 
+        
+        TrackingAbstractController controller2 = app.addChild(EntitySpec.create(TrackingAbstractController.class)
+                .configure(TrackingAbstractController.SERVER_POOL, cluster)
+                .configure(TrackingAbstractController.PROXY_HTTP_PORT, PortRanges.fromInteger(8081))
+                .location(LocationSpec.create(SshMachineLocationWithSubnetHostname.class)
+                        .configure("address", Inet4Address.getByName("1.1.1.1"))
+                        .configure(SshMachineLocation.PRIVATE_ADDRESSES, ImmutableList.of("2.2.2.2"))));
+        controller2.start(ImmutableList.<Location>of());
+
+        EntityAsserts.assertAttributeEquals(controller2, Attributes.ADDRESS, "1.1.1.1");
+        EntityAsserts.assertAttributeEquals(controller2, Attributes.SUBNET_ADDRESS, "2.2.2.2");
+        EntityAsserts.assertAttributeEquals(controller2, Attributes.MAIN_URI, URI.create("http://2.2.2.2:8081/"));
+        EntityAsserts.assertAttributeEquals(controller2, Attributes.MAIN_URI_MAPPED_PUBLIC, URI.create("http://1.1.1.1:8081/"));
+        EntityAsserts.assertAttributeEquals(controller2, Attributes.MAIN_URI_MAPPED_SUBNET, URI.create("http://2.2.2.2:8081/"));
+    }
+    public static class SshMachineLocationWithSubnetHostname extends SshMachineLocation implements HasSubnetHostname {
+        @Override public String getSubnetHostname() {
+            return getSubnetIp();
+        }
+        @Override public String getSubnetIp() {
+            Set<String> addrs = getPrivateAddresses();
+            return (addrs.isEmpty()) ? getAddress().getHostAddress() : Iterables.get(addrs, 0);
+        }
+    }
+    
     private void assertEventuallyAddressesMatchCluster() {
         assertEventuallyAddressesMatch(cluster.getMembers());
     }
 
     private void assertEventuallyAddressesMatch(final Collection<Entity> expectedMembers) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", 15000), new Runnable() {
+        Asserts.succeedsEventually(new Runnable() {
                 @Override public void run() {
                     assertAddressesMatch(locationsToAddresses(1234, expectedMembers));
                 }} );
     }
 
     private void assertEventuallyExplicitAddressesMatch(final Collection<String> expectedAddresses) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", 15000), new Runnable() {
+        Asserts.succeedsEventually(new Runnable() {
             @Override public void run() {
                 assertAddressesMatch(expectedAddresses);
             }} );
@@ -329,24 +372,14 @@
 
     private Collection<String> locationsToAddresses(int port, Collection<Entity> entities) {
         Set<String> result = MutableSet.of();
-        for (Entity e: entities) {
-            result.add( ((SshMachineLocation) e.getLocations().iterator().next()) .getAddress().getHostName()+":"+port);
+        for (Entity e : entities) {
+            SshMachineLocation machine = Machines.findUniqueMachineLocation(e.getLocations(), SshMachineLocation.class).get();
+            result.add(machine.getAddress().getHostName()+":"+port);
         }
         return result;
     }
 
-    public static class ClusteredEntity extends TestEntityImpl {
-        public static class Factory implements EntityFactory<ClusteredEntity> {
-            @Override
-            public ClusteredEntity newEntity(Map flags, Entity parent) {
-                return new ClusteredEntity(flags, parent);
-            }
-        }
-        public ClusteredEntity(Map flags, Entity parent) { super(flags,parent); }
-        public ClusteredEntity(Entity parent) { super(MutableMap.of(),parent); }
-        public ClusteredEntity(Map flags) { super(flags,null); }
-        public ClusteredEntity() { super(MutableMap.of(),null); }
-        
+    public static class WebServerEntity extends TestEntityImpl {
         @SetFromFlag("hostname")
         public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
         
@@ -356,10 +389,11 @@
         @SetFromFlag("hostAndPort")
         public static final AttributeSensor<String> HOST_AND_PORT = Attributes.HOST_AND_PORT;
         
-        MachineProvisioningLocation provisioner;
+        MachineProvisioningLocation<MachineLocation> provisioner;
         
+        @Override
         public void start(Collection<? extends Location> locs) {
-            provisioner = (MachineProvisioningLocation) locs.iterator().next();
+            provisioner = (MachineProvisioningLocation<MachineLocation>) locs.iterator().next();
             MachineLocation machine;
             try {
                 machine = provisioner.obtain(MutableMap.of());
@@ -369,9 +403,16 @@
             addLocations(Arrays.asList(machine));
             sensors().set(HOSTNAME, machine.getAddress().getHostName());
             sensors().set(Attributes.SUBNET_HOSTNAME, machine.getAddress().getHostName());
+            sensors().set(Attributes.MAIN_URI_MAPPED_SUBNET, URI.create(machine.getAddress().getHostName()));
+            sensors().set(Attributes.MAIN_URI_MAPPED_PUBLIC, URI.create("http://8.8.8.8:" + sensors().get(HTTP_PORT)));
         }
+        
+        @Override
         public void stop() {
-            if (provisioner!=null) provisioner.release((MachineLocation) firstLocation());
+            Maybe<MachineLocation> machine = Machines.findUniqueMachineLocation(getLocations(), MachineLocation.class);
+            if (provisioner != null) {
+                provisioner.release(machine.get());
+            }
         }
     }
 }
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java
index 7d18021..717c5d9 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java
@@ -18,69 +18,15 @@
  */
 package org.apache.brooklyn.entity.proxy;
 
-import java.util.Collection;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.MachineLocation;
-import org.apache.brooklyn.api.location.MachineProvisioningLocation;
-import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.api.entity.ImplementedBy;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
-import org.apache.brooklyn.core.entity.AbstractEntity;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.trait.Startable;
 import org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey;
-import org.apache.brooklyn.util.collections.MutableMap;
 
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-
-public class StubAppServer extends AbstractEntity implements Startable {
+@ImplementedBy(StubAppServerImpl.class)
+public interface StubAppServer extends Entity, Startable {
     public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
     public static final PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT;
-    public static AtomicInteger nextPort = new AtomicInteger(1234);
-
-    public StubAppServer(Map flags) {
-        super(flags);
-    }
-    
-    public StubAppServer(Map flags, Entity parent) {
-        super(flags, parent);
-    }
-    
-    @Override
-    public void start(Collection<? extends Location> locations) {
-        Location location = Iterables.getOnlyElement(locations);
-        if (location instanceof MachineProvisioningLocation) {
-            startInLocation((MachineProvisioningLocation)location);
-        } else {
-            startInLocation((MachineLocation)location);
-        }
-    }
-
-    private void startInLocation(MachineProvisioningLocation loc) {
-        try {
-            startInLocation(loc.obtain(MutableMap.of()));
-        } catch (NoMachinesAvailableException e) {
-            throw Throwables.propagate(e);
-        }
-    }
-    
-    private void startInLocation(MachineLocation loc) {
-        addLocations(ImmutableList.of((Location)loc));
-        sensors().set(HOSTNAME, loc.getAddress().getHostName());
-        sensors().set(HTTP_PORT, nextPort.getAndIncrement());
-        sensors().set(SERVICE_UP, true);
-    }
-
-    public void stop() {
-        sensors().set(SERVICE_UP, false);
-    }
-    
-    @Override
-    public void restart() {
-    }
-}
\ No newline at end of file
+}
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServerImpl.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServerImpl.java
new file mode 100644
index 0000000..7a51432
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServerImpl.java
@@ -0,0 +1,70 @@
+/*
+ * 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.entity.proxy;
+
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class StubAppServerImpl extends AbstractEntity implements StubAppServer {
+    public static AtomicInteger nextPort = new AtomicInteger(1234);
+
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        Location location = Iterables.getOnlyElement(locations);
+        if (location instanceof MachineProvisioningLocation) {
+            startInLocation((MachineProvisioningLocation)location);
+        } else {
+            startInLocation((MachineLocation)location);
+        }
+    }
+
+    private void startInLocation(MachineProvisioningLocation loc) {
+        try {
+            startInLocation(loc.obtain(MutableMap.of()));
+        } catch (NoMachinesAvailableException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+    
+    private void startInLocation(MachineLocation loc) {
+        addLocations(ImmutableList.of((Location)loc));
+        sensors().set(HOSTNAME, loc.getAddress().getHostName());
+        sensors().set(HTTP_PORT, nextPort.getAndIncrement());
+        sensors().set(SERVICE_UP, true);
+    }
+
+    public void stop() {
+        sensors().set(SERVICE_UP, false);
+    }
+    
+    @Override
+    public void restart() {
+    }
+}
\ No newline at end of file
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java
index a571627..65a6b4e 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java
@@ -32,8 +32,6 @@
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.factory.ApplicationBuilder;
-import org.apache.brooklyn.core.entity.factory.BasicConfigurableEntityFactory;
-import org.apache.brooklyn.core.entity.factory.EntityFactory;
 import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
 import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils;
 import org.apache.brooklyn.core.test.entity.TestApplication;
@@ -79,10 +77,10 @@
 
         app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
         
-        EntityFactory<StubAppServer> serverFactory = new BasicConfigurableEntityFactory<StubAppServer>(StubAppServer.class);
+        EntitySpec<StubAppServer> serverSpec = EntitySpec.create(StubAppServer.class);
         cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", initialClusterSize)
-                .configure("factory", serverFactory));
+                .configure(DynamicCluster.INITIAL_SIZE, initialClusterSize)
+                .configure(DynamicCluster.MEMBER_SPEC, serverSpec));
 
         urlMapping = app.createAndManageChild(EntitySpec.create(UrlMapping.class)
                 .configure("domain", "localhost")
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java
index f2c25d2..c533b8d 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java
@@ -30,8 +30,8 @@
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.core.entity.EntityAsserts;
-import org.apache.brooklyn.core.entity.factory.EntityFactory;
 import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
+import org.apache.brooklyn.core.test.entity.TestEntity;
 import org.apache.brooklyn.entity.group.DynamicCluster;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
 import org.apache.brooklyn.entity.webapp.JavaWebAppService;
@@ -79,10 +79,7 @@
     public void testWhenNoServersReturns404() {
         serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
                 .configure("initialSize", 0)
-                .configure(DynamicCluster.FACTORY, new EntityFactory<Entity>() {
-                    @Override public Entity newEntity(Map flags, Entity parent) {
-                        throw new UnsupportedOperationException();
-                    }}));
+                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
         
         nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
                 .configure("serverPool", serverPool)
@@ -98,10 +95,7 @@
     public void testRestart() {
         serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
                 .configure("initialSize", 0)
-                .configure(DynamicCluster.FACTORY, new EntityFactory<Entity>() {
-                    @Override public Entity newEntity(Map flags, Entity parent) {
-                        throw new UnsupportedOperationException();
-                    }}));
+                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
         
         nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
                 .configure("serverPool", serverPool)
@@ -244,10 +238,7 @@
     public void testTwoNginxesGetDifferentPorts() {
         serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
                 .configure("initialSize", 0)
-                .configure(DynamicCluster.FACTORY, new EntityFactory<Entity>() {
-                    @Override public Entity newEntity(Map flags, Entity parent) {
-                        throw new UnsupportedOperationException();
-                    }}));
+                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
         
         NginxController nginx1 = app.createAndManageChild(EntitySpec.create(NginxController.class)
                 .configure("serverPool", serverPool)
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java
index 4f80698..78759af 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java
@@ -25,8 +25,6 @@
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.core.entity.Attributes;
-import org.apache.brooklyn.core.entity.factory.BasicConfigurableEntityFactory;
-import org.apache.brooklyn.core.entity.factory.EntityFactory;
 import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
 import org.apache.brooklyn.entity.group.DynamicCluster;
 import org.apache.brooklyn.entity.proxy.StubAppServer;
@@ -39,20 +37,17 @@
 
 public class NginxLightIntegrationTest extends BrooklynAppUnitTestSupport {
 
-    private NginxController nginx;
-    private DynamicCluster cluster;
-
     // FIXME Fails because getting addEntity callback for group members while nginx is still starting,
     // so important nginx fields are still null. Therefore get NPE for cluster members, and thus targets
     // is of size zero.
     @Test(groups = {"Integration", "WIP"})
     public void testNginxTargetsMatchesClusterMembers() {
-        EntityFactory<StubAppServer> serverFactory = new BasicConfigurableEntityFactory<StubAppServer>(StubAppServer.class);
+        EntitySpec<StubAppServer> serverSpec = EntitySpec.create(StubAppServer.class);
         final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
                 .configure("initialSize", 2)
-                .configure("factory", serverFactory));
+                .configure(DynamicCluster.MEMBER_SPEC, serverSpec));
                 
-        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+        final NginxController nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
                 .configure("serverPool", cluster)
                 .configure("domain", "localhost"));
         
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java
index 1aee752..9f40dd0 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java
@@ -27,7 +27,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import org.apache.brooklyn.api.entity.Entity;
@@ -36,16 +35,14 @@
 import org.apache.brooklyn.api.mgmt.EntityManager;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.Entities;
-import org.apache.brooklyn.core.entity.factory.EntityFactory;
 import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
+import org.apache.brooklyn.core.test.entity.TestEntity;
 import org.apache.brooklyn.entity.group.BasicGroup;
 import org.apache.brooklyn.entity.group.DynamicCluster;
-import org.apache.brooklyn.entity.proxy.nginx.NginxController;
-import org.apache.brooklyn.entity.proxy.nginx.UrlMapping;
-import org.apache.brooklyn.entity.proxy.nginx.UrlRewriteRule;
 import org.apache.brooklyn.entity.webapp.JavaWebAppService;
 import org.apache.brooklyn.entity.webapp.WebAppService;
 import org.apache.brooklyn.entity.webapp.tomcat.Tomcat8Server;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.test.HttpTestUtils;
 import org.apache.brooklyn.test.support.TestResourceUnavailableException;
@@ -53,7 +50,6 @@
 import org.slf4j.LoggerFactory;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
-import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -429,10 +425,7 @@
     public void testUrlMappingWithEmptyCoreCluster() throws Exception {
         DynamicCluster nullCluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
             .configure("initialSize", 0)
-            .configure("factory", new EntityFactory<Entity>() {
-                public Entity newEntity(Map flags, Entity parent) {
-                    throw new UnsupportedOperationException();
-                }}));
+            .configure("membeSpec", EntitySpec.create(TestEntity.class)));
 
         DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
                 .configure("initialSize", 1)
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
index b4c7cb3..a6c9b2f 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
@@ -39,7 +39,6 @@
 
 import org.apache.brooklyn.api.entity.Application;
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.location.LocationSpec;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.mgmt.SubscriptionHandle;
@@ -474,7 +473,7 @@
         URL resource = getClass().getClassLoader().getResource(war);
         assertNotNull(resource);
         
-        ((EntityLocal)entity).config().set(JavaWebAppService.ROOT_WAR, resource.toString());
+        entity.config().set(JavaWebAppService.ROOT_WAR, resource.toString());
         Entities.start(entity.getApplication(), ImmutableList.of(loc));
         
         //tomcat may need a while to unpack everything
@@ -496,7 +495,7 @@
         URL resource = getClass().getClassLoader().getResource(war);
         assertNotNull(resource);
         
-        ((EntityLocal)entity).config().set(JavaWebAppService.NAMED_WARS, ImmutableList.of(resource.toString()));
+        entity.config().set(JavaWebAppService.NAMED_WARS, ImmutableList.of(resource.toString()));
         Entities.start(entity.getApplication(), ImmutableList.of(loc));
 
         Asserts.succeedsEventually(MutableMap.of("timeout", 60*1000), new Runnable() {
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterTest.java
index dfdc10a..5b9519c 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterTest.java
@@ -21,7 +21,6 @@
 import static org.testng.Assert.assertEquals;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.Entities;
@@ -98,7 +97,7 @@
         EntityAsserts.assertAttributeEqualsEventually(cluster, DynamicWebAppCluster.SERVICE_UP, true);
         
         // When child is !service_up, should report false
-        ((EntityLocal)Iterables.get(cluster.getMembers(), 0)).sensors().set(Startable.SERVICE_UP, false);
+        Iterables.get(cluster.getMembers(), 0).sensors().set(Startable.SERVICE_UP, false);
         EntityAsserts.assertAttributeEqualsEventually(cluster, DynamicWebAppCluster.SERVICE_UP, false);
         EntityAsserts.assertAttributeEqualsContinually(MutableMap.of("timeout", SHORT_WAIT_MS), cluster, DynamicWebAppCluster.SERVICE_UP, false);
         
@@ -109,7 +108,7 @@
 
         // And if that serviceUp child goes away, should again report false
         Entities.unmanage(Iterables.get(cluster.getMembers(), 1));
-        ((EntityLocal)Iterables.get(cluster.getMembers(), 0)).sensors().set(Startable.SERVICE_UP, false);
+        Iterables.get(cluster.getMembers(), 0).sensors().set(Startable.SERVICE_UP, false);
         
         EntityAsserts.assertAttributeEqualsEventually(cluster, DynamicWebAppCluster.SERVICE_UP, false);
     }
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricTest.java
index 10d07d0..a00f682 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricTest.java
@@ -21,7 +21,6 @@
 import java.util.List;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityAsserts;
@@ -73,7 +72,7 @@
         
         app.start(locs);
         for (Entity member : fabric.getChildren()) {
-            ((EntityLocal)member).sensors().set(Changeable.GROUP_SIZE, 1);
+            member.sensors().set(Changeable.GROUP_SIZE, 1);
         }
         
         for (Entity member : fabric.getChildren()) {