You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ge...@apache.org on 2017/02/24 15:18:14 UTC

[3/5] brooklyn-server git commit: LocationNetworkInfoCustomizer

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/3fd64c89/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolverTest.java
----------------------------------------------------------------------
diff --git a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolverTest.java b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolverTest.java
new file mode 100644
index 0000000..33a019a
--- /dev/null
+++ b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolverTest.java
@@ -0,0 +1,262 @@
+/*
+ * 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.jclouds;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Set;
+
+import org.apache.brooklyn.location.jclouds.DefaultConnectivityResolver.NetworkMode;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
+import org.apache.brooklyn.util.core.internal.winrm.RecordingWinRmTool;
+import org.apache.brooklyn.util.time.Duration;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.NodeMetadataBuilder;
+import org.jclouds.domain.LoginCredentials;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.net.HostAndPort;
+
+public class DefaultConnectivityResolverTest extends AbstractJcloudsStubbedUnitTest {
+
+    private final LoginCredentials credential = LoginCredentials.builder().user("AzureDiamond").password("hunter2").build();
+
+    @Test
+    public void testRespectsHostAndPortOverride() throws Exception {
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of());
+        // ideally would confirm that no credentials are tested either.
+        ConnectivityResolverOptions options = newResolveOptions()
+                .portForwardSshOverride(HostAndPort.fromParts("10.1.1.4", 4361))
+                .build();
+        DefaultConnectivityResolver customizer = new DefaultConnectivityResolver();
+        ManagementAddressResolveResult result = customizer.resolve(jcloudsLocation, newNodeMetadata(), ConfigBag.newInstance(), options);
+        assertEquals(result.hostAndPort().getHostText(), "10.1.1.4");
+        assertEquals(result.hostAndPort().getPort(), 4361);
+    }
+
+    @Test
+    public void testObtainsHostnameFromAwsMachine() throws Exception {
+        final String expectedHostname = "ec2-awshostname";
+        RecordingSshTool.setCustomResponse(".*curl.*169.254.169.254.*", new RecordingSshTool.CustomResponse(0, expectedHostname, ""));
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of(
+                JcloudsLocationConfig.LOOKUP_AWS_HOSTNAME, true));
+        ConnectivityResolverOptions options = newResolveOptions()
+                .waitForConnectable(true)
+                .pollForReachableAddresses(Predicates.<HostAndPort>alwaysTrue(), Duration.ONE_SECOND, true)
+                .userCredentials(credential)
+                .build();
+        DefaultConnectivityResolver customizer = new DefaultConnectivityResolver();
+        ConfigBag configBag = ConfigBag.newInstance();
+        ManagementAddressResolveResult result = customizer.resolve(
+                jcloudsLocation, newNodeMetadata(), configBag, options);
+        assertEquals(result.hostAndPort().getHostText(), expectedHostname);
+    }
+
+    @Test
+    public void testTestCredentialWithLinuxMachine() throws Exception {
+        final String allowedUser = "Mr. Big";
+        // Match every command.
+        RecordingSshTool.setCustomResponse(".*", new RecordingSshTool.CustomResponseGenerator() {
+            @Override
+            public RecordingSshTool.CustomResponse generate(RecordingSshTool.ExecParams execParams) throws Exception {
+                boolean valid = allowedUser.equals(execParams.constructorProps.get("user"));
+                return new RecordingSshTool.CustomResponse(valid ? 0 : 1, "", "");
+            }
+        });
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of());
+        DefaultConnectivityResolver customizer = new DefaultConnectivityResolver();
+        final ConfigBag config = ConfigBag.newInstance(ImmutableMap.of(
+                JcloudsLocationConfig.WAIT_FOR_SSHABLE, "1ms"));
+        assertTrue(customizer.checkCredential(
+                jcloudsLocation, HostAndPort.fromParts("10.0.0.234", 22),
+                LoginCredentials.builder().user(allowedUser).password("password1").build(), config, false));
+        assertFalse(customizer.checkCredential(
+                jcloudsLocation, HostAndPort.fromParts("10.0.0.234", 22), credential, config, false));
+    }
+
+    @Test
+    public void testTestCredentialWithWindowsMachine() throws Exception {
+        final String allowedUser = "Mr. Big";
+        // Match every command.
+        RecordingWinRmTool.setCustomResponse(".*", new RecordingWinRmTool.CustomResponseGenerator() {
+            @Override
+            public RecordingWinRmTool.CustomResponse generate(RecordingWinRmTool.ExecParams execParams) {
+                boolean valid = allowedUser.equals(execParams.constructorProps.get("user"));
+                return new RecordingWinRmTool.CustomResponse(valid ? 0 : 1, "", "");
+            }
+        });
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of());
+        DefaultConnectivityResolver customizer = new DefaultConnectivityResolver();
+        final ConfigBag config = ConfigBag.newInstance(ImmutableMap.of(
+                JcloudsLocationConfig.WAIT_FOR_WINRM_AVAILABLE, "1ms"));
+        assertTrue(customizer.checkCredential(
+                jcloudsLocation, HostAndPort.fromParts("10.0.0.234", 22),
+                LoginCredentials.builder().user(allowedUser).password("password1").build(), config, true));
+        assertFalse(customizer.checkCredential(
+                jcloudsLocation, HostAndPort.fromParts("10.0.0.234", 22), credential, config, true));
+    }
+
+    /**
+     * e.g. case when brooklyn on laptop provisions in one location then rebinds later
+     * from another location where the ip happens to resolve to a different machine.
+     */
+    @Test
+    public void testResolveChecksCredentials() throws Exception {
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of());
+        final HostAndPort authorisedHostAndPort = HostAndPort.fromParts("10.0.0.2", 22);
+        final HostAndPort otherHostAndPort = HostAndPort.fromParts("10.0.0.1", 22);
+        final Set<HostAndPort> reachableIps = Sets.newHashSet(authorisedHostAndPort, otherHostAndPort);
+
+        // Checks arguments and exits 0 if host+port match authorised.
+        RecordingSshTool.setCustomResponse(".*", new RecordingSshTool.CustomResponseGenerator() {
+            @Override
+            public RecordingSshTool.CustomResponse generate(RecordingSshTool.ExecParams execParams) throws Exception {
+                HostAndPort hap = HostAndPort.fromParts(
+                        (String) execParams.constructorProps.get("host"),
+                        (Integer) execParams.constructorProps.get("port"));
+                int exitCode = authorisedHostAndPort.equals(hap) ? 0 : 1;
+                return new RecordingSshTool.CustomResponse(exitCode, "", "");
+            }
+        });
+        ConfigBag config = ConfigBag.newInstance(ImmutableMap.of(
+                JcloudsLocationConfig.LOOKUP_AWS_HOSTNAME, false,
+                JcloudsLocationConfig.WAIT_FOR_SSHABLE, "1ms",
+                JcloudsLocation.CUSTOM_CREDENTIALS, credential));
+        ConnectivityResolverOptions options = newResolveOptions()
+                .pollForReachableAddresses(Predicates.in(reachableIps), Duration.ONE_SECOND, true)
+                .build();
+
+        // Chooses authorisedHostAndPort when credentials are tested.
+        DefaultConnectivityResolver customizer = new DefaultConnectivityResolver(ImmutableMap.of(
+                DefaultConnectivityResolver.CHECK_CREDENTIALS, true));
+
+        ManagementAddressResolveResult result = customizer.resolve(jcloudsLocation, newNodeMetadata(), config, options);
+        assertEquals(result.hostAndPort(), authorisedHostAndPort);
+        assertFalse(RecordingSshTool.getExecCmds().isEmpty(), "expected ssh connection to be made when testing credentials");
+
+        // Chooses otherHostAndPort when credentials aren't tested.
+        RecordingSshTool.clear();
+        customizer = new DefaultConnectivityResolver(ImmutableMap.of(
+                DefaultConnectivityResolver.CHECK_CREDENTIALS, false));
+        result = customizer.resolve(jcloudsLocation, newNodeMetadata(), config, options);
+        assertEquals(result.hostAndPort(), otherHostAndPort);
+        assertTrue(RecordingSshTool.getExecCmds().isEmpty(),
+                "expected no ssh connection to be made when not testing credentials: " + Iterables.toString(RecordingSshTool.getExecCmds()));
+    }
+
+    @DataProvider(name = "testModeDataProvider")
+    public Object[][] testModeDataProvider() throws Exception {
+        return new Object[][]{
+                new Object[]{NetworkMode.ONLY_PUBLIC, haps("10.0.0.2:22", "192.168.0.1:22", "192.168.0.2:22"), "10.0.0.2"},
+                new Object[]{NetworkMode.ONLY_PRIVATE, haps("10.0.0.1:22", "10.0.0.2:22", "192.168.0.1:22"), "192.168.0.1"},
+                new Object[]{NetworkMode.PREFER_PRIVATE, haps("10.0.0.2:22", "192.168.0.2:22"), "192.168.0.2"},
+                new Object[]{NetworkMode.PREFER_PUBLIC, haps("10.0.0.2:22", "192.168.0.1:22", "192.168.0.2:22"), "10.0.0.2"},
+                // Preference unavailable so falls back to next best.
+                new Object[]{NetworkMode.PREFER_PRIVATE, haps("10.0.0.1:22"), "10.0.0.1"},
+                new Object[]{NetworkMode.PREFER_PUBLIC, haps("192.168.0.1:22"), "192.168.0.1"},
+        };
+    }
+
+    @Test(dataProvider = "testModeDataProvider")
+    public void testMode(NetworkMode mode, Set<HostAndPort> reachableIps, String expectedIp) throws Exception {
+        final DefaultConnectivityResolver customizer = new DefaultConnectivityResolver(ImmutableMap.of(
+                DefaultConnectivityResolver.NETWORK_MODE, mode,
+                DefaultConnectivityResolver.CHECK_CREDENTIALS, false));
+
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of(
+                JcloudsLocationConfig.CONNECTIVITY_RESOLVER, customizer));
+
+        ConnectivityResolverOptions options = newResolveOptions()
+                .pollForReachableAddresses(Predicates.in(reachableIps), Duration.ONE_SECOND, true)
+                .build();
+        ConfigBag configBag = ConfigBag.newInstance();
+
+        ManagementAddressResolveResult result = customizer.resolve(jcloudsLocation, newNodeMetadata(), configBag, options);
+        assertEquals(result.hostAndPort().getHostText(), expectedIp);
+    }
+
+    @DataProvider(name = "fallibleModes")
+    public Object[][] testFallibleModesDataProvider() throws Exception {
+        return new Object[][]{
+                new Object[]{NetworkMode.ONLY_PUBLIC, haps("192.168.0.1:22")},
+                new Object[]{NetworkMode.ONLY_PRIVATE, haps("10.0.0.1:22")},
+                new Object[]{NetworkMode.PREFER_PUBLIC, haps()},
+                new Object[]{NetworkMode.PREFER_PRIVATE, haps()}
+        };
+    }
+
+    /**
+     * Tests behaviour when no desired addresses are available.
+     */
+    @Test(dataProvider = "fallibleModes", expectedExceptions = IllegalStateException.class)
+    public void testModeUnavailable(NetworkMode mode, Set<HostAndPort> reachableIps) throws Exception {
+        final DefaultConnectivityResolver customizer = new DefaultConnectivityResolver(ImmutableMap.of(
+                DefaultConnectivityResolver.NETWORK_MODE, mode,
+                DefaultConnectivityResolver.CHECK_CREDENTIALS, false));
+
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(), ImmutableMap.of(
+                JcloudsLocationConfig.CONNECTIVITY_RESOLVER, customizer));
+
+        ConnectivityResolverOptions options = newResolveOptions()
+                .pollForReachableAddresses(Predicates.in(reachableIps), Duration.ONE_SECOND, true)
+                .build();
+        ConfigBag configBag = ConfigBag.newInstance();
+        customizer.resolve(jcloudsLocation, newNodeMetadata(), configBag, options);
+    }
+
+    private ConnectivityResolverOptions.Builder newResolveOptions() {
+        return ConnectivityResolverOptions.builder()
+                .initialCredentials(credential);
+    }
+
+    private NodeMetadata newNodeMetadata() {
+        return new NodeMetadataBuilder()
+                .id("id")
+                .backendStatus("backendStatus")
+                .credentials(credential)
+                .group("group")
+                .hostname("hostname")
+                .loginPort(22)
+                .name("DefaultConnectivityResolverTest")
+                .publicAddresses(ImmutableList.of("10.0.0.1", "10.0.0.2"))
+                .privateAddresses(ImmutableList.of("192.168.0.1", "192.168.0.2"))
+                .status(NodeMetadata.Status.RUNNING)
+                .tags(ImmutableList.<String>of())
+                .userMetadata(ImmutableMap.<String, String>of())
+                .build();
+    }
+
+    private Set<HostAndPort> haps(String... s) {
+        Set<HostAndPort> hap = Sets.newHashSet();
+        for (String str : s) {
+            hap.add(HostAndPort.fromString(str));
+        }
+        return hap;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/3fd64c89/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsByonLocationResolverStubbedRebindTest.java
----------------------------------------------------------------------
diff --git a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsByonLocationResolverStubbedRebindTest.java b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsByonLocationResolverStubbedRebindTest.java
index 83684eb..0921044 100644
--- a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsByonLocationResolverStubbedRebindTest.java
+++ b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsByonLocationResolverStubbedRebindTest.java
@@ -142,6 +142,8 @@ public class JcloudsByonLocationResolverStubbedRebindTest extends AbstractJcloud
         String spec = "jcloudsByon:(provider=\""+SOFTLAYER_PROVIDER+"\",region=\""+SOFTLAYER_AMS01_REGION_NAME+"\",user=\"myuser\",password=\"mypassword\",hosts=\""+nodeId+"\")";
         Map<?,?> specFlags = ImmutableMap.builder()
                 .put(JcloudsLocationConfig.COMPUTE_SERVICE_REGISTRY, computeServiceRegistry)
+                .put(JcloudsLocationConfig.WAIT_FOR_SSHABLE, Duration.ONE_SECOND.toString())
+                .put(JcloudsLocation.POLL_FOR_FIRST_REACHABLE_ADDRESS, Duration.ONE_SECOND.toString())
                 .put(JcloudsLocation.POLL_FOR_FIRST_REACHABLE_ADDRESS_PREDICATE, Predicates.alwaysTrue())
                 .build();
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/3fd64c89/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/networking/JcloudsReachableAddressStubbedTest.java
----------------------------------------------------------------------
diff --git a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/networking/JcloudsReachableAddressStubbedTest.java b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/networking/JcloudsReachableAddressStubbedTest.java
index 4c883e5..6b2b10a 100644
--- a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/networking/JcloudsReachableAddressStubbedTest.java
+++ b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/networking/JcloudsReachableAddressStubbedTest.java
@@ -34,11 +34,12 @@ import org.apache.brooklyn.location.jclouds.JcloudsLocation;
 import org.apache.brooklyn.location.jclouds.JcloudsLocationConfig;
 import org.apache.brooklyn.location.jclouds.JcloudsSshMachineLocation;
 import org.apache.brooklyn.location.jclouds.JcloudsWinRmMachineLocation;
+import org.apache.brooklyn.location.jclouds.DefaultConnectivityResolver;
+import org.apache.brooklyn.location.jclouds.ConnectivityResolver;
 import org.apache.brooklyn.location.jclouds.StubbedComputeServiceRegistry.AbstractNodeCreator;
 import org.apache.brooklyn.location.jclouds.StubbedComputeServiceRegistry.SingleNodeCreator;
 import org.apache.brooklyn.location.jclouds.networking.JcloudsPortForwardingStubbedTest.RecordingJcloudsPortForwarderExtension;
 import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
-import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
 import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.CustomResponse;
 import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.CustomResponseGenerator;
@@ -55,11 +56,14 @@ import org.jclouds.domain.LoginCredentials;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.net.HostAndPort;
 
@@ -75,6 +79,7 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
     private static final Logger LOG = LoggerFactory.getLogger(JcloudsReachableAddressStubbedTest.class);
 
     protected String reachableIp;
+    protected ImmutableSet<String> additionalReachableIps;
     protected AddressChooser addressChooser;
     protected CustomResponseGeneratorImpl customResponseGenerator;
 
@@ -83,6 +88,7 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
     public void setUp() throws Exception {
         super.setUp();
         reachableIp = null; // expect test method to set this
+        additionalReachableIps = null;
         addressChooser = new AddressChooser();
         customResponseGenerator = new CustomResponseGeneratorImpl();
     }
@@ -116,6 +122,7 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
                 .build());
 
         addressChooser.assertCalled();
+        // Expect an SSH connection to have been made when determining readiness.
         assertEquals(RecordingSshTool.getLastExecCmd().constructorProps.get(SshTool.PROP_HOST.getName()), reachableIp);
 
         assertEquals(machine.getAddress().getHostAddress(), reachableIp);
@@ -231,7 +238,7 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
 
         JcloudsSshMachineLocation machine = newMachine(ImmutableMap.<ConfigKey<?>,Object>builder()
                 .put(JcloudsLocationConfig.WAIT_FOR_SSHABLE, "false")
-                .put(JcloudsLocation.POLL_FOR_FIRST_REACHABLE_ADDRESS, Duration.millis(50).toString())
+                .put(JcloudsLocation.POLL_FOR_FIRST_REACHABLE_ADDRESS, "false")
                 .build());
 
         addressChooser.assertNotCalled();
@@ -336,7 +343,47 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
 
         assertEquals(machine.getAddress().getHostAddress(), "1.1.1.1");
     }
-    
+
+    @DataProvider(name = "publicPrivateProvider")
+    public Object[][] publicPrivateProvider() {
+        List<String> pub = ImmutableList.of("1.1.1.1", "1.1.1.2");
+        List<String> pri = ImmutableList.of("2.1.1.1", "2.1.1.2", "2.1.1.3");
+        return new Object[][]{
+                // First available public address chosen.
+                {pub, pri, DefaultConnectivityResolver.NetworkMode.PREFER_PUBLIC, "1.1.1.1", ImmutableSet.of("1.1.1.2")},
+                // public desired and reachable. private address ignored.
+                {pub, pri, DefaultConnectivityResolver.NetworkMode.PREFER_PUBLIC, "1.1.1.2", ImmutableSet.of("2.1.1.1")},
+                // public desired but unreachable. first reachable private chosen.
+                {pub, pri, DefaultConnectivityResolver.NetworkMode.PREFER_PUBLIC, "2.1.1.2", ImmutableSet.of("2.1.1.3")},
+                // private desired and reachable. public addresses ignored.
+                {pub, pri, DefaultConnectivityResolver.NetworkMode.PREFER_PRIVATE, "2.1.1.2", ImmutableSet.of("1.1.1.1", "1.1.1.2")},
+                // private desired but unreachable.
+                {pub, pri, DefaultConnectivityResolver.NetworkMode.PREFER_PRIVATE, "1.1.1.1", ImmutableSet.of()},
+        };
+    }
+
+    @Test(dataProvider = "publicPrivateProvider")
+    public void testPublicPrivatePreference(
+            List<String> publicAddresses, List<String> privateAddresses,
+            DefaultConnectivityResolver.NetworkMode networkMode, String preferredIp, ImmutableSet<String> otherReachableIps) throws Exception {
+        LOG.info("Checking {} preferred when mode={}, other reachable IPs={}, public addresses={} and private={}",
+                new Object[]{preferredIp, networkMode, otherReachableIps, publicAddresses, privateAddresses});
+        this.reachableIp = preferredIp;
+        this.additionalReachableIps = otherReachableIps;
+        initNodeCreatorAndJcloudsLocation(newNodeCreator(publicAddresses, privateAddresses), ImmutableMap.of());
+
+        ConnectivityResolver customizer =
+                new DefaultConnectivityResolver(ImmutableMap.of(DefaultConnectivityResolver.NETWORK_MODE, networkMode));
+
+        JcloudsSshMachineLocation machine = newMachine(ImmutableMap.<ConfigKey<?>, Object>builder()
+                .put(JcloudsLocationConfig.WAIT_FOR_SSHABLE, Duration.ONE_SECOND.toString())
+                .put(JcloudsLocation.POLL_FOR_FIRST_REACHABLE_ADDRESS, Duration.ONE_SECOND.toString())
+                .put(JcloudsLocation.CONNECTIVITY_RESOLVER, customizer)
+                .build());
+
+        assertEquals(machine.getAddress().getHostAddress(), preferredIp);
+    }
+
     protected JcloudsSshMachineLocation newMachine() throws Exception {
         return newMachine(ImmutableMap.<ConfigKey<?>, Object>of());
     }
@@ -366,7 +413,7 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
         
         @Override public boolean apply(HostAndPort input) {
             calls.add(input);
-            return reachableIp != null && reachableIp.equals(input.getHostText());
+            return isReachable(input.getHostText());
         }
         
         public void assertCalled() {
@@ -376,17 +423,29 @@ public class JcloudsReachableAddressStubbedTest extends AbstractJcloudsStubbedUn
         public void assertNotCalled() {
             assertTrue(calls.isEmpty(), "unexpected calls to "+this+": "+calls);
         }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("reachableIp", reachableIp)
+                    .toString();
+        }
     }
     
     protected class CustomResponseGeneratorImpl implements CustomResponseGenerator {
         @Override public CustomResponse generate(ExecParams execParams) throws Exception {
             System.out.println("ssh call: "+execParams);
             Object host = execParams.constructorProps.get(SshTool.PROP_HOST.getName());
-            if (reachableIp != null && reachableIp.equals(host)) {
+            if (isReachable(host.toString())) {
                 return new CustomResponse(0, "", "");
             } else {
                 throw new IOException("Simulate VM not reachable for host '"+host+"'");
             }
         }
     }
+
+    protected boolean isReachable(String address) {
+        return (reachableIp != null && reachableIp.equals(address)) ||
+                (additionalReachableIps != null && additionalReachableIps.contains(address));
+    }
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/3fd64c89/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
index 98ca2cb..26c9ac4 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
@@ -495,10 +495,18 @@ public abstract class MachineLifecycleEffectorTasks {
             // simply because subnet is still being looked up)
             Maybe<String> lh = Machines.getSubnetHostname(machine);
             Maybe<String> la = Machines.getSubnetIp(machine);
-            if (lh.isPresent()) entity().sensors().set(Attributes.SUBNET_HOSTNAME, lh.get());
-            if (la.isPresent()) entity().sensors().set(Attributes.SUBNET_ADDRESS, la.get());
-            entity().sensors().set(Attributes.HOSTNAME, machine.getAddress().getHostName());
-            entity().sensors().set(Attributes.ADDRESS, machine.getAddress().getHostAddress());
+            if (lh.isPresent() && entity().sensors().get(Attributes.SUBNET_HOSTNAME) == null) {
+                entity().sensors().set(Attributes.SUBNET_HOSTNAME, lh.get());
+            }
+            if (la.isPresent() && entity().sensors().get(Attributes.SUBNET_ADDRESS) == null) {
+                entity().sensors().set(Attributes.SUBNET_ADDRESS, la.get());
+            }
+            if (entity().sensors().get(Attributes.HOSTNAME) == null) {
+                entity().sensors().set(Attributes.HOSTNAME, machine.getAddress().getHostName());
+            }
+            if (entity().sensors().get(Attributes.ADDRESS) == null) {
+                entity().sensors().set(Attributes.ADDRESS, machine.getAddress().getHostAddress());
+            }
             if (machine instanceof SshMachineLocation) {
                 SshMachineLocation sshMachine = (SshMachineLocation) machine;
                 UserAndHostAndPort sshAddress = UserAndHostAndPort.fromParts(

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/3fd64c89/software/base/src/test/java/org/apache/brooklyn/entity/AbstractEc2LiveTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/AbstractEc2LiveTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/AbstractEc2LiveTest.java
index dd947db..e245395 100644
--- a/software/base/src/test/java/org/apache/brooklyn/entity/AbstractEc2LiveTest.java
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/AbstractEc2LiveTest.java
@@ -18,31 +18,14 @@
  */
 package org.apache.brooklyn.entity;
 
-import static org.testng.Assert.fail;
-
-import java.io.ByteArrayOutputStream;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.core.internal.BrooklynProperties;
-import org.apache.brooklyn.core.location.Machines;
-import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
-import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.entity.software.base.SoftwareProcess;
 import org.apache.brooklyn.location.jclouds.JcloudsLocation;
 import org.apache.brooklyn.location.jclouds.JcloudsLocationConfig;
-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.ssh.BashCommands;
-import org.apache.brooklyn.util.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
 /**