You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/08/19 13:10:04 UTC

[46/72] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - jclouds last few package prefixes needed, and tidy in core and elsewhere related (or observed in the process)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/MultiLocationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/MultiLocationTest.java b/core/src/test/java/org/apache/brooklyn/location/core/MultiLocationTest.java
new file mode 100644
index 0000000..ab527f2
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/MultiLocationTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.location.core;
+
+import static org.testng.Assert.assertTrue;
+
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.net.Networking;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.location.byon.FixedListMachineProvisioningLocation;
+import org.apache.brooklyn.location.cloud.AvailabilityZoneExtension;
+import org.apache.brooklyn.location.core.MultiLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class MultiLocationTest {
+
+    private static final Logger log = LoggerFactory.getLogger(MultiLocationTest.class);
+    
+    private LocalManagementContext managementContext;
+    private SshMachineLocation mac1a;
+    private SshMachineLocation mac1b;
+    private SshMachineLocation mac2a;
+    private SshMachineLocation mac2b;
+    private FixedListMachineProvisioningLocation<SshMachineLocation> loc1;
+    private FixedListMachineProvisioningLocation<SshMachineLocation> loc2;
+    private MultiLocation<SshMachineLocation> multiLoc;
+    
+    @SuppressWarnings("unchecked")
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = LocalManagementContextForTests.newInstance();
+        mac1a = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .displayName("mac1a")
+                .configure("address", Networking.getInetAddressWithFixedName("1.1.1.1")));
+        mac1b = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .displayName("mac1b")
+                .configure("address", Networking.getInetAddressWithFixedName("1.1.1.2")));
+        mac2a = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .displayName("mac2a")
+                .configure("address", Networking.getInetAddressWithFixedName("1.1.1.3")));
+        mac2b = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .displayName("mac2b")
+                .configure("address", Networking.getInetAddressWithFixedName("1.1.1.4")));
+        loc1 = managementContext.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)
+                .displayName("loc1")
+                .configure("machines", MutableSet.of(mac1a, mac1b)));
+        loc2 = managementContext.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)
+                .displayName("loc2")
+                .configure("machines", MutableSet.of(mac2a, mac2b)));
+        multiLoc = managementContext.getLocationManager().createLocation(LocationSpec.create(MultiLocation.class)
+                        .displayName("multiLoc")
+                        .configure("subLocations", ImmutableList.of(loc1, loc2)));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    @Test
+    public void testHasAvailabilityZonesAsSubLocations() throws Exception {
+        multiLoc.hasExtension(AvailabilityZoneExtension.class);
+        AvailabilityZoneExtension extension = multiLoc.getExtension(AvailabilityZoneExtension.class);
+        Asserts.assertEqualsIgnoringOrder(extension.getAllSubLocations(), ImmutableList.of(loc1, loc2));
+        Asserts.assertEqualsIgnoringOrder(extension.getSubLocations(2), ImmutableList.of(loc1, loc2));
+        assertTrue(ImmutableList.of(loc1, loc2).containsAll(extension.getSubLocations(1)));
+    }
+    
+    @Test
+    public void testObtainAndReleaseDelegateToSubLocation() throws Exception {
+        SshMachineLocation obtained = multiLoc.obtain(ImmutableMap.of());
+        assertTrue(ImmutableList.of(mac1a, mac1b, mac2a, mac2b).contains(obtained));
+        multiLoc.release(obtained);
+    }
+    
+    @Test
+    public void testObtainsMovesThroughSubLocations() throws Exception {
+        Assert.assertEquals(multiLoc.obtain().getAddress().getHostAddress(), "1.1.1.1");
+        Assert.assertEquals(multiLoc.obtain().getAddress().getHostAddress(), "1.1.1.2");
+        Assert.assertEquals(multiLoc.obtain().getAddress().getHostAddress(), "1.1.1.3");
+        Assert.assertEquals(multiLoc.obtain().getAddress().getHostAddress(), "1.1.1.4");
+        try {
+            multiLoc.obtain();
+            Assert.fail();
+        } catch (NoMachinesAvailableException e) {
+            log.info("Error when no machines available across locations is: "+e);
+            Assert.assertTrue(e.toString().contains("loc1"), "Message should have referred to sub-location message: "+e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/PaasLocationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/PaasLocationTest.java b/core/src/test/java/org/apache/brooklyn/location/core/PaasLocationTest.java
new file mode 100644
index 0000000..a58c0a1
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/PaasLocationTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.location.core;
+
+import org.apache.brooklyn.core.test.location.TestPaasLocation;
+import org.apache.brooklyn.location.paas.PaasLocation;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class PaasLocationTest {
+
+    private PaasLocation location;
+    
+    @Test
+    public void testProviderName(){
+        location = new TestPaasLocation();
+        Assert.assertEquals(location.getPaasProviderName(), "TestPaas");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/PortRangesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/PortRangesTest.java b/core/src/test/java/org/apache/brooklyn/location/core/PortRangesTest.java
new file mode 100644
index 0000000..e4d7036
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/PortRangesTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.location.core;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Iterator;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.PortRange;
+import org.apache.brooklyn.location.core.PortRanges;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class PortRangesTest {
+
+    @Test
+    public void testSingleRange() {
+        PortRange r = PortRanges.fromInteger(1234);
+        assertContents(r, 1234);
+    }
+
+    @Test
+    public void testFromCollection() {
+        PortRange r = PortRanges.fromCollection(ImmutableList.of(1234, 2345));
+        assertContents(r, 1234, 2345);
+    }
+
+    @Test
+    public void testFromString() {
+        PortRange r = PortRanges.fromString("80,8080,8000,8080-8099");
+        assertContents(r, 80, 8080, 8000, 
+                8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089,
+                8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099);
+    }
+
+    @Test
+    public void testFromStringWithSpaces() {
+        PortRange r = PortRanges.fromString(" 80 , 8080  , 8000 , 8080  - 8099 ");
+        assertContents(r, 80, 8080, 8000, 
+                8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089,
+                8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099);
+    }
+
+    @Test
+    public void testFromStringWithSpacesToString() {
+        PortRange r = PortRanges.fromString(" 80 , 8080  , 8000 , 8080  - 8099 ");
+        Assert.assertEquals(r.toString(), "80,8080,8000,8080-8099");
+    }
+    
+    @Test
+    public void testFromStringThrowsIllegalArgumentException() {
+        assertFromStringThrowsIllegalArgumentException("80-100000");
+        assertFromStringThrowsIllegalArgumentException("0-80");
+    }
+
+    @Test
+    public void testCoercion() {
+        PortRanges.init();
+        PortRange r = TypeCoercions.coerce("80", PortRange.class);
+        assertContents(r, 80);
+    }
+
+    @Test
+    public void testCoercionInt() {
+        PortRanges.init();
+        PortRange r = TypeCoercions.coerce(80, PortRange.class);
+        assertContents(r, 80);
+    }
+    
+    @Test
+    public void testLinearRangeOfSizeOne() throws Exception {
+        PortRanges.LinearPortRange range = new PortRanges.LinearPortRange(80, 80);
+        assertEquals(Lists.newArrayList(range), ImmutableList.of(80));
+    }
+
+    @Test
+    public void testLinearRangeCountingUpwards() throws Exception {
+        PortRanges.LinearPortRange range = new PortRanges.LinearPortRange(80, 81);
+        assertEquals(Lists.newArrayList(range), ImmutableList.of(80, 81));
+    }
+    
+    @Test
+    public void testLinearRangeCountingDownwards() throws Exception {
+        PortRanges.LinearPortRange range = new PortRanges.LinearPortRange(80, 79);
+        assertEquals(Lists.newArrayList(range), ImmutableList.of(80, 79));
+    }
+    
+    protected void assertFromStringThrowsIllegalArgumentException(String range) {
+        try {
+            PortRanges.fromString(range);
+            Assert.fail();
+        } catch (IllegalArgumentException e) {
+            // success
+        }
+    }
+
+    private static <T> void assertContents(Iterable<T> actual, T ...expected) {
+        Iterator<T> i = actual.iterator();
+        int c = 0;
+        while (i.hasNext()) {
+            if (expected.length<=c) {
+                Assert.fail("Iterable contained more than the "+c+" expected elements");
+            }
+            Assert.assertEquals(i.next(), expected[c++]);
+        }
+        if (expected.length>c) {
+            Assert.fail("Iterable contained only "+c+" elements, "+expected.length+" expected");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/RecordingMachineLocationCustomizer.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/RecordingMachineLocationCustomizer.java b/core/src/test/java/org/apache/brooklyn/location/core/RecordingMachineLocationCustomizer.java
new file mode 100644
index 0000000..efaa94f
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/RecordingMachineLocationCustomizer.java
@@ -0,0 +1,71 @@
+/*
+ * 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.location.core;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineLocationCustomizer;
+
+public class RecordingMachineLocationCustomizer implements MachineLocationCustomizer {
+    public static class Call {
+        public final String methodName;
+        public final List<?> args;
+        
+        public Call(String methodName, List<?> args) {
+            this.methodName = checkNotNull(methodName);
+            this.args = checkNotNull(args);
+        }
+        
+        @Override
+        public String toString() {
+            return methodName+args;
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(methodName, args);
+        }
+        
+        @Override
+        public boolean equals(Object other) {
+            return (other instanceof RecordingMachineLocationCustomizer.Call) && 
+                    methodName.equals(((RecordingMachineLocationCustomizer.Call)other).methodName) && 
+                    args.equals(((RecordingMachineLocationCustomizer.Call)other).args);
+        }
+    }
+    
+    public final List<RecordingMachineLocationCustomizer.Call> calls = Lists.newCopyOnWriteArrayList();
+    
+    @Override
+    public void customize(MachineLocation machine) {
+        calls.add(new Call("customize", ImmutableList.of(machine)));
+    }
+
+    @Override
+    public void preRelease(MachineLocation machine) {
+        calls.add(new Call("preRelease", ImmutableList.of(machine)));
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/SimulatedLocation.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/SimulatedLocation.java b/core/src/test/java/org/apache/brooklyn/location/core/SimulatedLocation.java
new file mode 100644
index 0000000..e9f572c
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/SimulatedLocation.java
@@ -0,0 +1,141 @@
+/*
+ * 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.location.core;
+
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.location.HardwareDetails;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineDetails;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.OsDetails;
+import org.apache.brooklyn.api.location.PortRange;
+import org.apache.brooklyn.api.location.PortSupplier;
+import org.apache.brooklyn.location.core.AbstractLocation;
+import org.apache.brooklyn.location.core.BasicHardwareDetails;
+import org.apache.brooklyn.location.core.BasicMachineDetails;
+import org.apache.brooklyn.location.core.BasicOsDetails;
+import org.apache.brooklyn.location.core.PortRanges;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.net.Networking;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+
+/** Location for use in dev/test, defining custom start/stop support, and/or tweaking the ports which are permitted to be available
+ * (using setPermittedPorts(Iterable))
+ */
+public class SimulatedLocation extends AbstractLocation implements MachineProvisioningLocation<MachineLocation>, MachineLocation, PortSupplier {
+
+    private static final long serialVersionUID = 1L;
+    
+    private static final InetAddress address;
+    static {
+        address = Networking.getLocalHost();
+    }
+
+    Iterable<Integer> permittedPorts = PortRanges.fromString("1+");
+    Set<Integer> usedPorts = Sets.newLinkedHashSet();
+
+    public SimulatedLocation() {
+        this(MutableMap.<String,Object>of());
+    }
+    public SimulatedLocation(Map<String,? extends Object> flags) {
+        super(flags);
+    }
+    
+    @Override
+    public SimulatedLocation newSubLocation(Map<?,?> newFlags) {
+        // TODO shouldn't have to copy config bag as it should be inherited (but currently it is not used inherited everywhere; just most places)
+        return getManagementContext().getLocationManager().createLocation(LocationSpec.create(getClass())
+                .parent(this)
+                .configure(config().getLocalBag().getAllConfig())  // FIXME Should this just be inherited?
+                .configure(newFlags));
+    }
+
+    public MachineLocation obtain(Map<?,?> flags) {
+        return this;
+    }
+
+    public void release(MachineLocation machine) {
+    }
+
+    public Map<String,Object> getProvisioningFlags(Collection<String> tags) {
+        return MutableMap.<String,Object>of();
+    }
+    
+    public InetAddress getAddress() {
+        return address;
+    }
+
+    @Override
+    public String getHostname() {
+        String hostname = address.getHostName();
+        return (hostname == null || hostname.equals(address.getHostAddress())) ? null : hostname;
+    }
+    
+    @Override
+    public Set<String> getPublicAddresses() {
+        return ImmutableSet.of(address.getHostAddress());
+    }
+    
+    @Override
+    public Set<String> getPrivateAddresses() {
+        return ImmutableSet.of();
+    }
+
+    public synchronized boolean obtainSpecificPort(int portNumber) {
+        if (!Iterables.contains(permittedPorts, portNumber)) return false;
+        if (usedPorts.contains(portNumber)) return false;
+        usedPorts.add(portNumber);
+        return true;
+    }
+
+    public synchronized int obtainPort(PortRange range) {
+        for (int p: range)
+            if (obtainSpecificPort(p)) return p;
+        return -1;
+    }
+
+    public synchronized void releasePort(int portNumber) {
+        usedPorts.remove(portNumber);
+    }
+    
+    public synchronized void setPermittedPorts(Iterable<Integer> ports) {
+        permittedPorts  = ports;
+    }
+
+    @Override
+    public OsDetails getOsDetails() {
+        return getMachineDetails().getOsDetails();
+    }
+
+    @Override
+    public MachineDetails getMachineDetails() {
+        HardwareDetails hardwareDetails = new BasicHardwareDetails(null, null);
+        OsDetails osDetails = BasicOsDetails.Factory.ANONYMOUS_LINUX;
+        return new BasicMachineDetails(hardwareDetails, osDetails);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/TestPortSupplierLocation.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/TestPortSupplierLocation.java b/core/src/test/java/org/apache/brooklyn/location/core/TestPortSupplierLocation.java
new file mode 100644
index 0000000..df37585
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/TestPortSupplierLocation.java
@@ -0,0 +1,90 @@
+/*
+ * 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.location.core;
+
+import static org.testng.Assert.assertEquals;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.location.core.PortRanges;
+import org.apache.brooklyn.sensor.core.PortAttributeSensorAndConfigKey;
+import org.apache.brooklyn.sensor.feed.ConfigToAttributes;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPortSupplierLocation extends BrooklynAppUnitTestSupport {
+
+    SimulatedLocation loc;
+    PortAttributeSensorAndConfigKey ps;
+    TestEntity entity;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = app.newSimulatedLocation();
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        app.start(ImmutableList.of(loc));
+        
+        ps = new PortAttributeSensorAndConfigKey("some.port", "for testing", "1234+");
+    }
+
+    @Test
+    public void testObtainsPort() throws Exception {
+        ConfigToAttributes.apply(entity, ps);
+        
+        int p = entity.getAttribute(ps);
+        assertEquals(p, 1234);
+        
+        //sensor access should keep the same value
+        p = entity.getAttribute(ps);
+        assertEquals(p, 1234);
+    }
+    
+    @Test
+    public void testRepeatedConvertAccessIncrements() throws Exception {
+        int p = ps.getAsSensorValue(entity);
+        assertEquals(p, 1234);
+
+        //but direct access should see it as being reserved (not required behaviour, but it is the current behaviour)
+        int p2 = ps.getAsSensorValue(entity);
+        assertEquals(p2, 1235);
+    }
+
+    @Test
+    public void testNullBeforeSetting() throws Exception {
+        // currently getting the attribute before explicitly setting return null; i.e. no "auto-set" -- 
+        // but this behaviour may be changed
+        Integer p = entity.getAttribute(ps);
+        assertEquals(p, null);
+    }
+
+    @Test
+    public void testSimulatedRestrictedPermitted() throws Exception {
+        loc.setPermittedPorts(PortRanges.fromString("1240+"));
+        
+        ConfigToAttributes.apply(entity, ps);
+        int p = entity.getAttribute(ps);
+        assertEquals((int)p, 1240);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostLocationResolverTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostLocationResolverTest.java b/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostLocationResolverTest.java
new file mode 100644
index 0000000..bb781a4
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostLocationResolverTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.location.core.localhost;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.location.byon.FixedListMachineProvisioningLocation;
+import org.apache.brooklyn.location.core.BasicLocationRegistry;
+import org.apache.brooklyn.location.core.internal.LocationInternal;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class LocalhostLocationResolverTest {
+
+    private BrooklynProperties brooklynProperties;
+    private LocalManagementContext managementContext;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = LocalManagementContextForTests.newInstance();
+        brooklynProperties = managementContext.getBrooklynProperties();
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    @Test
+    public void testTakesLocalhostScopedProperties() {
+        brooklynProperties.put("brooklyn.location.localhost.privateKeyFile", "myprivatekeyfile");
+        brooklynProperties.put("brooklyn.location.localhost.publicKeyFile", "mypublickeyfile");
+        brooklynProperties.put("brooklyn.location.localhost.privateKeyData", "myprivateKeyData");
+        brooklynProperties.put("brooklyn.location.localhost.publicKeyData", "myPublicKeyData");
+        brooklynProperties.put("brooklyn.location.localhost.privateKeyPassphrase", "myprivateKeyPassphrase");
+
+        Map<String, Object> conf = resolve("localhost").config().getBag().getAllConfig();
+        
+        assertEquals(conf.get("privateKeyFile"), "myprivatekeyfile");
+        assertEquals(conf.get("publicKeyFile"), "mypublickeyfile");
+        assertEquals(conf.get("privateKeyData"), "myprivateKeyData");
+        assertEquals(conf.get("publicKeyData"), "myPublicKeyData");
+        assertEquals(conf.get("privateKeyPassphrase"), "myprivateKeyPassphrase");
+    }
+
+    @Test
+    public void testTakesLocalhostDeprecatedScopedProperties() {
+        brooklynProperties.put("brooklyn.localhost.privateKeyFile", "myprivatekeyfile");
+        brooklynProperties.put("brooklyn.localhost.publicKeyFile", "mypublickeyfile");
+        brooklynProperties.put("brooklyn.localhost.privateKeyData", "myprivateKeyData");
+        brooklynProperties.put("brooklyn.localhost.publicKeyData", "myPublicKeyData");
+        brooklynProperties.put("brooklyn.localhost.privateKeyPassphrase", "myprivateKeyPassphrase");
+
+        Map<String, Object> conf = resolve("localhost").config().getBag().getAllConfig();
+        
+        assertEquals(conf.get("privateKeyFile"), "myprivatekeyfile");
+        assertEquals(conf.get("publicKeyFile"), "mypublickeyfile");
+        assertEquals(conf.get("privateKeyData"), "myprivateKeyData");
+        assertEquals(conf.get("publicKeyData"), "myPublicKeyData");
+        assertEquals(conf.get("privateKeyPassphrase"), "myprivateKeyPassphrase");
+    }
+
+    @Test
+    public void testTakesDeprecatedProperties() {
+        brooklynProperties.put("brooklyn.localhost.private-key-file", "myprivatekeyfile");
+        brooklynProperties.put("brooklyn.localhost.public-key-file", "mypublickeyfile");
+        brooklynProperties.put("brooklyn.localhost.private-key-data", "myprivateKeyData");
+        brooklynProperties.put("brooklyn.localhost.public-key-data", "myPublicKeyData");
+        brooklynProperties.put("brooklyn.localhost.private-key-passphrase", "myprivateKeyPassphrase");
+        Map<String, Object> conf = resolve("localhost").config().getBag().getAllConfig();
+        
+        assertEquals(conf.get("privateKeyFile"), "myprivatekeyfile");
+        assertEquals(conf.get("publicKeyFile"), "mypublickeyfile");
+        assertEquals(conf.get("privateKeyData"), "myprivateKeyData");
+        assertEquals(conf.get("publicKeyData"), "myPublicKeyData");
+        assertEquals(conf.get("privateKeyPassphrase"), "myprivateKeyPassphrase");
+    }
+    
+    @Test
+    public void testPropertyScopePrescedence() {
+        brooklynProperties.put("brooklyn.location.named.mynamed", "localhost");
+        
+        // prefer those in "named" over everything else
+        brooklynProperties.put("brooklyn.location.named.mynamed.privateKeyFile", "privateKeyFile-inNamed");
+        brooklynProperties.put("brooklyn.location.localhost.privateKeyFile", "privateKeyFile-inProviderSpecific");
+        brooklynProperties.put("brooklyn.localhost.privateKeyFile", "privateKeyFile-inGeneric");
+
+        // prefer those in provider-specific over generic
+        brooklynProperties.put("brooklyn.location.localhost.publicKeyFile", "publicKeyFile-inProviderSpecific");
+        brooklynProperties.put("brooklyn.location.publicKeyFile", "publicKeyFile-inGeneric");
+
+        // prefer location-generic if nothing else
+        brooklynProperties.put("brooklyn.location.privateKeyData", "privateKeyData-inGeneric");
+
+        Map<String, Object> conf = resolve("named:mynamed").config().getBag().getAllConfig();
+        
+        assertEquals(conf.get("privateKeyFile"), "privateKeyFile-inNamed");
+        assertEquals(conf.get("publicKeyFile"), "publicKeyFile-inProviderSpecific");
+        assertEquals(conf.get("privateKeyData"), "privateKeyData-inGeneric");
+    }
+
+    @Test
+    public void testLocalhostLoads() {
+        Assert.assertTrue(resolve("localhost") instanceof LocalhostMachineProvisioningLocation);
+    }
+
+    @Test
+    public void testThrowsOnInvalid() throws Exception {
+        assertThrowsNoSuchElement("wrongprefix");
+        assertThrowsIllegalArgument("localhost(name=abc"); // no closing bracket
+        assertThrowsIllegalArgument("localhost(name)"); // no value for name
+        assertThrowsIllegalArgument("localhost(name=)"); // no value for name
+    }
+    
+
+    @Test
+    public void testAcceptsList() {
+        List<Location> l = getLocationResolver().resolve(ImmutableList.of("localhost"));
+        assertEquals(l.size(), 1, "l="+l);
+        assertTrue(l.get(0) instanceof LocalhostMachineProvisioningLocation, "l="+l);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testRegistryCommaResolution() throws NoMachinesAvailableException {
+        List<Location> l;
+        l = getLocationResolver().resolve(JavaStringEscapes.unwrapJsonishListIfPossible("localhost,localhost,localhost"));
+        assertEquals(l.size(), 3, "l="+l);
+        assertTrue(l.get(0) instanceof LocalhostMachineProvisioningLocation, "l="+l);
+        assertTrue(l.get(1) instanceof LocalhostMachineProvisioningLocation, "l="+l);
+        assertTrue(l.get(2) instanceof LocalhostMachineProvisioningLocation, "l="+l);
+
+        // And check works if comma in brackets
+        l = getLocationResolver().resolve(JavaStringEscapes.unwrapJsonishListIfPossible(
+            "[ \"byon:(hosts=\\\"192.168.0.1\\\",user=bob)\", \"byon:(hosts=\\\"192.168.0.2\\\",user=bob2)\" ]"));
+        assertEquals(l.size(), 2, "l="+l);
+        assertTrue(l.get(0) instanceof FixedListMachineProvisioningLocation, "l="+l);
+        assertTrue(l.get(1) instanceof FixedListMachineProvisioningLocation, "l="+l);
+        assertEquals(((FixedListMachineProvisioningLocation<SshMachineLocation>)l.get(0)).obtain().getUser(), "bob");
+        assertEquals(((FixedListMachineProvisioningLocation<SshMachineLocation>)l.get(1)).obtain().getUser(), "bob2");
+    }
+
+    @Test(expectedExceptions={NoSuchElementException.class})
+    public void testRegistryCommaResolutionInListNotAllowed1() throws NoMachinesAvailableException {
+        // disallowed since 0.7.0
+        getLocationResolver().resolve(ImmutableList.of("localhost,localhost,localhost"));
+    }
+
+    @Test(expectedExceptions={IllegalArgumentException.class})
+    public void testRegistryCommaResolutionInListNotAllowed2() throws NoMachinesAvailableException {
+        // disallowed since 0.7.0
+        // fails because it interprets the entire string as a single spec, which does not parse
+        getLocationResolver().resolve(ImmutableList.of("localhost(),localhost()"));
+    }
+
+    @Test(expectedExceptions={IllegalArgumentException.class})
+    public void testRegistryCommaResolutionInListNotAllowed3() throws NoMachinesAvailableException {
+        // disallowed since 0.7.0
+        // fails because it interprets the entire string as a single spec, which does not parse
+        getLocationResolver().resolve(ImmutableList.of("localhost(name=a),localhost(name=b)"));
+    }
+
+    @Test(expectedExceptions={IllegalArgumentException.class})
+    public void testDoesNotAcceptsListOLists() {
+        ((BasicLocationRegistry)managementContext.getLocationRegistry()).resolve(ImmutableList.of(ImmutableList.of("localhost")));
+    }
+
+    @Test
+    public void testResolvesExplicitName() throws Exception {
+        Location location = resolve("localhost(name=myname)");
+        assertTrue(location instanceof LocalhostMachineProvisioningLocation);
+        assertEquals(location.getDisplayName(), "myname");
+    }
+    
+    @Test
+    public void testWithOldStyleColon() throws Exception {
+        Location location = resolve("localhost:(name=myname)");
+        assertTrue(location instanceof LocalhostMachineProvisioningLocation);
+        assertEquals(location.getDisplayName(), "myname");
+    }
+    
+    @Test
+    public void testResolvesPropertiesInSpec() throws Exception {
+        LocationInternal location = resolve("localhost(privateKeyFile=myprivatekeyfile,name=myname)");
+        assertTrue(location instanceof LocalhostMachineProvisioningLocation);
+        assertEquals(location.getDisplayName(), "myname");
+        assertEquals(location.config().getBag().getStringKey("privateKeyFile"), "myprivatekeyfile");
+    }
+    
+    @Test
+    public void testResolvesDefaultName() throws Exception {
+        Location location = resolve("localhost");
+        assertTrue(location instanceof LocalhostMachineProvisioningLocation);
+        assertEquals(location.getDisplayName(), "localhost");
+
+        Location location2 = resolve("localhost()");
+        assertTrue(location2 instanceof LocalhostMachineProvisioningLocation);
+        assertEquals(location2.getDisplayName(), "localhost");
+    }
+    
+    private BasicLocationRegistry getLocationResolver() {
+        return (BasicLocationRegistry) managementContext.getLocationRegistry();
+    }
+    
+    private LocationInternal resolve(String val) {
+        Location l = managementContext.getLocationRegistry().resolve(val);
+        Assert.assertNotNull(l);
+        return (LocationInternal) l;
+    }
+    
+    private void assertThrowsNoSuchElement(String val) {
+        try {
+            resolve(val);
+            fail();
+        } catch (NoSuchElementException e) {
+            // success
+        }
+
+        // and check the long form returns an Absent (not throwing)
+        Assert.assertTrue(managementContext.getLocationRegistry().resolve(val, false, null).isAbsent());
+    }
+    
+    private void assertThrowsIllegalArgument(String val) {
+        try {
+            resolve(val);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // success
+        }
+        
+        // and check the long form returns an Absent (not throwing)
+        Assert.assertTrue(managementContext.getLocationRegistry().resolve(val, false, null).isAbsent());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostMachineProvisioningLocationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostMachineProvisioningLocationTest.java b/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostMachineProvisioningLocationTest.java
new file mode 100644
index 0000000..4735350
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostMachineProvisioningLocationTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.location.core.localhost;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
+
+import java.net.ServerSocket;
+
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.net.Networking;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.api.location.PortRange;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.location.core.PortRanges;
+import org.apache.brooklyn.location.geo.HostGeoInfo;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+
+public class LocalhostMachineProvisioningLocationTest {
+
+    private static final Logger log = LoggerFactory.getLogger(LocalhostMachineProvisioningLocationTest.class);
+    
+    private LocalManagementContext mgmt;
+
+    @BeforeMethod
+    @AfterClass
+    protected void clearStatics() {
+        LocalhostMachineProvisioningLocation.clearStaticData();
+    }
+    
+    @BeforeClass
+    protected void setup() {
+        mgmt = LocalManagementContextForTests.newInstance();
+    }
+    
+    @AfterClass
+    protected void teardown() {
+        Entities.destroyAll(mgmt);
+    }
+    
+    protected LocalhostMachineProvisioningLocation newLocalhostProvisioner() {
+        return mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class));
+    }
+    
+    protected LocalhostMachineProvisioningLocation newLocalhostProvisionerWithAddress(String address) {
+        return mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+            .configure("address", address));
+    }
+    
+    @Test
+    public void defaultInvocationCanProvisionALocalhostInstance() throws Exception {
+        LocalhostMachineProvisioningLocation provisioner = mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class));
+        SshMachineLocation machine = provisioner.obtain();
+        assertNotNull(machine);
+        assertEquals(machine.getAddress(), Networking.getLocalHost());
+    }
+
+    @Test
+    public void testUsesLocationNameProvided() throws Exception {
+        LocalhostMachineProvisioningLocation provisioner = newLocalhostProvisionerWithAddress("localhost");
+        assertEquals(((SshMachineLocation)provisioner.obtain()).getAddress().getHostName(), "localhost");
+
+        LocalhostMachineProvisioningLocation provisioner2 = newLocalhostProvisionerWithAddress("1.2.3.4");
+        assertEquals(((SshMachineLocation)provisioner2.obtain()).getAddress().getHostName(), "1.2.3.4");
+        
+        LocalhostMachineProvisioningLocation provisioner3 = newLocalhostProvisionerWithAddress("127.0.0.1");
+        assertEquals(((SshMachineLocation)provisioner3.obtain()).getAddress().getHostName(), "127.0.0.1");
+    }
+    
+    public void provisionWithASpecificNumberOfInstances() throws NoMachinesAvailableException {
+        LocalhostMachineProvisioningLocation provisioner = mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+            .configure("count", 2));
+
+        // first machine
+        SshMachineLocation first = provisioner.obtain();
+        assertNotNull(first);
+        assertEquals(first.getAddress(), Networking.getLocalHost());
+
+        // second machine
+        SshMachineLocation second = provisioner.obtain();
+        assertNotNull(second);
+        assertEquals(second.getAddress(), Networking.getLocalHost());
+
+        // third machine - fails
+        try {
+            SshMachineLocation third = provisioner.obtain();
+            fail("did not throw expected exception; got "+third);
+        } catch (NoMachinesAvailableException e) {
+            /* expected */
+        }
+    }
+    
+    @Test
+    public void obtainTwoAddressesInRangeThenDontObtain() throws Exception {
+        LocalhostMachineProvisioningLocation p = newLocalhostProvisioner();
+        SshMachineLocation m = p.obtain();
+
+        // Find two ports that are free, rather than risk false-negatives if a port was left open by something else.
+        int start = 48311;
+        while (true) {
+            if (Networking.isPortAvailable(m.getAddress(), start) && Networking.isPortAvailable(m.getAddress(), start+1)) {
+                break;
+            } else {
+                start++;
+            }
+        }
+        PortRange r = PortRanges.fromString(""+start+"-"+(start+1));
+        
+        try {
+            int i1 = m.obtainPort(r);
+            Assert.assertEquals(i1, start);
+            int i2 = m.obtainPort(r);
+            Assert.assertEquals(i2, start+1);
+            
+            //should fail
+            int i3 = m.obtainPort(r);
+            Assert.assertEquals(i3, -1);
+
+            //releasing and reapplying should succed
+            m.releasePort(i2);
+            int i4 = m.obtainPort(r);
+            Assert.assertEquals(i4, i2);
+
+        } finally {
+            m.releasePort(start);
+            m.releasePort(start+1);
+        }
+    }
+    
+    @Test
+    public void obtainLowNumberedPortsAutomatically() throws Exception {
+        LocalhostMachineProvisioningLocation p = newLocalhostProvisioner();
+        SshMachineLocation m = p.obtain();
+        int start = 983;  //random rarely used port, not that it matters
+        try {
+            int actual = m.obtainPort(PortRanges.fromInteger(start));
+            Assert.assertEquals(actual, start);
+        } finally {
+            m.releasePort(start);
+        }
+
+    }
+
+    @Test
+    public void obtainPortFailsIfInUse() throws Exception {
+        LocalhostMachineProvisioningLocation p = newLocalhostProvisioner();
+        SshMachineLocation m = p.obtain();
+        
+        // Find two ports that are free, rather than risk false-negatives if a port was left open by something else.
+        int start = 48311;
+        while (true) {
+            if (Networking.isPortAvailable(m.getAddress(), start) && Networking.isPortAvailable(m.getAddress(), start+1)) {
+                break;
+            } else {
+                start++;
+            }
+        }
+        PortRange r = PortRanges.fromString(""+start+"-"+(start+1));
+
+        ServerSocket ss = null;
+        try {
+            ss = new ServerSocket(start, 0, m.getAddress());
+            int i1 = m.obtainPort(r);
+            Assert.assertEquals(i1, start+1);
+        } finally {
+            if (ss!=null) ss.close();
+            m.releasePort(start);
+            m.releasePort(start+1);
+        }
+    }
+
+    @Test
+    public void obtainLocationWithGeography() throws Exception {
+        mgmt.getBrooklynProperties().put("brooklyn.location.named.lhx", "localhost");
+        // bogus location so very little chance of it being what maxmind returns!
+        mgmt.getBrooklynProperties().put("brooklyn.location.named.lhx.latitude", 42d);
+        mgmt.getBrooklynProperties().put("brooklyn.location.named.lhx.longitude", -20d);
+        MachineProvisioningLocation<?> p = (MachineProvisioningLocation<?>) mgmt.getLocationRegistry().resolve("named:lhx");
+        SshMachineLocation m = (SshMachineLocation) p.obtain(MutableMap.of());
+        HostGeoInfo geo = HostGeoInfo.fromLocation(m);
+        log.info("Geo info for "+m+" is: "+geo);
+        Assert.assertEquals(geo.latitude, 42d, 0.00001);
+        Assert.assertEquals(geo.longitude, -20d, 0.00001);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostProvisioningAndAccessTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostProvisioningAndAccessTest.java b/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostProvisioningAndAccessTest.java
new file mode 100644
index 0000000..3bb594e
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/core/localhost/LocalhostProvisioningAndAccessTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.core.localhost;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class LocalhostProvisioningAndAccessTest {
+
+    private LocalManagementContext mgmt;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        mgmt = new LocalManagementContext(BrooklynProperties.Factory.newDefault());
+    }
+    
+    @AfterMethod(alwaysRun = true)
+    public void tearDown(){
+        if (mgmt != null) Entities.destroyAll(mgmt);
+    }
+
+    @Test(groups="Integration")
+    public void testProvisionAndConnect() throws Exception {
+        Location location = mgmt.getLocationRegistry().resolve("localhost");
+        assertTrue(location instanceof LocalhostMachineProvisioningLocation);
+        SshMachineLocation m = ((LocalhostMachineProvisioningLocation)location).obtain();
+        int result = m.execCommands("test", Arrays.asList("echo hello world"));
+        assertEquals(result, 0);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoInfoTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoInfoTest.java b/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoInfoTest.java
index 2ca5807..56c1b41 100644
--- a/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoInfoTest.java
+++ b/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoInfoTest.java
@@ -22,7 +22,7 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertNotNull;
 
 import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.location.basic.SimulatedLocation;
+import org.apache.brooklyn.location.core.SimulatedLocation;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.testng.annotations.Test;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoLookupIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoLookupIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoLookupIntegrationTest.java
index c80534f..1990905 100644
--- a/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoLookupIntegrationTest.java
+++ b/core/src/test/java/org/apache/brooklyn/location/geo/HostGeoLookupIntegrationTest.java
@@ -24,8 +24,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.Test;
-import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
-import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.util.time.Duration;
 
 import com.google.common.base.Objects;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationIntegrationTest.java
new file mode 100644
index 0000000..cd9f724
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationIntegrationTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.location.ssh;
+
+import java.io.ByteArrayOutputStream;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.crypto.SecureKeys;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
+import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool.SshjToolBuilder;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Preconditions;
+
+import static org.testng.Assert.assertEquals;
+
+public class SshMachineLocationIntegrationTest {
+
+    protected TestApplication app;
+    protected ManagementContext mgmt;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setup() throws Exception {
+        mgmt = LocalManagementContextForTests.builder(true)
+            .useDefaultProperties()
+            .build();
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (mgmt != null) Entities.destroyAll(mgmt);
+        mgmt = null;
+    }
+
+    // Note: requires `named:localhost-passphrase` set up with a key whose passphrase is "localhost"
+    // * create the key with:
+    //      ssh-keygen -t rsa -N "brooklyn" -f ~/.ssh/id_rsa_passphrase
+    //      ssh-copy-id localhost
+    // * create brooklyn.properties, containing:
+    //      brooklyn.location.named.localhost-passphrase=localhost
+    //      brooklyn.location.named.localhost-passphrase.privateKeyFile=~/.ssh/id_rsa_passphrase
+    //      brooklyn.location.named.localhost-passphrase.privateKeyPassphrase=brooklyn
+    @Test(groups = "Integration")
+    public void testExtractingConnectablePassphraselessKey() throws Exception {
+        LocalhostMachineProvisioningLocation lhp = (LocalhostMachineProvisioningLocation) mgmt.getLocationRegistry().resolve("named:localhost-passphrase", true, null).orNull();
+        Preconditions.checkNotNull(lhp, "This test requires a localhost named location called 'localhost-passphrase' (which should have a passphrase set)");
+        SshMachineLocation sm = lhp.obtain();
+        
+        SshjToolBuilder builder = SshjTool.builder().host(sm.getAddress().getHostName()).user(sm.getUser());
+        
+        KeyPair data = sm.findKeyPair();
+        if (data!=null) builder.privateKeyData(SecureKeys.toPem(data));
+        String password = sm.findPassword();
+        if (password!=null) builder.password(password);
+        SshjTool tool = builder.build();
+        tool.connect();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        int result = tool.execCommands(MutableMap.<String,Object>of("out", out), Arrays.asList("date"));
+        Assert.assertTrue(out.toString().contains(" 20"), "out="+out);
+        assertEquals(result, 0);
+    }
+
+    @Test(groups = "Integration")
+    public void testExecScriptScriptDirFlagIsRespected() throws Exception {
+        // For explanation of (some of) the magic behind this command, see http://stackoverflow.com/a/229606/68898
+        final String command = "if [[ \"$0\" == \"/var/tmp/\"* ]]; then true; else false; fi";
+
+        LocalhostMachineProvisioningLocation lhp = (LocalhostMachineProvisioningLocation) mgmt.getLocationRegistry().resolve("localhost", true, null).orNull();
+        SshMachineLocation sm = lhp.obtain();
+
+        Map<String, Object> props = ImmutableMap.<String, Object>builder()
+                .put(SshTool.PROP_SCRIPT_DIR.getName(), "/var/tmp")
+                .build();
+        int rc = sm.execScript(props, "Test script directory execution", ImmutableList.of(command));
+        assertEquals(rc, 0);
+    }
+
+    @Test(groups = "Integration")
+    public void testLocationScriptDirConfigIsRespected() throws Exception {
+        // For explanation of (some of) the magic behind this command, see http://stackoverflow.com/a/229606/68898
+        final String command = "if [[ \"$0\" == \"/var/tmp/\"* ]]; then true; else false; fi";
+
+        Map<String, Object> locationConfig = ImmutableMap.<String, Object>builder()
+                .put(SshMachineLocation.SCRIPT_DIR.getName(), "/var/tmp")
+                .build();
+
+        LocalhostMachineProvisioningLocation lhp = (LocalhostMachineProvisioningLocation) mgmt.getLocationRegistry().resolve("localhost", locationConfig);
+        SshMachineLocation sm = lhp.obtain();
+
+        int rc = sm.execScript("Test script directory execution", ImmutableList.of(command));
+        assertEquals(rc, 0);
+    }
+    
+    @Test(groups = "Integration")
+    public void testMissingLocationScriptDirIsAlsoOkay() throws Exception {
+        final String command = "echo hello";
+
+        Map<String, Object> locationConfig = ImmutableMap.<String, Object>builder()
+//                .put(SshMachineLocation.SCRIPT_DIR.getName(), "/var/tmp")
+                .build();
+
+        LocalhostMachineProvisioningLocation lhp = (LocalhostMachineProvisioningLocation) mgmt.getLocationRegistry().resolve("localhost", locationConfig);
+        SshMachineLocation sm = lhp.obtain();
+
+        int rc = sm.execScript("Test script directory execution", ImmutableList.of(command));
+        assertEquals(rc, 0);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationPerformanceTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationPerformanceTest.java b/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationPerformanceTest.java
new file mode 100644
index 0000000..27ca938
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationPerformanceTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.location.ssh;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.test.PerformanceTestUtils;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.net.Networking;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.text.Identifiers;
+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 org.testng.annotations.Test;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+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;
+
+/**
+ * Test the performance of different variants of invoking the sshj tool.
+ * 
+ * Intended for human-invocation and inspection, to see which parts are most expensive.
+ */
+public class SshMachineLocationPerformanceTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SshMachineLocationPerformanceTest.class);
+    
+    private SshMachineLocation machine;
+    private ListeningExecutorService executor;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        machine = new SshMachineLocation(MutableMap.of("address", "localhost"));
+        executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void afterMethod() throws Exception {
+        if (executor != null) executor.shutdownNow();
+        Streams.closeQuietly(machine);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveSmallCommands() throws Exception {
+        runExecManyCommands(ImmutableList.of("true"), "small-cmd", 10);
+    }
+
+    // Mimics SshSensorAdapter's polling
+    @Test(groups = {"Integration"})
+    public void testConsecutiveSmallCommandsWithCustomStdoutAndErr() throws Exception {
+        final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+        final ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+        
+        Runnable task = new Runnable() {
+            @Override public void run() {
+                machine.execScript(ImmutableMap.of("out", stdout, "err", stderr), "test", ImmutableList.of("true"));
+            }};
+        runMany(task, "small-cmd-custom-stdout", 1, 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConcurrentSmallCommands() throws Exception {
+        runExecManyCommands(ImmutableList.of("true"), "small-cmd", 10, 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveBigStdoutCommands() throws Exception {
+        runExecManyCommands(ImmutableList.of("head -c 100000 /dev/urandom"), "big-stdout", 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveBigStdinCommands() throws Exception {
+        String bigstr = Identifiers.makeRandomId(100000);
+        runExecManyCommands(ImmutableList.of("echo "+bigstr+" | wc -c"), "big-stdin", 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveSmallCommandsWithDifferentProperties() throws Exception {
+        final Map<String, ?> emptyProperties = Collections.emptyMap();
+        final Map<String, ?> customProperties = MutableMap.of(
+                "address", Networking.getLocalHost(),
+                SshTool.PROP_SESSION_TIMEOUT.getName(), 20000,
+                SshTool.PROP_CONNECT_TIMEOUT.getName(), 50000,
+                SshTool.PROP_SCRIPT_HEADER.getName(), "#!/bin/bash");
+
+        Runnable task = new Runnable() {
+            @Override public void run() {
+                if (Math.random() < 0.5) {
+                    machine.execScript(emptyProperties, "test", ImmutableList.of("true"));
+                } else {
+                    machine.execScript(customProperties, "test", ImmutableList.of("true"));
+                }
+            }};
+        runMany(task, "small-cmd-custom-ssh-properties", 1, 10);
+    }
+
+    private void runExecManyCommands(final List<String> cmds, String context, int iterations) throws Exception {
+        runExecManyCommands(cmds, context, 1, iterations);
+    }
+    
+    private void runExecManyCommands(final List<String> cmds, String context, int concurrentRuns, int iterations) throws Exception {
+        Runnable task = new Runnable() {
+                @Override public void run() {
+                    execScript(cmds);
+                }};
+        runMany(task, context, concurrentRuns, iterations);
+    }
+    
+    private void runMany(final Runnable task, final String context, int concurrentRuns, int iterations) throws Exception {
+        long preCpuTime = PerformanceTestUtils.getProcessCpuTime();
+        Stopwatch stopwatch = Stopwatch.createStarted();
+
+        for (int i = 0; i < iterations; i++) {
+            List<ListenableFuture<?>> futures = Lists.newArrayList();
+            for (int j = 0; j < concurrentRuns; j++) {
+                futures.add(executor.submit(new Runnable() {
+                    public void run() {
+                        try {
+                            task.run();
+                        } catch (Exception e) {
+                            LOG.error("Error for "+context+", executing "+task, e);
+                            throw Throwables.propagate(e);
+                        }
+                    }}));
+            }
+            Futures.allAsList(futures).get();
+            
+            long postCpuTime = PerformanceTestUtils.getProcessCpuTime();
+            long elapsedTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+            double fractionCpu = (elapsedTime > 0) ? ((double)postCpuTime-preCpuTime) / TimeUnit.MILLISECONDS.toNanos(elapsedTime) : -1;
+            LOG.info("Executing {}; completed {}; took {}; fraction cpu {}",
+                    new Object[] {context, (i+1), Time.makeTimeStringRounded(elapsedTime), fractionCpu});
+        }
+    }
+
+    private int execScript(List<String> cmds) {
+        return machine.execScript("mysummary", cmds);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationReuseIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationReuseIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationReuseIntegrationTest.java
new file mode 100644
index 0000000..e129dc1
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/ssh/SshMachineLocationReuseIntegrationTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.location.ssh;
+
+import static org.testng.Assert.assertEquals;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
+import org.apache.brooklyn.util.net.Networking;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Tests the re-use of SshTools in SshMachineLocation
+ */
+public class SshMachineLocationReuseIntegrationTest {
+
+    public static class RecordingSshjTool extends SshjTool {
+        public static final AtomicBoolean forbidden = new AtomicBoolean(false); 
+        public static final AtomicInteger connectionCount = new AtomicInteger(0);
+        public static final AtomicInteger disconnectionCount = new AtomicInteger();
+        
+        public RecordingSshjTool(Map<String, ?> map) {
+            super(map);
+        }
+
+        @Override
+        public void connect() {
+            if (forbidden.get()) throw new IllegalStateException("forbidden at this time");
+            connectionCount.incrementAndGet();
+            super.connect();
+        }
+
+        @Override
+        public void disconnect() {
+            disconnectionCount.incrementAndGet();
+            super.disconnect();
+        }
+
+        public static void reset() {
+            forbidden.set(false);
+            connectionCount.set(0);
+            disconnectionCount.set(0);
+        }
+
+        @Override
+        public int execCommands(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
+            if (forbidden.get()) throw new IllegalStateException("forbidden at this time");
+            return super.execCommands(props, commands, env);
+        }
+        
+        @Override
+        public int execScript(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
+            if (forbidden.get()) throw new IllegalStateException("forbidden at this time");
+            return super.execScript(props, commands, env);
+        }
+        
+        @Override
+        public int execShellDirect(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
+            if (forbidden.get()) throw new IllegalStateException("forbidden at this time");
+            return super.execShellDirect(props, commands, env);
+        }
+    }
+
+    private SshMachineLocation host;
+    private LocalManagementContext managementContext;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = new LocalManagementContext();
+        host = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", Networking.getLocalHost())
+                .configure(SshTool.PROP_TOOL_CLASS, RecordingSshjTool.class.getName()));
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (host != null) Streams.closeQuietly(host);
+        if (managementContext != null) Entities.destroyAll(managementContext);
+        RecordingSshjTool.reset();
+    }
+
+    @Test(groups = "Integration")
+    public void testBasicReuse() throws Exception {
+        host.execScript("mysummary", ImmutableList.of("exit"));
+        host.execScript("mysummary", ImmutableList.of("exit"));
+        assertEquals(RecordingSshjTool.connectionCount.get(), 1, "Expected one SSH connection to have been recorded");
+    }
+
+    @Test(groups = "Integration")
+    public void testReuseWithInterestingProps() throws Exception {
+        host.execScript(customSshConfigKeys(), "mysummary", ImmutableList.of("exit"));
+        host.execScript(customSshConfigKeys(), "mysummary", ImmutableList.of("exit"));
+        assertEquals(RecordingSshjTool.connectionCount.get(), 1, "Expected one SSH connection to have been recorded");
+    }
+
+    @Test(groups = "Integration")
+    public void testNewConnectionForDifferentProps() throws Exception {
+        host.execScript("mysummary", ImmutableList.of("exit"));
+        host.execScript(customSshConfigKeys(), "mysummary", ImmutableList.of("exit"));
+        assertEquals(RecordingSshjTool.connectionCount.get(), 2, "Expected two SSH connections to have been recorded");
+    }
+
+    @Test(groups = "Integration")
+    public void testSshToolReusedWhenConfigDiffers() throws Exception {
+        Map<String, Object> props = customSshConfigKeys();
+        host.execScript(props, "mysummary", ImmutableList.of("exit"));
+
+        // Use another output stream for second request
+        props.put(SshTool.PROP_SCRIPT_HEADER.getName(), "#!/bin/bash -e\n");
+        host.execScript(props, "mysummary", ImmutableList.of("exit"));
+        assertEquals(RecordingSshjTool.connectionCount.get(), 1, "Expected one SSH connection to have been recorded even though out script header differed.");
+    }
+
+    @Test(groups = "Integration")
+    public void testSshCacheExpiresEvenIfNotUsed() throws Exception {
+        SshMachineLocation host2 = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", InetAddress.getLocalHost())
+                .configure(SshMachineLocation.SSH_CACHE_EXPIRY_DURATION, Duration.ONE_SECOND)
+                .configure(SshTool.PROP_TOOL_CLASS, RecordingSshjTool.class.getName()));
+        
+        Map<String, Object> props = customSshConfigKeys();
+        host2.execScript(props, "mysummary", ImmutableList.of("exit"));
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                assertEquals(RecordingSshjTool.disconnectionCount.get(), 1);
+            }});
+    }
+
+    public Map<String, Object> customSshConfigKeys() throws UnknownHostException {
+        return MutableMap.<String, Object>of(
+                "address", Networking.getLocalHost(),
+                SshTool.PROP_SESSION_TIMEOUT.getName(), 20000,
+                SshTool.PROP_CONNECT_TIMEOUT.getName(), 50000,
+                SshTool.PROP_SCRIPT_HEADER.getName(), "#!/bin/bash");
+    }
+}