This closes #1165
diff --git a/brooklyn-docs/guide/ops/externalized-configuration.md b/brooklyn-docs/guide/ops/externalized-configuration.md
index ea6c63c..24fd4eb 100644
--- a/brooklyn-docs/guide/ops/externalized-configuration.md
+++ b/brooklyn-docs/guide/ops/externalized-configuration.md
@@ -122,6 +122,23 @@
 {% endhighlight %}
 
 
+## Referring to External Configuration in Catalog Items
+
+The same blueprint language DSL can be used within YAML catalog items. For example:
+
+    brooklyn.catalog:
+      id: com.example.myblueprint
+      version: 1.2.3
+      brooklyn.libraries:
+      - >
+        $brooklyn:formatString("https://%s:%s@repo.example.com/libs/myblueprint-1.2.3.jar", 
+        external("mysuppier", "username"), external("mysupplier", "password"))
+      item:
+        type: com.example.MyBlueprint
+
+Note the `>` in the example above is used to split across multiple lines.
+
+
 ## Suppliers available with Brooklyn
 
 Brooklyn ships with a number of external configuration suppliers ready to use.
diff --git a/brooklyn-docs/guide/yaml/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml b/brooklyn-docs/guide/yaml/test/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml
similarity index 100%
rename from brooklyn-docs/guide/yaml/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml
rename to brooklyn-docs/guide/yaml/test/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml
diff --git a/brooklyn-docs/guide/yaml/example_yaml/entities/loopovergroupmembers-entity.yaml b/brooklyn-docs/guide/yaml/test/example_yaml/entities/loopovergroupmembers-entity.yaml
similarity index 100%
rename from brooklyn-docs/guide/yaml/example_yaml/entities/loopovergroupmembers-entity.yaml
rename to brooklyn-docs/guide/yaml/test/example_yaml/entities/loopovergroupmembers-entity.yaml
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicExecutionManager.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicExecutionManager.java
index 0aab7d5..c75ad0b 100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicExecutionManager.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicExecutionManager.java
@@ -67,7 +67,6 @@
 import com.google.common.base.CaseFormat;
 import com.google.common.base.Function;
 import com.google.common.base.Preconditions;
-import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ExecutionList;
@@ -565,7 +564,15 @@
                 int subtasksReallyCancelled=0;
                 
                 if (task instanceof HasTaskChildren) {
-                    for (Task<?> child: ((HasTaskChildren)task).getChildren()) {
+                    // cancel tasks in reverse order --
+                    // it should be the case that if child1 is cancelled,
+                    // a parentTask should NOT call a subsequent child2,
+                    // but just in case, we cancel child2 first
+                    // NB: DST and others may apply their own recursive cancel behaviour
+                    MutableList<Task<?>> childrenReversed = MutableList.copyOf( ((HasTaskChildren)task).getChildren() );
+                    Collections.reverse(childrenReversed);
+                    
+                    for (Task<?> child: childrenReversed) {
                         if (log.isTraceEnabled()) {
                             log.trace("Cancelling "+child+" on recursive cancellation of "+task);
                         }
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicTask.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicTask.java
index 7c29bba..efd3001 100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicTask.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/task/BasicTask.java
@@ -298,7 +298,7 @@
     public synchronized boolean cancel(TaskCancellationMode mode) {
         if (isDone()) return false;
         if (log.isTraceEnabled()) {
-            log.trace("BT cancelling "+this+" mode "+mode);
+            log.trace("BT cancelling "+this+" mode "+mode+", from thread "+Thread.currentThread());
         }
         cancelled = true;
         doCancel(mode);
diff --git a/brooklyn-server/core/src/test/java/org/apache/brooklyn/util/core/task/DynamicSequentialTaskTest.java b/brooklyn-server/core/src/test/java/org/apache/brooklyn/util/core/task/DynamicSequentialTaskTest.java
index 364870a..763c067 100644
--- a/brooklyn-server/core/src/test/java/org/apache/brooklyn/util/core/task/DynamicSequentialTaskTest.java
+++ b/brooklyn-server/core/src/test/java/org/apache/brooklyn/util/core/task/DynamicSequentialTaskTest.java
@@ -139,6 +139,7 @@
                         Thread.sleep(duration.toMillisecondsRoundingUp());
                     }
                 } catch (InterruptedException e) {
+                    log.info("releasing semaphore on interruption after saying "+message);
                     cancellations.release();
                     throw Exceptions.propagate(e);
                 }
@@ -159,7 +160,8 @@
     }
     
     public Task<String> sayTask(String message, Duration duration, String message2) {
-        return Tasks.<String>builder().displayName("say:"+message).body(sayCallable(message, duration, message2)).build();
+        return Tasks.<String>builder().displayName("say:"+message+(duration!=null ? ":wait("+duration+")" : "")+(message2!=null ? ":"+message2 : ""))
+            .body(sayCallable(message, duration, message2)).build();
     }
     
     public <T> Task<T> submitting(final Task<T> task) {
@@ -193,33 +195,52 @@
                 sayTask("2a", Duration.THIRTY_SECONDS, "2b"),
                 sayTask("3"));
         ec.submit(t);
-        
+
+        // wait for 2 to start, saying "2a", and the first interruptible block is when it waits for its 30s
         waitForMessages(Predicates.compose(MathPredicates.greaterThanOrEqual(2), CollectionFunctionals.sizeFunction()), TIMEOUT);
         Assert.assertEquals(messages, Arrays.asList("1", "2a"));
-        Time.sleep(Duration.millis(50));
+        
+        // now cancel, and make sure we get the right behaviour
         t.cancel(true);
         Assert.assertTrue(t.isDone());
-        // 2 should get cancelled, and invoke the cancellation semaphore
+        // 2 should get cancelled, and invoke the cancellation semaphore, but not say 2b
         // 3 should get cancelled and not run at all
-        Assert.assertEquals(messages, Arrays.asList("1", "2a"));
         
-        // Need to ensure that 2 has been started; race where we might cancel it before its run method
-        // is even begun. Hence doing "2a; pause; 2b" where nothing is interruptable before pause.
+        // cancel(..) currently cancels everything in the tree in the calling thread
+        // so we could even assert task3.isCancelled() now
+        // but not sure we will guarantee that for subtasks, so weaker assertion
+        // that it is eventually cancelled, and that it for sure never starts
+        
+        // message list is still 1, 2a
+        Assert.assertEquals(messages, Arrays.asList("1", "2a"));
+        // And 2 when cancelled should release the semaphore
+        log.info("testCancelled waiting on semaphore; permits left is "+cancellations.availablePermits());
         Assert.assertTrue(cancellations.tryAcquire(10, TimeUnit.SECONDS));
+        log.info("testCancelled acquired semaphore; permits left is "+cancellations.availablePermits());
         
         Iterator<Task<?>> ci = ((HasTaskChildren)t).getChildren().iterator();
+        // 1 completed fine
         Assert.assertEquals(ci.next().get(), "1");
+        // 2 is cancelled -- cancelled flag should always be set *before* the interrupt sent
+        // (and that released the semaphore above, even if thread is not completed, so this should be set)
         Task<?> task2 = ci.next();
         Assert.assertTrue(task2.isBegun());
         Assert.assertTrue(task2.isDone());
         Assert.assertTrue(task2.isCancelled());
-        
+
         Task<?> task3 = ci.next();
+        // 3 is being cancelled in the thread which cancelled 2, and should either be
+        // *before* 2 was cancelled or *not run* because the parent was cancelled 
+        // so we shouldn't need to wait ... but if we did:
+//        Asserts.eventually(Suppliers.ofInstance(task3), TaskPredicates.isDone());
+        Assert.assertTrue(task3.isDone());
+        Assert.assertTrue(task3.isCancelled());
         Assert.assertFalse(task3.isBegun());
-        Assert.assertTrue(task2.isDone());
-        Assert.assertTrue(task2.isCancelled());
-        
-        // but we do _not_ get a mutex from task3 as it does not run (is not interrupted)
+        // messages unchanged
+        Assert.assertEquals(messages, Arrays.asList("1", "2a"));
+        // no further mutexes should be available (ie 3 should not run)
+        // TODO for some reason this was observed to fail on the jenkins box (2016-01)
+        // but i can't see why; have added logging in case it happens again
         Assert.assertEquals(cancellations.availablePermits(), 0);
     }
     
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java
index d243833..4ffeb1a 100644
--- a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java
@@ -107,40 +107,43 @@
             skipStart = entityStarted.or(false);
         }
         if (!skipStart) {
-            Optional<Boolean> locationInstalled = Optional.fromNullable(getLocation().getConfig(BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION));
-            Optional<Boolean> entityInstalled = Optional.fromNullable(entity.getConfig(BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION));
-            boolean skipInstall = locationInstalled.or(entityInstalled).or(false);
-            if (!skipInstall) {
-                DynamicTasks.queue("copy-pre-install-resources", new Runnable() { public void run() {
-                    waitForConfigKey(BrooklynConfigKeys.PRE_INSTALL_RESOURCES_LATCH);
-                    copyPreInstallResources();
-                }});
+            DynamicTasks.queue("install", new Runnable() { public void run() {
 
-                DynamicTasks.queue("pre-install", new Runnable() { public void run() {
-                    preInstall();
-                }});
+                Optional<Boolean> locationInstalled = Optional.fromNullable(getLocation().getConfig(BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION));
+                Optional<Boolean> entityInstalled = Optional.fromNullable(entity.getConfig(BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION));
+                boolean skipInstall = locationInstalled.or(entityInstalled).or(false);
+                if (!skipInstall) {
+                    DynamicTasks.queue("copy-pre-install-resources", new Runnable() { public void run() {
+                        waitForConfigKey(BrooklynConfigKeys.PRE_INSTALL_RESOURCES_LATCH);
+                        copyPreInstallResources();
+                    }});
 
-                DynamicTasks.queue("pre-install-command", new Runnable() { public void run() {
-                    runPreInstallCommand();
-                }});
-                DynamicTasks.queue("setup", new Runnable() { public void run() {
-                    waitForConfigKey(BrooklynConfigKeys.SETUP_LATCH);
-                    setup();
-                }});
+                    DynamicTasks.queue("pre-install", new Runnable() { public void run() {
+                        preInstall();
+                    }});
 
-                DynamicTasks.queue("copy-install-resources", new Runnable() { public void run() {
-                    waitForConfigKey(BrooklynConfigKeys.INSTALL_RESOURCES_LATCH);
-                    copyInstallResources();
-                }});
+                    DynamicTasks.queue("pre-install-command", new Runnable() { public void run() {
+                        runPreInstallCommand();
+                    }});
+                    DynamicTasks.queue("setup", new Runnable() { public void run() {
+                        waitForConfigKey(BrooklynConfigKeys.SETUP_LATCH);
+                        setup();
+                    }});
 
-                DynamicTasks.queue("install", new Runnable() { public void run() {
-                    waitForConfigKey(BrooklynConfigKeys.INSTALL_LATCH);
-                    install();
-                }});
-            }
+                    DynamicTasks.queue("copy-install-resources", new Runnable() { public void run() {
+                        waitForConfigKey(BrooklynConfigKeys.INSTALL_RESOURCES_LATCH);
+                        copyInstallResources();
+                    }});
 
-            DynamicTasks.queue("post-install-command", new Runnable() { public void run() {
-                runPostInstallCommand();
+                    DynamicTasks.queue("install (main)", new Runnable() { public void run() {
+                        waitForConfigKey(BrooklynConfigKeys.INSTALL_LATCH);
+                        install();
+                    }});
+                }
+
+                DynamicTasks.queue("post-install-command", new Runnable() { public void run() {
+                    runPostInstallCommand();
+                }});
             }});
 
             DynamicTasks.queue("customize", new Runnable() { public void run() {
@@ -148,22 +151,24 @@
                 customize();
             }});
 
-            DynamicTasks.queue("copy-runtime-resources", new Runnable() { public void run() {
-                waitForConfigKey(BrooklynConfigKeys.RUNTIME_RESOURCES_LATCH);
-                copyRuntimeResources();
-            }});
-
-            DynamicTasks.queue("pre-launch-command", new Runnable() { public void run() {
-                runPreLaunchCommand();
-            }});
-
             DynamicTasks.queue("launch", new Runnable() { public void run() {
-                waitForConfigKey(BrooklynConfigKeys.LAUNCH_LATCH);
-                launch();
-            }});
+                DynamicTasks.queue("copy-runtime-resources", new Runnable() { public void run() {
+                    waitForConfigKey(BrooklynConfigKeys.RUNTIME_RESOURCES_LATCH);
+                    copyRuntimeResources();
+                }});
 
-            DynamicTasks.queue("post-launch-command", new Runnable() { public void run() {
-                runPostLaunchCommand();
+                DynamicTasks.queue("pre-launch-command", new Runnable() { public void run() {
+                    runPreLaunchCommand();
+                }});
+
+                DynamicTasks.queue("launch (main)", new Runnable() { public void run() {
+                    waitForConfigKey(BrooklynConfigKeys.LAUNCH_LATCH);
+                    launch();
+                }});
+
+                DynamicTasks.queue("post-launch-command", new Runnable() { public void run() {
+                    runPostLaunchCommand();
+                }});
             }});
         }
 
@@ -187,6 +192,7 @@
     public abstract void customize();
     public abstract void runPreLaunchCommand();
     public abstract void launch();
+    /** Only run if launch is run (if start is not skipped). */
     public abstract void runPostLaunchCommand();
 
     @Override
@@ -195,7 +201,8 @@
     }
 
     /**
-     * Implement this method in child classes to add some post-launch behavior
+     * Implement this method in child classes to add some post-launch behavior.
+     * This is run even if start is skipped and launch is not run.
      */
     public void postLaunch() {}
 
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/InboundPortsUtils.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/InboundPortsUtils.java
new file mode 100644
index 0000000..adc7b58
--- /dev/null
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/InboundPortsUtils.java
@@ -0,0 +1,98 @@
+/*
+ * 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.software.base;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.reflect.TypeToken;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.PortRange;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class InboundPortsUtils {
+    private static final Logger log = LoggerFactory.getLogger(InboundPortsUtils.class);
+
+    /**
+     * Returns the required open inbound ports for an Entity.
+     * If {@code portsAutoInfer} is {@code true} then
+     * return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
+     * config key {@link PortRange} plus any ports defined with a config key matching the provided regex.
+     * @param entity the Entity
+     * @param portsAutoInfer if {@code true} then also return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
+     * config key {@link PortRange} plus any ports defined with a config keys matching the provided regex
+     * @param portRegex the regex to match config keys that define inbound ports
+     * @return a collection of port numbers
+     */
+    public static Collection<Integer> getRequiredOpenPorts(Entity entity, Boolean portsAutoInfer, String portRegex) {
+        return getRequiredOpenPorts(entity, ImmutableSet.<ConfigKey<?>>of(), portsAutoInfer, portRegex);
+    }
+
+    /**
+     * Returns the required open inbound ports for an Entity.
+     * If {@code portsAutoInfer} is {@code true} then
+     * return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
+     * config key {@link PortRange} plus any ports defined with a config key matching the provided regex.
+     * This method also accepts an extra set of config keys in addition to those that are defined in the EntityType of the entity itself.
+     * @param entity the Entity
+     * @param extraConfigKeys extra set of config key to inspect for inbound ports
+     * @param portsAutoInfer if {@code true} then return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
+     * config key {@link PortRange} plus any ports defined with a config keys matching the provided regex
+     * @param portRegex the regex to match config keys that define inbound ports
+     * @return a collection of port numbers
+     */
+    public static Collection<Integer> getRequiredOpenPorts(Entity entity, Set<ConfigKey<?>> extraConfigKeys, Boolean portsAutoInfer, String portRegex) {
+        Set<Integer> ports = MutableSet.of();
+
+        /* TODO: This won't work if there's a port collision, which will cause the corresponding port attribute
+           to be incremented until a free port is found. In that case the entity will use the free port, but the
+           firewall will open the initial port instead. Mostly a problem for SameServerEntity, localhost location.
+         */
+        if (portsAutoInfer == null || portsAutoInfer.booleanValue()) { // auto-infer defaults to true if not specified
+            Set<ConfigKey<?>> configKeys = Sets.newHashSet(extraConfigKeys);
+            configKeys.addAll(entity.getEntityType().getConfigKeys());
+
+            if (portRegex == null) portRegex = ".*\\.port"; // defaults to legacy regex if not specified
+            Pattern portsPattern = Pattern.compile(portRegex);
+            for (ConfigKey<?> k : configKeys) {
+                if (PortRange.class.isAssignableFrom(k.getType()) || portsPattern.matcher(k.getName()).matches()) {
+                    Object value = entity.config().get(k);
+                    Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, new TypeToken<PortRange>() {
+                    });
+                    if (maybePortRange.isPresentAndNonNull()) {
+                        PortRange p = maybePortRange.get();
+                        if (p != null && !p.isEmpty())
+                            ports.add(p.iterator().next());
+                    }
+                }
+            }
+        }
+
+        log.debug("getRequiredOpenPorts detected default {} for {}", ports, entity);
+        return ports;
+    }
+}
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java
index 9cd6149..178f471 100644
--- a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java
@@ -57,6 +57,7 @@
 
     /**
      * @return the ports that this entity wants to use, aggregated for all its child entities.
+     * @see InboundPortsUtils#getRequiredOpenPorts(Entity, Boolean, String)
      */
     protected Collection<Integer> getRequiredOpenPorts() {
         Set<Integer> result = Sets.newLinkedHashSet();
@@ -67,31 +68,21 @@
 
     /** @return the ports required for a specific child entity */
     protected Collection<Integer> getRequiredOpenPorts(Entity entity) {
-        Set<Integer> ports = MutableSet.of(22);
-        /* TODO: This won't work if there's a port collision, which will cause the corresponding port attribute
-           to be incremented until a free port is found. In that case the entity will use the free port, but the
-           firewall will open the initial port instead. Mostly a problem for SameServerEntity, localhost location.
-        */
-        // TODO: Remove duplication between this and SoftwareProcessImpl.getRequiredOpenPorts
-        final Set<ConfigKey<?>> configKeys = entity.getEntityType().getConfigKeys();
-        for (ConfigKey<?> k: configKeys) {
-            if (PortRange.class.isAssignableFrom(k.getType()) || k.getName().matches(".*\\.port")) {
-                Object value = entity.config().get(k);
-                Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, new TypeToken<PortRange>() {});
-                if (maybePortRange.isPresentAndNonNull()) {
-                    PortRange p = maybePortRange.get();
-                    if (p != null && !p.isEmpty()) {
-                        ports.add(p.iterator().next());
-                    }
-                }
-            }
-        }
+        Set<Integer> ports = MutableSet.of();
+        addRequiredOpenPortsRecursively(entity, ports);
+        return ports;
+    }
+
+    private void addRequiredOpenPortsRecursively(Entity entity, Set<Integer> ports) {
+        ports.addAll(entity.getConfig(SameServerEntity.REQUIRED_OPEN_LOGIN_PORTS));
+        Boolean portsAutoInfer = entity.getConfig(SameServerEntity.INBOUND_PORTS_AUTO_INFER);
+        String portsRegex = entity.getConfig(SameServerEntity.INBOUND_PORTS_CONFIG_REGEX);
+        ports.addAll(InboundPortsUtils.getRequiredOpenPorts(entity, portsAutoInfer, portsRegex));
         LOG.debug("getRequiredOpenPorts detected default {} for {}", ports, entity);
 
         for (Entity child : entity.getChildren()) {
-            ports.addAll(getRequiredOpenPorts(child));
+            addRequiredOpenPortsRecursively(child, ports);
         }
-        return ports;
     }
 
     @SuppressWarnings("unchecked")
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerEntity.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerEntity.java
index ea82b3d..6395350 100644
--- a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerEntity.java
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerEntity.java
@@ -18,6 +18,7 @@
  */
 package org.apache.brooklyn.entity.software.base;
 
+import java.util.Collection;
 import java.util.Map;
 
 import org.apache.brooklyn.api.entity.Entity;
@@ -59,6 +60,12 @@
     ConfigKey<QuorumCheck> UP_QUORUM_CHECK = ComputeServiceIndicatorsFromChildrenAndMembers.UP_QUORUM_CHECK;
     ConfigKey<QuorumCheck> RUNNING_QUORUM_CHECK = ComputeServiceIndicatorsFromChildrenAndMembers.RUNNING_QUORUM_CHECK;
 
+    ConfigKey<Collection<Integer>> REQUIRED_OPEN_LOGIN_PORTS = SoftwareProcess.REQUIRED_OPEN_LOGIN_PORTS;
+
+    ConfigKey<String> INBOUND_PORTS_CONFIG_REGEX = SoftwareProcess.INBOUND_PORTS_CONFIG_REGEX;
+
+    ConfigKey<Boolean> INBOUND_PORTS_AUTO_INFER = SoftwareProcess.INBOUND_PORTS_AUTO_INFER;
+
     AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = Attributes.SERVICE_STATE_ACTUAL;
 
     @SuppressWarnings("rawtypes")
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
index b14d6d8..c0e87bd 100644
--- a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
@@ -20,14 +20,20 @@
 
 import java.util.Collection;
 import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
 
+import com.google.common.collect.Sets;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.PortRange;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.annotation.Effector;
 import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.ConfigUtils;
 import org.apache.brooklyn.core.config.MapConfigKey;
+import org.apache.brooklyn.core.entity.AbstractEntity;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
@@ -36,7 +42,10 @@
 import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.time.Duration;
 
 import com.google.common.annotations.Beta;
@@ -50,13 +59,20 @@
     AttributeSensor<String> SUBNET_HOSTNAME = Attributes.SUBNET_HOSTNAME;
     AttributeSensor<String> SUBNET_ADDRESS = Attributes.SUBNET_ADDRESS;
 
-    @SuppressWarnings("serial")
     ConfigKey<Collection<Integer>> REQUIRED_OPEN_LOGIN_PORTS = ConfigKeys.newConfigKey(
             new TypeToken<Collection<Integer>>() {},
             "requiredOpenLoginPorts",
             "The port(s) to be opened, to allow login",
             ImmutableSet.of(22));
 
+    ConfigKey<String> INBOUND_PORTS_CONFIG_REGEX = ConfigKeys.newStringConfigKey("inboundPorts.configRegex",
+            "Regex governing the opening of ports based on config names",
+            ".*\\.port");
+
+    ConfigKey<Boolean> INBOUND_PORTS_AUTO_INFER = ConfigKeys.newBooleanConfigKey("inboundPorts.autoInfer",
+            "If set to false turns off the opening of ports based on naming convention, and also those that are of type PortRange in Java entities",
+            true);
+
     @SetFromFlag("startTimeout")
     ConfigKey<Duration> START_TIMEOUT = BrooklynConfigKeys.START_TIMEOUT;
 
@@ -300,12 +316,12 @@
     AttributeSensor<MachineProvisioningLocation> PROVISIONING_LOCATION = Sensors.newSensor(
             MachineProvisioningLocation.class, "softwareservice.provisioningLocation", "Location used to provision a machine where this is running");
 
-    AttributeSensor<Boolean> SERVICE_PROCESS_IS_RUNNING = Sensors.newBooleanSensor("service.process.isRunning", 
+    AttributeSensor<Boolean> SERVICE_PROCESS_IS_RUNNING = Sensors.newBooleanSensor("service.process.isRunning",
             "Whether the process for the service is confirmed as running");
-    
+
     AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = Attributes.SERVICE_STATE_ACTUAL;
     AttributeSensor<Transition> SERVICE_STATE_EXPECTED = Attributes.SERVICE_STATE_EXPECTED;
- 
+
     AttributeSensor<String> PID_FILE = Sensors.newStringSensor("softwareprocess.pid.file", "PID file");
 
     @Beta
@@ -319,14 +335,14 @@
             "Whether to restart/replace the machine provisioned for this entity:  'true', 'false', or 'auto' are supported, "
             + "with the default being 'auto' which means to restart or reprovision the machine if there is no simpler way known to restart the entity "
             + "(for example, if the machine is unhealthy, it would not be possible to restart the process, not even via a stop-then-start sequence); "
-            + "if the machine was not provisioned for this entity, this parameter has no effect", 
+            + "if the machine was not provisioned for this entity, this parameter has no effect",
             RestartMachineMode.AUTO.toString().toLowerCase());
-        
+
         // we supply a typed variant for retrieval; we want the untyped (above) to use lower case as the default in the GUI
-        // (very hard if using enum, since enum takes the name, and RendererHints do not apply to parameters) 
+        // (very hard if using enum, since enum takes the name, and RendererHints do not apply to parameters)
         @Beta /** @since 0.7.0 semantics of parameters to restart being explored */
         public static final ConfigKey<RestartMachineMode> RESTART_MACHINE_TYPED = ConfigKeys.newConfigKey(RestartMachineMode.class, "restartMachine");
-            
+
         public enum RestartMachineMode { TRUE, FALSE, AUTO }
     }
 
@@ -349,7 +365,7 @@
                 "IF_NOT_STOPPED stops the machine only if the entity is not marked as stopped, " +
                 "NEVER doesn't stop the machine.", StopMode.IF_NOT_STOPPED);
     }
-    
+
     // NB: the START, STOP, and RESTART effectors themselves are (re)defined by MachineLifecycleEffectorTasks
 
     /**
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java
index d3e6593..c62cc3d 100644
--- a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java
@@ -27,6 +27,7 @@
 import java.util.TimerTask;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntityLocal;
@@ -498,35 +499,19 @@
         return result.getAllConfigMutable();
     }
 
-    /** returns the ports that this entity wants to use;
-     * default implementation returns {@link SoftwareProcess#REQUIRED_OPEN_LOGIN_PORTS} plus first value 
-     * for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey} config key {@link PortRange}
-     * plus any ports defined with a config keys ending in {@code .port}.
+    /**
+     * Returns the ports that this entity wants to be opened.
+     * @see InboundPortsUtils#getRequiredOpenPorts(Entity, Set, Boolean, String)
+     * @see #REQUIRED_OPEN_LOGIN_PORTS
+     * @see #INBOUND_PORTS_AUTO_INFER
+     * @see #INBOUND_PORTS_CONFIG_REGEX
      */
     @SuppressWarnings("serial")
     protected Collection<Integer> getRequiredOpenPorts() {
         Set<Integer> ports = MutableSet.copyOf(getConfig(REQUIRED_OPEN_LOGIN_PORTS));
-        Map<ConfigKey<?>, ?> allConfig = config().getBag().getAllConfigAsConfigKeyMap();
-        Set<ConfigKey<?>> configKeys = Sets.newHashSet(allConfig.keySet());
-        configKeys.addAll(getEntityType().getConfigKeys());
-
-        /* TODO: This won't work if there's a port collision, which will cause the corresponding port attribute
-           to be incremented until a free port is found. In that case the entity will use the free port, but the
-           firewall will open the initial port instead. Mostly a problem for SameServerEntity, localhost location.
-         */
-        for (ConfigKey<?> k: configKeys) {
-            if (PortRange.class.isAssignableFrom(k.getType()) || k.getName().matches(".*\\.port")) {
-                Object value = config().get(k);
-                Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, new TypeToken<PortRange>() {});
-                if (maybePortRange.isPresentAndNonNull()) {
-                    PortRange p = maybePortRange.get();
-                    if (p != null && !p.isEmpty())
-                        ports.add(p.iterator().next());
-                }
-            }
-        }
-        
-        log.debug("getRequiredOpenPorts detected default {} for {}", ports, this);
+        Boolean portsAutoInfer = getConfig(INBOUND_PORTS_AUTO_INFER);
+        String portsRegex = getConfig(INBOUND_PORTS_CONFIG_REGEX);
+        ports.addAll(InboundPortsUtils.getRequiredOpenPorts(this, config().getBag().getAllConfigAsConfigKeyMap().keySet(), portsAutoInfer, portsRegex));
         return ports;
     }
 
diff --git a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java
index 6aa8122..d79274e 100644
--- a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java
+++ b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java
@@ -49,6 +49,8 @@
                 "test.double", "double", 2.0);
         ConfigKey<String> STRING = ConfigKeys.newStringConfigKey(
                 "test.string", "string", "3");
+        ConfigKey<Integer> REGEX_PORT = ConfigKeys.newIntegerConfigKey(
+                "my.port", "int", null);
     }
 
     public static class EntityWithConfigImpl extends AbstractEntity implements EntityWithConfig {
@@ -69,4 +71,54 @@
                 "expected=" + Iterables.toString(expected) + ", actual=" + Iterables.toString(requiredPorts));
     }
 
+    @Test
+    public void testGetRequiredOpenPortsByConfigName() {
+        SameServerEntity entity = app.createAndManageChild(EntitySpec.create(SameServerEntity.class).child(
+                EntitySpec.create(EntityWithConfig.class)
+                        // Previously SSDLET coerced everything TypeCoercions could handle to a port!
+                        .configure(EntityWithConfig.INTEGER, 1)
+                        .configure(EntityWithConfig.DOUBLE, 2.0)
+                        .configure(EntityWithConfig.STRING, "3")
+                        .configure(EntityWithConfig.REGEX_PORT, 4321)));
+        SameServerDriverLifecycleEffectorTasks effectorTasks = new SameServerDriverLifecycleEffectorTasks();
+        Collection<Integer> requiredPorts = effectorTasks.getRequiredOpenPorts(entity);
+        final ImmutableSet<Integer> expected = ImmutableSet.of(22, 1234, 4321);
+        assertEquals(requiredPorts, expected,
+                "expected=" + Iterables.toString(expected) + ", actual=" + Iterables.toString(requiredPorts));
+    }
+
+    @Test
+    public void testGetRequiredOpenPortsNoAutoInfer() {
+        SameServerEntity entity = app.createAndManageChild(EntitySpec.create(SameServerEntity.class)
+                .child(
+                        EntitySpec.create(EntityWithConfig.class)
+                                // Previously SSDLET coerced everything TypeCoercions could handle to a port!
+                                .configure(EntityWithConfig.INTEGER, 1)
+                                .configure(EntityWithConfig.DOUBLE, 2.0)
+                                .configure(EntityWithConfig.STRING, "3")
+                                .configure(EntityWithConfig.REGEX_PORT, 4321)
+                                .configure(SameServerEntity.INBOUND_PORTS_AUTO_INFER, false)));
+        SameServerDriverLifecycleEffectorTasks effectorTasks = new SameServerDriverLifecycleEffectorTasks();
+        Collection<Integer> requiredPorts = effectorTasks.getRequiredOpenPorts(entity);
+        final ImmutableSet<Integer> expected = ImmutableSet.of(22);
+        assertEquals(requiredPorts, expected,
+                "expected=" + Iterables.toString(expected) + ", actual=" + Iterables.toString(requiredPorts));
+    }
+
+    @Test
+    public void testGetRequiredOpenPortsWithCustomLoginPort() {
+        SameServerEntity entity = app.createAndManageChild(EntitySpec.create(SameServerEntity.class)
+                .configure(SameServerEntity.REQUIRED_OPEN_LOGIN_PORTS, ImmutableSet.of(2022))
+                .child(
+                        EntitySpec.create(EntityWithConfig.class)
+                                // Previously SSDLET coerced everything TypeCoercions could handle to a port!
+                                .configure(EntityWithConfig.INTEGER, 1)
+                                .configure(EntityWithConfig.DOUBLE, 2.0)
+                                .configure(EntityWithConfig.STRING, "3")));
+        SameServerDriverLifecycleEffectorTasks effectorTasks = new SameServerDriverLifecycleEffectorTasks();
+        Collection<Integer> requiredPorts = effectorTasks.getRequiredOpenPorts(entity);
+        final ImmutableSet<Integer> expected = ImmutableSet.of(2022, 1234);
+        assertEquals(requiredPorts, expected,
+                "expected=" + Iterables.toString(expected) + ", actual=" + Iterables.toString(requiredPorts));
+    }
 }
diff --git a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/jmx/JmxService.java b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/jmx/JmxService.java
index 2e23789..397aff3 100644
--- a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/jmx/JmxService.java
+++ b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/jmx/JmxService.java
@@ -21,7 +21,6 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
 
 import javax.management.InstanceAlreadyExistsException;
 import javax.management.MBeanNotificationInfo;
@@ -47,6 +46,7 @@
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.entity.java.UsesJmx;
 import org.apache.brooklyn.feed.jmx.JmxHelper;
+import org.apache.brooklyn.test.NetworkingTestUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,8 +68,10 @@
     private String url;
 
     public JmxService() throws Exception {
-        this("localhost", 28000 + (int)Math.floor(new Random().nextDouble() * 1000));
-        logger.warn("use of deprecated default host and port in JmxService");
+        this("localhost", NetworkingTestUtils.randomPortAround(28000));
+        
+        // TODO why this message if the constructor is not actually deprecated, and it seems useful?
+        //logger.warn("use of deprecated default host and port in JmxService");
     }
     
     /**
@@ -151,10 +153,12 @@
      * @throws MBeanRegistrationException 
      * @throws InstanceAlreadyExistsException 
      */
+    @SuppressWarnings({ "rawtypes" })
     public GeneralisedDynamicMBean registerMBean(Map initialAttributes, String name) throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException, MalformedObjectNameException, NullPointerException {
         return registerMBean(initialAttributes, ImmutableMap.of(), name);
     }
     
+    @SuppressWarnings({ "rawtypes", "unchecked" })
     public GeneralisedDynamicMBean registerMBean(Map initialAttributes, Map operations, String name) throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException, MalformedObjectNameException, NullPointerException {
         GeneralisedDynamicMBean mbean = new GeneralisedDynamicMBean(initialAttributes, operations);
         server.registerMBean(mbean, new ObjectName(name));
diff --git a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/feed/jmx/JmxFeedTest.java b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/feed/jmx/JmxFeedTest.java
index a90d657..d8df35b 100644
--- a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/feed/jmx/JmxFeedTest.java
+++ b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/feed/jmx/JmxFeedTest.java
@@ -65,6 +65,7 @@
 import org.apache.brooklyn.entity.software.base.test.jmx.JmxService;
 import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
 import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.test.NetworkingTestUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.annotations.AfterMethod;
@@ -114,7 +115,13 @@
         @Override public void init() {
             sensors().set(Attributes.HOSTNAME, "localhost");
             sensors().set(UsesJmx.JMX_PORT, 
-                    LocalhostMachineProvisioningLocation.obtainPort(PortRanges.fromString("40123+")));
+                    LocalhostMachineProvisioningLocation.obtainPort(PortRanges.fromString(
+                        // just doing "40123+" was not enough to avoid collisions (on 40125),
+                        // observed on jenkins, not sure why but 
+                        // maybe something else had a UDP connection we weren't detected,
+                        // or the static lock our localhost uses was being bypassed;
+                        // this should improve things (2016-01)
+                        NetworkingTestUtils.randomPortAround(40000)+"+")));
             // only supports no-agent, at the moment
             config().set(UsesJmx.JMX_AGENT_MODE, JmxAgentModes.NONE);
             sensors().set(UsesJmx.RMI_REGISTRY_PORT, -1);  // -1 means to use the JMX_PORT only
diff --git a/brooklyn-server/test-support/src/main/java/org/apache/brooklyn/test/NetworkingTestUtils.java b/brooklyn-server/test-support/src/main/java/org/apache/brooklyn/test/NetworkingTestUtils.java
index 7b48eaf..3cb4ba9 100644
--- a/brooklyn-server/test-support/src/main/java/org/apache/brooklyn/test/NetworkingTestUtils.java
+++ b/brooklyn-server/test-support/src/main/java/org/apache/brooklyn/test/NetworkingTestUtils.java
@@ -22,13 +22,13 @@
 
 import java.util.Map;
 
-import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.net.Networking;
 import org.apache.brooklyn.util.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.Beta;
 import com.google.common.collect.ImmutableMap;
 
 public class NetworkingTestUtils {
@@ -65,4 +65,14 @@
             assertTrue(Networking.isPortAvailable(entry.getValue()), errmsg);
         }
     }
+    
+    /** Returns a port not in use somewhere around the seed;
+     * this is not a foolproof way to prevent collisions, 
+     * but strikes a good balance of traceability (different callers will use different ranges)
+     * and collision avoidance, esp when combined with <code>Localhost...obtain(thisResult+"+");</code>.
+     */
+    @Beta
+    public static int randomPortAround(int seed) {
+        return Networking.nextAvailablePort( seed + (int)Math.floor(Math.random() * 1000) );
+    }
 }
diff --git a/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/net/Networking.java b/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/net/Networking.java
index fb988c7..85c87a0 100644
--- a/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/net/Networking.java
+++ b/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/net/Networking.java
@@ -18,6 +18,8 @@
  */
 package org.apache.brooklyn.util.net;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import java.io.IOException;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
@@ -26,7 +28,6 @@
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketException;
-import java.net.URI;
 import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Enumeration;
@@ -48,8 +49,6 @@
 import com.google.common.net.HostAndPort;
 import com.google.common.primitives.UnsignedBytes;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
 public class Networking {
 
     private static final Logger log = LoggerFactory.getLogger(Networking.class);