You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2015/08/12 17:55:24 UTC

[06/35] incubator-brooklyn git commit: [BROOKLYN-162] package rename to org.apache.brooklyn: software/webapp

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java
new file mode 100644
index 0000000..42649e8
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/AbstractControllerTest.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.net.Inet4Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityFactory;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.entity.group.Cluster;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.location.Location;
+import brooklyn.location.LocationSpec;
+import brooklyn.location.MachineLocation;
+import brooklyn.location.MachineProvisioningLocation;
+import brooklyn.location.NoMachinesAvailableException;
+import brooklyn.location.basic.FixedListMachineProvisioningLocation;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.test.Asserts;
+import brooklyn.test.entity.TestEntity;
+import brooklyn.test.entity.TestEntityImpl;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class AbstractControllerTest extends BrooklynAppUnitTestSupport {
+
+    private static final Logger log = LoggerFactory.getLogger(AbstractControllerTest.class);
+    
+    FixedListMachineProvisioningLocation<?> loc;
+    Cluster cluster;
+    TrackingAbstractController controller;
+    
+    @BeforeMethod(alwaysRun = true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        
+        List<SshMachineLocation> machines = new ArrayList<SshMachineLocation>();
+        for (int i=1; i<=10; i++) {
+            SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                    .configure("address", Inet4Address.getByName("1.1.1."+i)));
+            machines.add(machine);
+        }
+        loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)
+                .configure("machines", machines));
+        
+        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure("factory", new ClusteredEntity.Factory()));
+        
+        controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
+                .configure("serverPool", cluster) 
+                .configure("portNumberSensor", ClusteredEntity.HTTP_PORT)
+                .configure("domain", "mydomain"));
+        
+        app.start(ImmutableList.of(loc));
+    }
+    
+    // Fixes bug where entity that wrapped an AS7 entity was never added to nginx because hostname+port
+    // was set after service_up. Now we listen to those changes and reset the nginx pool when these
+    // values change.
+    @Test
+    public void testUpdateCalledWhenChildHostnameAndPortChanges() throws Exception {
+        TestEntity child = cluster.addChild(EntitySpec.create(TestEntity.class));
+        Entities.manage(child);
+        cluster.addMember(child);
+
+        List<Collection<String>> u = Lists.newArrayList(controller.getUpdates());
+        assertTrue(u.isEmpty(), "expected no updates, but got "+u);
+        
+        child.setAttribute(Startable.SERVICE_UP, true);
+        
+        // TODO Ugly sleep to allow AbstractController to detect node having been added
+        Thread.sleep(100);
+        
+        child.setAttribute(ClusteredEntity.HOSTNAME, "mymachine");
+        child.setAttribute(Attributes.SUBNET_HOSTNAME, "mymachine");
+        child.setAttribute(ClusteredEntity.HTTP_PORT, 1234);
+        assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine:1234"));
+        
+        child.setAttribute(ClusteredEntity.HOSTNAME, "mymachine2");
+        child.setAttribute(Attributes.SUBNET_HOSTNAME, "mymachine2");
+        assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine2:1234"));
+        
+        child.setAttribute(ClusteredEntity.HTTP_PORT, 1235);
+        assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine2:1235"));
+        
+        child.setAttribute(ClusteredEntity.HOSTNAME, null);
+        child.setAttribute(Attributes.SUBNET_HOSTNAME, null);
+        assertEventuallyExplicitAddressesMatch(ImmutableList.<String>of());
+    }
+
+    @Test
+    public void testUpdateCalledWithAddressesOfNewChildren() {
+        // First child
+        cluster.resize(1);
+        EntityLocal child = (EntityLocal) Iterables.getOnlyElement(cluster.getMembers());
+        
+        List<Collection<String>> u = Lists.newArrayList(controller.getUpdates());
+        assertTrue(u.isEmpty(), "expected empty list but got "+u);
+        
+        child.setAttribute(ClusteredEntity.HTTP_PORT, 1234);
+        child.setAttribute(Startable.SERVICE_UP, true);
+        assertEventuallyAddressesMatchCluster();
+
+        // Second child
+        cluster.resize(2);
+        Asserts.succeedsEventually(new Runnable() {
+            @Override
+            public void run() {
+                assertEquals(cluster.getMembers().size(), 2);
+            }});
+        EntityLocal child2 = (EntityLocal) Iterables.getOnlyElement(MutableSet.builder().addAll(cluster.getMembers()).remove(child).build());
+        
+        child2.setAttribute(ClusteredEntity.HTTP_PORT, 1234);
+        child2.setAttribute(Startable.SERVICE_UP, true);
+        assertEventuallyAddressesMatchCluster();
+        
+        // And remove all children; expect all addresses to go away
+        cluster.resize(0);
+        assertEventuallyAddressesMatchCluster();
+    }
+
+    @Test(groups = "Integration", invocationCount=10)
+    public void testUpdateCalledWithAddressesOfNewChildrenManyTimes() {
+        testUpdateCalledWithAddressesOfNewChildren();
+    }
+    
+    @Test
+    public void testUpdateCalledWithAddressesRemovedForStoppedChildren() {
+        // Get some children, so we can remove one...
+        cluster.resize(2);
+        for (Entity it: cluster.getMembers()) { 
+            ((EntityLocal)it).setAttribute(ClusteredEntity.HTTP_PORT, 1234);
+            ((EntityLocal)it).setAttribute(Startable.SERVICE_UP, true);
+        }
+        assertEventuallyAddressesMatchCluster();
+
+        // Now remove one child
+        cluster.resize(1);
+        assertEquals(cluster.getMembers().size(), 1);
+        assertEventuallyAddressesMatchCluster();
+    }
+
+    @Test
+    public void testUpdateCalledWithAddressesRemovedForServiceDownChildrenThatHaveClearedHostnamePort() {
+        // Get some children, so we can remove one...
+        cluster.resize(2);
+        for (Entity it: cluster.getMembers()) { 
+            ((EntityLocal)it).setAttribute(ClusteredEntity.HTTP_PORT, 1234);
+            ((EntityLocal)it).setAttribute(Startable.SERVICE_UP, true);
+        }
+        assertEventuallyAddressesMatchCluster();
+
+        // Now unset host/port, and remove children
+        // Note the unsetting of hostname is done in SoftwareProcessImpl.stop(), so this is realistic
+        for (Entity it : cluster.getMembers()) {
+            ((EntityLocal)it).setAttribute(ClusteredEntity.HTTP_PORT, null);
+            ((EntityLocal)it).setAttribute(ClusteredEntity.HOSTNAME, null);
+            ((EntityLocal)it).setAttribute(Startable.SERVICE_UP, false);
+        }
+        assertEventuallyAddressesMatch(ImmutableList.<Entity>of());
+    }
+
+    @Test
+    public void testUsesHostAndPortSensor() throws Exception {
+        controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
+                .configure("serverPool", cluster) 
+                .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT)
+                .configure("domain", "mydomain"));
+        controller.start(Arrays.asList(loc));
+        
+        TestEntity child = cluster.addChild(EntitySpec.create(TestEntity.class));
+        Entities.manage(child);
+        cluster.addMember(child);
+
+        List<Collection<String>> u = Lists.newArrayList(controller.getUpdates());
+        assertTrue(u.isEmpty(), "expected no updates, but got "+u);
+        
+        child.setAttribute(Startable.SERVICE_UP, true);
+        
+        // TODO Ugly sleep to allow AbstractController to detect node having been added
+        Thread.sleep(100);
+        
+        child.setAttribute(ClusteredEntity.HOST_AND_PORT, "mymachine:1234");
+        assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine:1234"));
+    }
+
+    @Test
+    public void testFailsIfSetHostAndPortAndHostnameOrPortNumberSensor() throws Exception {
+        try {
+            TrackingAbstractController controller2 = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
+                    .configure("serverPool", cluster) 
+                    .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT)
+                    .configure("hostnameSensor", ClusteredEntity.HOSTNAME)
+                    .configure("domain", "mydomain"));
+            controller2.start(Arrays.asList(loc));
+        } catch (Exception e) {
+            IllegalStateException unwrapped = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
+            if (unwrapped != null && unwrapped.toString().contains("Must not set Sensor")) {
+                // success
+            } else {
+                throw e;
+            }
+        }
+
+        try {
+            TrackingAbstractController controller3 = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
+                    .configure("serverPool", cluster) 
+                    .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT)
+                    .configure("portNumberSensor", ClusteredEntity.HTTP_PORT)
+                    .configure("domain", "mydomain"));
+            controller3.start(Arrays.asList(loc));
+        } catch (Exception e) {
+            IllegalStateException unwrapped = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
+            if (unwrapped != null && unwrapped.toString().contains("Must not set Sensor")) {
+                // success
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    // Manual visual inspection test. Previously it repeatedly logged:
+    //     Unable to construct hostname:port representation for TestEntityImpl{id=jzwSBRQ2} (null:null); skipping in TrackingAbstractControllerImpl{id=tOn4k5BA}
+    // every time the service-up was set to true again.
+    @Test
+    public void testMemberWithoutHostAndPortDoesNotLogErrorRepeatedly() throws Exception {
+        controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class)
+                .configure("serverPool", cluster) 
+                .configure("domain", "mydomain"));
+        controller.start(ImmutableList.of(loc));
+        
+        TestEntity child = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        cluster.addMember(child);
+
+        for (int i = 0; i < 100; i++) {
+            child.setAttribute(Attributes.SERVICE_UP, true);
+        }
+        
+        Thread.sleep(100);
+        List<Collection<String>> u = Lists.newArrayList(controller.getUpdates());
+        assertTrue(u.isEmpty(), "expected no updates, but got "+u);
+    }
+
+    private void assertEventuallyAddressesMatchCluster() {
+        assertEventuallyAddressesMatch(cluster.getMembers());
+    }
+
+    private void assertEventuallyAddressesMatch(final Collection<Entity> expectedMembers) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", 15000), new Runnable() {
+                @Override public void run() {
+                    assertAddressesMatch(locationsToAddresses(1234, expectedMembers));
+                }} );
+    }
+
+    private void assertEventuallyExplicitAddressesMatch(final Collection<String> expectedAddresses) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", 15000), new Runnable() {
+            @Override public void run() {
+                assertAddressesMatch(expectedAddresses);
+            }} );
+    }
+
+    private void assertAddressesMatch(final Collection<String> expectedAddresses) {
+        List<Collection<String>> u = Lists.newArrayList(controller.getUpdates());
+        Collection<String> last = Iterables.getLast(u, null);
+        log.debug("test "+u.size()+" updates, expecting "+expectedAddresses+"; actual "+last);
+        assertTrue(u.size() > 0);
+        assertEquals(ImmutableSet.copyOf(last), ImmutableSet.copyOf(expectedAddresses), "actual="+last+" expected="+expectedAddresses);
+        assertEquals(last.size(), expectedAddresses.size(), "actual="+last+" expected="+expectedAddresses);
+    }
+
+    private Collection<String> locationsToAddresses(int port, Collection<Entity> entities) {
+        Set<String> result = MutableSet.of();
+        for (Entity e: entities) {
+            result.add( ((SshMachineLocation) e.getLocations().iterator().next()) .getAddress().getHostName()+":"+port);
+        }
+        return result;
+    }
+
+    public static class ClusteredEntity extends TestEntityImpl {
+        public static class Factory implements EntityFactory<ClusteredEntity> {
+            @Override
+            public ClusteredEntity newEntity(Map flags, Entity parent) {
+                return new ClusteredEntity(flags, parent);
+            }
+        }
+        public ClusteredEntity(Map flags, Entity parent) { super(flags,parent); }
+        public ClusteredEntity(Entity parent) { super(MutableMap.of(),parent); }
+        public ClusteredEntity(Map flags) { super(flags,null); }
+        public ClusteredEntity() { super(MutableMap.of(),null); }
+        
+        @SetFromFlag("hostname")
+        public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
+        
+        @SetFromFlag("port")
+        public static final AttributeSensor<Integer> HTTP_PORT = Attributes.HTTP_PORT;
+        
+        @SetFromFlag("hostAndPort")
+        public static final AttributeSensor<String> HOST_AND_PORT = Attributes.HOST_AND_PORT;
+        
+        MachineProvisioningLocation provisioner;
+        
+        public void start(Collection<? extends Location> locs) {
+            provisioner = (MachineProvisioningLocation) locs.iterator().next();
+            MachineLocation machine;
+            try {
+                machine = provisioner.obtain(MutableMap.of());
+            } catch (NoMachinesAvailableException e) {
+                throw Exceptions.propagate(e);
+            }
+            addLocations(Arrays.asList(machine));
+            setAttribute(HOSTNAME, machine.getAddress().getHostName());
+            setAttribute(Attributes.SUBNET_HOSTNAME, machine.getAddress().getHostName());
+        }
+        public void stop() {
+            if (provisioner!=null) provisioner.release((MachineLocation) firstLocation());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/ProxySslConfigTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/ProxySslConfigTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/ProxySslConfigTest.java
new file mode 100644
index 0000000..218debf
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/ProxySslConfigTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy;
+
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.flags.TypeCoercions;
+
+@Test
+public class ProxySslConfigTest {
+
+    @Test
+    public void testFromMap() {
+        ProxySslConfig config = TypeCoercions.coerce(MutableMap.of(
+            "certificateSourceUrl", "file://tmp/cert.txt", 
+            "keySourceUrl", "file://tmp/key.txt", 
+            "keyDestination", "dest.txt", 
+            "targetIsSsl", true, 
+            "reuseSessions", true), 
+            ProxySslConfig.class);
+        Assert.assertEquals(config.getCertificateSourceUrl(), "file://tmp/cert.txt");
+        Assert.assertEquals(config.getKeySourceUrl(), "file://tmp/key.txt");
+        Assert.assertEquals(config.getKeyDestination(), "dest.txt");
+        Assert.assertEquals(config.getTargetIsSsl(), true);
+        Assert.assertEquals(config.getReuseSessions(), true);
+    }
+    
+    @Test
+    public void testFromMapWithNullsAndDefaults() {
+        ProxySslConfig config = TypeCoercions.coerce(MutableMap.of(
+            "certificateSourceUrl", "file://tmp/cert.txt", 
+            "keySourceUrl", null, 
+            "targetIsSsl", "false"), 
+            ProxySslConfig.class);
+        Assert.assertEquals(config.getCertificateSourceUrl(), "file://tmp/cert.txt");
+        Assert.assertEquals(config.getKeySourceUrl(), null);
+        Assert.assertEquals(config.getKeyDestination(), null);
+        Assert.assertEquals(config.getTargetIsSsl(), false);
+        Assert.assertEquals(config.getReuseSessions(), false);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java
new file mode 100644
index 0000000..92cdd84
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/StubAppServer.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
+import brooklyn.location.Location;
+import brooklyn.location.MachineLocation;
+import brooklyn.location.MachineProvisioningLocation;
+import brooklyn.location.NoMachinesAvailableException;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class StubAppServer extends AbstractEntity implements Startable {
+    public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
+    public static final PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT;
+    public static AtomicInteger nextPort = new AtomicInteger(1234);
+
+    public StubAppServer(Map flags) {
+        super(flags);
+    }
+    
+    public StubAppServer(Map flags, Entity parent) {
+        super(flags, parent);
+    }
+    
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        Location location = Iterables.getOnlyElement(locations);
+        if (location instanceof MachineProvisioningLocation) {
+            startInLocation((MachineProvisioningLocation)location);
+        } else {
+            startInLocation((MachineLocation)location);
+        }
+    }
+
+    private void startInLocation(MachineProvisioningLocation loc) {
+        try {
+            startInLocation(loc.obtain(MutableMap.of()));
+        } catch (NoMachinesAvailableException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+    
+    private void startInLocation(MachineLocation loc) {
+        addLocations(ImmutableList.of((Location)loc));
+        setAttribute(HOSTNAME, loc.getAddress().getHostName());
+        setAttribute(HTTP_PORT, nextPort.getAndIncrement());
+        setAttribute(SERVICE_UP, true);
+    }
+
+    public void stop() {
+        setAttribute(SERVICE_UP, false);
+    }
+    
+    @Override
+    public void restart() {
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractController.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractController.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractController.java
new file mode 100644
index 0000000..f5e7ff5
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractController.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.brooklyn.entity.proxy.AbstractController;
+
+import brooklyn.entity.proxying.ImplementedBy;
+
+@ImplementedBy(TrackingAbstractControllerImpl.class)
+public interface TrackingAbstractController extends AbstractController {
+    List<Collection<String>> getUpdates();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractControllerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractControllerImpl.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractControllerImpl.java
new file mode 100644
index 0000000..90fea43
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/TrackingAbstractControllerImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.proxy.AbstractControllerImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.driver.MockSshDriver;
+
+import com.google.common.collect.Lists;
+
+public class TrackingAbstractControllerImpl extends AbstractControllerImpl implements TrackingAbstractController {
+    
+    private static final Logger log = LoggerFactory.getLogger(TrackingAbstractControllerImpl.class);
+
+    private final List<Collection<String>> updates = Lists.newCopyOnWriteArrayList();
+    
+    @Override
+    public List<Collection<String>> getUpdates() {
+        return updates;
+    }
+    
+    @Override
+    public void connectSensors() {
+        super.connectSensors();
+        setAttribute(SERVICE_UP, true);
+    }
+    
+    @Override
+    protected void reconfigureService() {
+        Set<String> addresses = getServerPoolAddresses();
+        log.info("test controller reconfigure, targets "+addresses);
+        if ((!addresses.isEmpty() && updates.isEmpty()) || (!updates.isEmpty() && addresses != updates.get(updates.size()-1))) {
+            updates.add(addresses);
+        }
+    }
+
+    @Override
+    public Class getDriverInterface() {
+        return MockSshDriver.class;
+    }
+    
+    @Override
+    public void reload() {
+        // no-op
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java
new file mode 100644
index 0000000..ba51549
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/UrlMappingTest.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.File;
+import java.util.HashSet;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.entity.proxy.nginx.UrlMapping;
+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 brooklyn.entity.Entity;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.BasicConfigurableEntityFactory;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityFactory;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.rebind.RebindTestUtils;
+import brooklyn.location.LocationSpec;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.management.internal.LocalManagementContext;
+import brooklyn.test.Asserts;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+
+public class UrlMappingTest {
+    
+    private static final Logger log = LoggerFactory.getLogger(UrlMappingTest.class);
+    
+    private final int initialClusterSize = 2;
+    
+    private ClassLoader classLoader = getClass().getClassLoader();
+    private LocalManagementContext managementContext;
+    private File mementoDir;
+    
+    private TestApplication app;
+    private DynamicCluster cluster;
+    private UrlMapping urlMapping;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setup() {
+        mementoDir = Files.createTempDir();
+        managementContext = RebindTestUtils.newPersistingManagementContext(mementoDir, classLoader);
+
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        
+        EntityFactory<StubAppServer> serverFactory = new BasicConfigurableEntityFactory<StubAppServer>(StubAppServer.class);
+        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", initialClusterSize)
+                .configure("factory", serverFactory));
+
+        urlMapping = app.createAndManageChild(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost")
+                .configure("target", cluster));
+
+        app.start( ImmutableList.of(
+                managementContext.getLocationManager().createLocation(
+                        LocationSpec.create(LocalhostMachineProvisioningLocation.class))
+                ));
+        log.info("app's location managed: "+managementContext.getLocationManager().isManaged(Iterables.getOnlyElement(app.getLocations())));
+        log.info("clusters's location managed: "+managementContext.getLocationManager().isManaged(Iterables.getOnlyElement(cluster.getLocations())));
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void shutdown() {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+        if (mementoDir != null) RebindTestUtils.deleteMementoDir(mementoDir);
+    }
+
+    @Test(groups = "Integration")
+    public void testTargetMappingsMatchesClusterMembers() {
+        // Check updates its TARGET_ADDRESSES (through async subscription)
+        assertExpectedTargetsEventually(cluster.getMembers());
+    }
+    
+    @Test(groups = "Integration")
+    public void testTargetMappingsRemovesUnmanagedMember() {
+        Iterable<StubAppServer> members = Iterables.filter(cluster.getChildren(), StubAppServer.class);
+        assertEquals(Iterables.size(members), 2);
+        StubAppServer target1 = Iterables.get(members, 0);
+        StubAppServer target2 = Iterables.get(members, 1);
+        
+        // First wait for targets to be listed
+        assertExpectedTargetsEventually(members);
+        
+        // Unmanage one member, and expect the URL Mapping to be updated accordingly
+        Entities.unmanage(target1);
+
+        assertExpectedTargetsEventually(ImmutableSet.of(target2));
+    }
+    
+    @Test(groups = "Integration", invocationCount=50)
+    public void testTargetMappingsRemovesUnmanagedMemberManyTimes() {
+        testTargetMappingsRemovesUnmanagedMember();
+    }
+    
+    @Test(groups = "Integration")
+    public void testTargetMappingsRemovesDownMember() {
+        Iterable<StubAppServer> members = Iterables.filter(cluster.getChildren(), StubAppServer.class);
+        StubAppServer target1 = Iterables.get(members, 0);
+        StubAppServer target2 = Iterables.get(members, 1);
+        
+        // First wait for targets to be listed
+        assertExpectedTargetsEventually(members);
+        
+        // Stop one member, and expect the URL Mapping to be updated accordingly
+        target1.setAttribute(StubAppServer.SERVICE_UP, false);
+
+        assertExpectedTargetsEventually(ImmutableSet.of(target2));
+    }
+
+    // i think no real reason for other methods to be Integration apart from the time they take;
+    // having one in the unit tests is very handy however, and this is a good choice because it does quite a lot!
+    @Test
+    public void testTargetMappingUpdatesAfterRebind() throws Exception {
+        log.info("starting testTargetMappingUpdatesAfterRebind");
+        Iterable<StubAppServer> members = Iterables.filter(cluster.getChildren(), StubAppServer.class);
+        assertExpectedTargetsEventually(members);
+        
+        Assert.assertTrue(managementContext.getLocationManager().isManaged(Iterables.getOnlyElement(cluster.getLocations())));
+        rebind();
+        Assert.assertTrue(managementContext.getLocationManager().isManaged(Iterables.getOnlyElement(cluster.getLocations())),
+                "location not managed after rebind");
+        
+        Iterable<StubAppServer> members2 = Iterables.filter(cluster.getChildren(), StubAppServer.class);
+        StubAppServer target1 = Iterables.get(members2, 0);
+        StubAppServer target2 = Iterables.get(members2, 1);
+
+        // Expect to have existing targets
+        assertExpectedTargetsEventually(ImmutableSet.of(target1, target2));
+
+        // Add a new member; expect member to be added
+        log.info("resizing "+cluster+" - "+cluster.getChildren());
+        Integer result = cluster.resize(3);
+        Assert.assertTrue(managementContext.getLocationManager().isManaged(Iterables.getOnlyElement(cluster.getLocations())));
+        log.info("resized "+cluster+" ("+result+") - "+cluster.getChildren());
+        HashSet<StubAppServer> newEntities = Sets.newHashSet(Iterables.filter(cluster.getChildren(), StubAppServer.class));
+        newEntities.remove(target1);
+        newEntities.remove(target2);
+        StubAppServer target3 = Iterables.getOnlyElement(newEntities);
+        log.info("expecting "+ImmutableSet.of(target1, target2, target3));
+        assertExpectedTargetsEventually(ImmutableSet.of(target1, target2, target3));
+        
+        // Stop one member, and expect the URL Mapping to be updated accordingly
+        log.info("pretending one node down");
+        target1.setAttribute(StubAppServer.SERVICE_UP, false);
+        assertExpectedTargetsEventually(ImmutableSet.of(target2, target3));
+
+        // Unmanage a member, and expect the URL Mapping to be updated accordingly
+        log.info("unmanaging another node");
+        Entities.unmanage(target2);
+        assertExpectedTargetsEventually(ImmutableSet.of(target3));
+        log.info("success - testTargetMappingUpdatesAfterRebind");
+    }
+    
+    private void assertExpectedTargetsEventually(final Iterable<? extends Entity> members) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", Duration.ONE_MINUTE), new Runnable() {
+            public void run() {
+                Iterable<String> expectedTargets = Iterables.transform(members, new Function<Entity,String>() {
+                        @Override public String apply(@Nullable Entity input) {
+                            return input.getAttribute(Attributes.HOSTNAME)+":"+input.getAttribute(Attributes.HTTP_PORT);
+                        }});
+
+                assertEquals(ImmutableSet.copyOf(urlMapping.getAttribute(UrlMapping.TARGET_ADDRESSES)), ImmutableSet.copyOf(expectedTargets));
+                assertEquals(urlMapping.getAttribute(UrlMapping.TARGET_ADDRESSES).size(), Iterables.size(members));
+            }});
+    }
+    
+    private void rebind() throws Exception {
+        RebindTestUtils.waitForPersisted(app);
+        
+        // Stop the old management context, so original nginx won't interfere
+        managementContext.terminate();
+        
+        app = (TestApplication) RebindTestUtils.rebind(mementoDir, getClass().getClassLoader());
+        managementContext = (LocalManagementContext) ((EntityInternal)app).getManagementContext();
+        cluster = (DynamicCluster) Iterables.find(app.getChildren(), Predicates.instanceOf(DynamicCluster.class));
+        urlMapping = (UrlMapping) Iterables.find(app.getChildren(), Predicates.instanceOf(UrlMapping.class));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxClusterIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxClusterIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxClusterIntegrationTest.java
new file mode 100644
index 0000000..6eeb5c8
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxClusterIntegrationTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.brooklyn.entity.proxy.LoadBalancerCluster;
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.proxy.nginx.UrlMapping;
+import org.apache.brooklyn.entity.webapp.JavaWebAppService;
+import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
+import org.apache.brooklyn.management.EntityManager;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppLiveTestSupport;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.BasicGroup;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.trait.Startable;
+import brooklyn.location.Location;
+import brooklyn.location.basic.PortRanges;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+/**
+ * Test the operation of the {@link NginxController} class.
+ */
+public class NginxClusterIntegrationTest extends BrooklynAppLiveTestSupport {
+    @SuppressWarnings("unused")
+    private static final Logger log = LoggerFactory.getLogger(NginxClusterIntegrationTest.class);
+
+    private static final long TIMEOUT_MS = 60*1000;
+    
+    private Location localhostProvisioningLoc;
+    private EntityManager entityManager;
+    private LoadBalancerCluster loadBalancerCluster;
+    private EntitySpec<NginxController> nginxSpec;
+    private Group urlMappings;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        localhostProvisioningLoc = app.newLocalhostProvisioningLocation();
+        
+        urlMappings = app.createAndManageChild(EntitySpec.create(BasicGroup.class)
+                .configure("childrenAsMembers", true));
+        entityManager = app.getManagementContext().getEntityManager();
+        
+        nginxSpec = EntitySpec.create(NginxController.class);
+    }
+
+    public String getTestWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
+        return "classpath://hello-world.war";
+    }
+
+    @Test(groups = "Integration")
+    public void testCreatesNginxInstancesAndResizes() {
+        loadBalancerCluster = app.createAndManageChild(EntitySpec.create(LoadBalancerCluster.class)
+                .configure(LoadBalancerCluster.MEMBER_SPEC, nginxSpec)
+                .configure("initialSize", 1)
+                .configure(NginxController.DOMAIN_NAME, "localhost"));
+        
+        app.start(ImmutableList.of(localhostProvisioningLoc));
+        
+        assertEquals(findNginxs().size(), 1);
+        assertNginxsResponsiveEvenutally(findNginxs());
+        
+        // Resize load-balancer cluster
+        loadBalancerCluster.resize(2);
+        assertEquals(findNginxs().size(), 2);
+        assertNoDuplicates(findNginxRootUrls());
+        assertNginxsResponsiveEvenutally(findNginxs());
+    }
+    
+    @Test(groups = "Integration")
+    public void testNginxInstancesConfiguredWithServerPool() {
+        DynamicCluster serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        loadBalancerCluster = app.createAndManageChild(EntitySpec.create(LoadBalancerCluster.class)
+                .configure("serverPool", serverPool)
+                .configure(LoadBalancerCluster.MEMBER_SPEC, nginxSpec)
+                .configure("initialSize", 1)
+                .configure(NginxController.DOMAIN_NAME, "localhost"));
+        
+        app.start(ImmutableList.of(localhostProvisioningLoc));
+        
+        assertEquals(findNginxs().size(), 1);
+        
+        String hostname = "localhost";
+        List<String> pathsFor200 = ImmutableList.of(""); // i.e. app deployed at root
+        assertNginxsResponsiveEvenutally(findNginxs(), hostname, pathsFor200);
+    }
+
+    @Test(groups = "Integration")
+    public void testNginxInstancesConfiguredWithUrlMappings() {
+        DynamicCluster c1 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.NAMED_WARS, ImmutableList.of(getTestWar())));
+
+        UrlMapping urlMapping = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost")
+                .configure("path", "/hello-world($|/.*)")
+                .configure("target", c1)
+                .parent(urlMappings));
+        Entities.manage(urlMapping);
+        
+        loadBalancerCluster = app.createAndManageChild(EntitySpec.create(LoadBalancerCluster.class)
+                .configure("urlMappings", urlMappings)
+                .configure(LoadBalancerCluster.MEMBER_SPEC, nginxSpec)
+                .configure("initialSize", 1));
+
+        app.start(ImmutableList.of(localhostProvisioningLoc));
+        
+        assertEquals(findNginxs().size(), 1);
+        
+        String hostname = "localhost";
+        List<String> pathsFor200 = ImmutableList.of("hello-world", "hello-world/");
+        assertNginxsResponsiveEvenutally(findNginxs(), hostname, pathsFor200);
+    }
+
+    @Test(groups = "Integration")
+    public void testClusterIsUpIffHasChildLoadBalancer() {
+        // Note the up-quorum-check behaves different for initialSize==0 (if explicit value not given):
+        // it would accept a size==0 as being serviceUp=true. Therefore don't do that!
+        loadBalancerCluster = app.createAndManageChild(EntitySpec.create(LoadBalancerCluster.class)
+                .configure(LoadBalancerCluster.MEMBER_SPEC, nginxSpec)
+                .configure("initialSize", 1)
+                .configure(NginxController.DOMAIN_NAME, "localhost"));
+        
+        app.start(ImmutableList.of(localhostProvisioningLoc));
+        EntityTestUtils.assertAttributeEqualsContinually(loadBalancerCluster, Startable.SERVICE_UP, true);
+        
+        loadBalancerCluster.resize(0);
+        EntityTestUtils.assertAttributeEqualsEventually(loadBalancerCluster, Startable.SERVICE_UP, false);
+        
+        loadBalancerCluster.resize(1);
+        EntityTestUtils.assertAttributeEqualsEventually(loadBalancerCluster, Startable.SERVICE_UP, true);
+    }
+    
+    // Warning: test is a little brittle for if a previous run leaves something on these required ports
+    @Test(groups = "Integration")
+    public void testConfiguresNginxInstancesWithInheritedPortConfig() {
+        loadBalancerCluster = app.createAndManageChild(EntitySpec.create(LoadBalancerCluster.class)
+                .configure(LoadBalancerCluster.MEMBER_SPEC, nginxSpec)
+                .configure("initialSize", 1)
+                .configure(NginxController.DOMAIN_NAME, "localhost")
+                .configure(NginxController.PROXY_HTTP_PORT, PortRanges.fromString("8765+")));
+        
+        app.start(ImmutableList.of(localhostProvisioningLoc));
+        
+        NginxController nginx1 = Iterables.getOnlyElement(findNginxs());
+
+        loadBalancerCluster.resize(2);
+        NginxController nginx2 = Iterables.getOnlyElement(Iterables.filter(findNginxs(), 
+                Predicates.not(Predicates.in(ImmutableList.of(nginx1)))));
+
+        assertEquals((int) nginx1.getAttribute(NginxController.PROXY_HTTP_PORT), 8765);
+        assertEquals((int) nginx2.getAttribute(NginxController.PROXY_HTTP_PORT), 8766);
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    private List<NginxController> findNginxs() {
+        ImmutableList result = ImmutableList.copyOf(Iterables.filter(app.getManagementContext().getEntityManager().getEntities(), Predicates.instanceOf(NginxController.class)));
+        return (List<NginxController>) result;
+    }
+
+    private List<String> findNginxRootUrls() {
+        List<String> result = Lists.newArrayList();
+        for (NginxController nginx : findNginxs()) {
+            result.add(nginx.getAttribute(NginxController.ROOT_URL));
+        }
+        return result;
+    }
+
+    private void assertNginxsResponsiveEvenutally(final Iterable<NginxController> nginxs) {
+        assertNginxsResponsiveEvenutally(nginxs, null, Collections.<String>emptyList());
+    }
+
+    private void assertNginxsResponsiveEvenutally(final Iterable<NginxController> nginxs, final String hostname, final List<String> pathsFor200) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                for (NginxController nginx : nginxs) {
+                    assertTrue(nginx.getAttribute(NginxController.SERVICE_UP));
+                    
+                    String normalRootUrl = nginx.getAttribute(NginxController.ROOT_URL);
+                    int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+                    String rootUrl = (hostname != null) ? ("http://"+hostname+":"+port+"/") : normalRootUrl;
+                    
+                    String wrongUrl = rootUrl+"doesnotexist";
+                    HttpTestUtils.assertHttpStatusCodeEquals(wrongUrl, 404);
+                    
+                    for (String pathFor200 : pathsFor200) {
+                        String url = rootUrl+pathFor200;
+                        HttpTestUtils.assertHttpStatusCodeEquals(url, 200);
+                    }
+                }
+            }});
+    }
+
+    private void assertNoDuplicates(Iterable<String> c) {
+        assertEquals(Iterables.size(c), ImmutableSet.copyOf(c).size(), "c="+c);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxEc2LiveTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxEc2LiveTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxEc2LiveTest.java
new file mode 100644
index 0000000..0902444
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxEc2LiveTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.AbstractEc2LiveTest;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.location.Location;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * A simple test of installing+running on AWS-EC2, using various OS distros and versions. 
+ */
+public class NginxEc2LiveTest extends AbstractEc2LiveTest {
+    
+    /* FIXME Currently fails on:
+     *   test_Debian_5:                   installation of nginx failed
+     *   test_Ubuntu_12_0:                invocation error for disable requiretty 
+     */
+    
+    /* PASSED: test_CentOS_5
+     * PASSED: test_CentOS_6_3
+     * PASSED: test_Debian_6
+     * PASSED: test_Ubuntu_10_0
+     * 
+     * test_Red_Hat_Enterprise_Linux_6 passes, if get it to wait for ssh-login rather than "failed to SSH in as root"
+     */
+    
+    @SuppressWarnings("unused")
+    private static final Logger log = LoggerFactory.getLogger(NginxEc2LiveTest.class);
+
+    private NginxController nginx;
+
+    @Override
+    protected void doTest(Location loc) throws Exception {
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("portNumberSensor", WebAppService.HTTP_PORT));
+        
+        app.start(ImmutableList.of(loc));
+
+        // nginx should be up, and URL reachable
+        EntityTestUtils.assertAttributeEqualsEventually(nginx, SoftwareProcess.SERVICE_UP, true);
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals(nginx.getAttribute(NginxController.ROOT_URL), 404);
+    }
+    
+    @Test(enabled=false)
+    public void testDummy() {} // Convince testng IDE integration that this really does have test methods  
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxHttpsSslIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxHttpsSslIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxHttpsSslIntegrationTest.java
new file mode 100644
index 0000000..5e53054
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxHttpsSslIntegrationTest.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.brooklyn.entity.proxy.LoadBalancer;
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.webapp.JavaWebAppService;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppLiveTestSupport;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
+import brooklyn.location.Location;
+import brooklyn.location.basic.PortRanges;
+import brooklyn.test.Asserts;
+import brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+/**
+ * Test the operation of the {@link NginxController} class.
+ */
+public class NginxHttpsSslIntegrationTest extends BrooklynAppLiveTestSupport {
+    
+    private static final Logger log = LoggerFactory.getLogger(NginxHttpsSslIntegrationTest.class);
+
+    private NginxController nginx;
+    private DynamicCluster cluster;
+    private Location localLoc;
+
+    private static final String CERTIFICATE_URL = "classpath://ssl/certs/localhost/server.crt";
+    private static final String KEY_URL = "classpath://ssl/certs/localhost/server.key";
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        localLoc = mgmt.getLocationRegistry().resolve("localhost");
+    }
+    
+    private static void urlContainsPort(NginxController nginx, PortAttributeSensorAndConfigKey sensor, String portRange) {
+        Integer port = nginx.getAttribute(sensor);
+        Assert.assertTrue(Iterables.contains(PortRanges.fromString(portRange), port), "Port "+port+" not in range "+portRange);
+        String url = Preconditions.checkNotNull(nginx.getAttribute(LoadBalancer.MAIN_URI), "main uri").toString();
+        Assert.assertTrue(url.contains(":"+port), "URL does not contain expected port; port "+port+", url "+url);
+    }
+
+    public String getTestWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
+        return "classpath://hello-world.war";
+    }
+
+    /**
+     * Test that the Nginx proxy starts up and sets SERVICE_UP correctly.
+     */
+    @Test(groups = "Integration")
+    public void testStartsWithGlobalSsl_withCertificateAndKeyCopy() {
+        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+            .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+            .configure("initialSize", 1)
+            .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        ProxySslConfig ssl = ProxySslConfig.builder()
+                .certificateSourceUrl(CERTIFICATE_URL)
+                .keySourceUrl(KEY_URL)
+                .build();
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("sticky", false)
+                .configure("serverPool", cluster)
+                .configure("domain", "localhost")
+                .configure("httpsPort", "8453+")
+                .configure("ssl", ssl));
+        
+        app.start(ImmutableList.of(localLoc));
+
+        urlContainsPort(nginx, LoadBalancer.PROXY_HTTPS_PORT, "8453+");
+
+        final String url = nginx.getAttribute(WebAppService.ROOT_URL);
+        log.info("URL for nginx is "+url);
+        if (!url.startsWith("https://")) Assert.fail("URL should be https: "+url);
+        
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                // Services are running
+                assertTrue(cluster.getAttribute(SoftwareProcess.SERVICE_UP));
+                for (Entity member : cluster.getMembers()) {
+                    assertTrue(member.getAttribute(SoftwareProcess.SERVICE_UP));
+                }
+                
+                assertTrue(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+    
+                // Nginx URL is available
+                HttpTestUtils.assertHttpStatusCodeEquals(url, 200);
+    
+                // Web-server URL is available
+                for (Entity member : cluster.getMembers()) {
+                    HttpTestUtils.assertHttpStatusCodeEquals(member.getAttribute(WebAppService.ROOT_URL), 200);
+                }
+            }});
+        
+        app.stop();
+
+        // Services have stopped
+        assertFalse(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+        assertFalse(cluster.getAttribute(SoftwareProcess.SERVICE_UP));
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(member.getAttribute(SoftwareProcess.SERVICE_UP));
+        }
+    }
+
+    private String getFile(String file) {
+        return new File(getClass().getResource("/" + file).getFile()).getAbsolutePath();
+    }
+
+    @Test(groups = "Integration")
+    public void testStartsWithGlobalSsl_withPreinstalledCertificateAndKey() {
+        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+            .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+            .configure("initialSize", 1)
+            .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        ProxySslConfig ssl = ProxySslConfig.builder()
+                .certificateDestination(getFile("ssl/certs/localhost/server.crt"))
+                .keyDestination(getFile("ssl/certs/localhost/server.key"))
+                .build();
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("sticky", false)
+                .configure("serverPool", cluster)
+                .configure("domain", "localhost")
+                .configure("port", "8443+")
+                .configure("ssl", ssl));
+
+        app.start(ImmutableList.of(localLoc));
+
+        final String url = nginx.getAttribute(WebAppService.ROOT_URL);
+        if (!url.startsWith("https://")) Assert.fail("URL should be https: "+url);
+
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                // Services are running
+                assertTrue(cluster.getAttribute(SoftwareProcess.SERVICE_UP));
+                for (Entity member : cluster.getMembers()) {
+                    assertTrue(member.getAttribute(SoftwareProcess.SERVICE_UP));
+                }
+    
+                assertTrue(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+    
+                // Nginx URL is available
+                HttpTestUtils.assertHttpStatusCodeEquals(url, 200);
+    
+                // Web-server URL is available
+                for (Entity member : cluster.getMembers()) {
+                    HttpTestUtils.assertHttpStatusCodeEquals(member.getAttribute(WebAppService.ROOT_URL), 200);
+                }
+            }});
+
+        app.stop();
+
+        // Services have stopped
+        assertFalse(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+        assertFalse(cluster.getAttribute(SoftwareProcess.SERVICE_UP));
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(member.getAttribute(SoftwareProcess.SERVICE_UP));
+        }
+    }
+
+    @Test(groups = "Integration")
+    public void testStartsNonSslThenBecomesSsl() {
+        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+            .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+            .configure("initialSize", 1)
+            .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+            .configure("serverPool", cluster)
+            .configure("domain", "localhost"));
+
+        app.start(ImmutableList.of(localLoc));
+
+        urlContainsPort(nginx, LoadBalancer.PROXY_HTTP_PORT, "8000-8100");
+        
+        ProxySslConfig ssl = ProxySslConfig.builder()
+                .certificateDestination(getFile("ssl/certs/localhost/server.crt"))
+                .keyDestination(getFile("ssl/certs/localhost/server.key"))
+                .build();
+        ((EntityInternal)nginx).setConfig(LoadBalancer.PROXY_HTTPS_PORT, PortRanges.fromString("8443+"));
+        ((EntityInternal)nginx).setConfig(NginxController.SSL_CONFIG, ssl);
+
+        try {
+            log.info("restarting nginx as ssl");
+            nginx.restart();
+            urlContainsPort(nginx, LoadBalancer.PROXY_HTTPS_PORT, "8443-8543");
+
+            app.stop();
+            
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java
new file mode 100644
index 0000000..d208cfb
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxIntegrationTest.java
@@ -0,0 +1,454 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.apache.brooklyn.test.EntityTestUtils.assertAttributeEqualsEventually;
+import static org.apache.brooklyn.test.HttpTestUtils.assertHttpStatusCodeEquals;
+import static org.apache.brooklyn.test.HttpTestUtils.assertHttpStatusCodeEventuallyEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.webapp.JavaWebAppService;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+import org.apache.brooklyn.test.WebAppMonitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppLiveTestSupport;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.EntityFactory;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.location.Location;
+import brooklyn.test.Asserts;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+/**
+ * Test the operation of the {@link NginxController} class.
+ */
+public class NginxIntegrationTest extends BrooklynAppLiveTestSupport {
+    private static final Logger log = LoggerFactory.getLogger(NginxIntegrationTest.class);
+
+    private NginxController nginx;
+    private DynamicCluster serverPool;
+    private Location localLoc;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        localLoc = mgmt.getLocationRegistry().resolve("localhost");
+    }
+
+    public String getTestWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
+        return "classpath://hello-world.war";
+    }
+
+    /**
+     * Test that the Nginx proxy starts up and sets SERVICE_UP correctly.
+     */
+    @Test(groups = "Integration")
+    public void testWhenNoServersReturns404() {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure(DynamicCluster.FACTORY, new EntityFactory<Entity>() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        throw new UnsupportedOperationException();
+                    }}));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost"));
+        
+        app.start(ImmutableList.of(localLoc));
+        
+        assertAttributeEqualsEventually(nginx, SoftwareProcess.SERVICE_UP, true);
+        assertHttpStatusCodeEventuallyEquals(nginx.getAttribute(NginxController.ROOT_URL), 404);
+    }
+
+    @Test(groups = "Integration")
+    public void testRestart() {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure(DynamicCluster.FACTORY, new EntityFactory<Entity>() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        throw new UnsupportedOperationException();
+                    }}));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost"));
+        
+        app.start(ImmutableList.of(localLoc));
+
+        nginx.restart();
+        
+        assertAttributeEqualsEventually(nginx, SoftwareProcess.SERVICE_UP, true);
+        assertHttpStatusCodeEventuallyEquals(nginx.getAttribute(NginxController.ROOT_URL), 404);
+    }
+
+    /**
+     * Test that the Nginx proxy starts up and sets SERVICE_UP correctly.
+     */
+    @Test(groups = "Integration")
+    public void testCanStartupAndShutdown() {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT));
+        
+        app.start(ImmutableList.of(localLoc));
+        
+        // App-servers and nginx has started
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                for (Entity member : serverPool.getMembers()) {
+                    assertTrue(member.getAttribute(SoftwareProcess.SERVICE_UP));
+                }
+                assertTrue(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+            }});
+
+        // URLs reachable
+        assertHttpStatusCodeEventuallyEquals(nginx.getAttribute(NginxController.ROOT_URL), 200);
+        for (Entity member : serverPool.getMembers()) {
+            assertHttpStatusCodeEventuallyEquals(member.getAttribute(WebAppService.ROOT_URL), 200);
+        }
+
+        app.stop();
+
+        // Services have stopped
+        assertFalse(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+        assertFalse(serverPool.getAttribute(SoftwareProcess.SERVICE_UP));
+        for (Entity member : serverPool.getMembers()) {
+            assertFalse(member.getAttribute(SoftwareProcess.SERVICE_UP));
+        }
+    }
+
+    /**
+     * Test that the Nginx proxy starts up and sets SERVICE_UP correctly using the config file template.
+     */
+    @Test(groups = "Integration")
+    public void testCanStartupAndShutdownUsingTemplate() {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT)
+                .configure("configTemplate", "classpath://org/apache/brooklyn/entity/proxy/nginx/server.conf"));
+
+        app.start(ImmutableList.of(localLoc));
+
+        // App-servers and nginx has started
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                for (Entity member : serverPool.getMembers()) {
+                    assertTrue(member.getAttribute(SoftwareProcess.SERVICE_UP));
+                }
+                assertTrue(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+            }});
+
+        // URLs reachable
+        assertHttpStatusCodeEventuallyEquals(nginx.getAttribute(NginxController.ROOT_URL), 200);
+        for (Entity member : serverPool.getMembers()) {
+            assertHttpStatusCodeEventuallyEquals(member.getAttribute(WebAppService.ROOT_URL), 200);
+        }
+
+        app.stop();
+
+        // Services have stopped
+        assertFalse(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+        assertFalse(serverPool.getAttribute(SoftwareProcess.SERVICE_UP));
+        for (Entity member : serverPool.getMembers()) {
+            assertFalse(member.getAttribute(SoftwareProcess.SERVICE_UP));
+        }
+    }
+
+    /**
+     * Test that the Nginx proxy works, serving all domains, if no domain is set
+     */
+    @Test(groups = "Integration")
+    public void testDomainless() {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT));
+        
+        app.start(ImmutableList.of(localLoc));
+        
+        // App-servers and nginx has started
+        assertAttributeEqualsEventually(serverPool, SoftwareProcess.SERVICE_UP, true);
+        for (Entity member : serverPool.getMembers()) {
+            assertAttributeEqualsEventually(member, SoftwareProcess.SERVICE_UP, true);
+        }
+        assertAttributeEqualsEventually(nginx, SoftwareProcess.SERVICE_UP, true);
+
+        // URLs reachable
+        assertHttpStatusCodeEventuallyEquals(nginx.getAttribute(NginxController.ROOT_URL), 200);
+        for (Entity member : serverPool.getMembers()) {
+            assertHttpStatusCodeEventuallyEquals(member.getAttribute(WebAppService.ROOT_URL), 200);
+        }
+
+        app.stop();
+
+        // Services have stopped
+        assertFalse(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+        assertFalse(serverPool.getAttribute(SoftwareProcess.SERVICE_UP));
+        for (Entity member : serverPool.getMembers()) {
+            assertFalse(member.getAttribute(SoftwareProcess.SERVICE_UP));
+        }
+    }
+    
+    @Test(groups = "Integration")
+    public void testTwoNginxesGetDifferentPorts() {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure(DynamicCluster.FACTORY, new EntityFactory<Entity>() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        throw new UnsupportedOperationException();
+                    }}));
+        
+        NginxController nginx1 = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost")
+                .configure("port", "14000+"));
+        
+        NginxController nginx2 = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost")
+                .configure("port", "14000+"));
+        
+        app.start(ImmutableList.of(localLoc));
+
+        String url1 = nginx1.getAttribute(NginxController.ROOT_URL);
+        String url2 = nginx2.getAttribute(NginxController.ROOT_URL);
+
+        assertTrue(url1.contains(":1400"), url1);
+        assertTrue(url2.contains(":1400"), url2);
+        assertNotEquals(url1, url2, "Two nginxs should listen on different ports, not both on "+url1);
+        
+        // Nginx has started
+        assertAttributeEqualsEventually(nginx1, SoftwareProcess.SERVICE_UP, true);
+        assertAttributeEqualsEventually(nginx2, SoftwareProcess.SERVICE_UP, true);
+
+        // Nginx reachable (returning default 404)
+        assertHttpStatusCodeEventuallyEquals(url1, 404);
+        assertHttpStatusCodeEventuallyEquals(url2, 404);
+    }
+    
+    /** Test that site access does not fail even while nginx is reloaded */
+    // FIXME test disabled -- reload isn't a problem, but #365 is
+    @Test(enabled = false, groups = "Integration")
+    public void testServiceContinuity() throws Exception {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool));
+        
+        app.start(ImmutableList.of(localLoc));
+
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                for (Entity member : serverPool.getMembers()) {
+                    assertHttpStatusCodeEquals(member.getAttribute(WebAppService.ROOT_URL), 200);
+                }
+                assertHttpStatusCodeEquals(nginx.getAttribute(WebAppService.ROOT_URL), 200);
+            }});
+
+        WebAppMonitor monitor = new WebAppMonitor(nginx.getAttribute(WebAppService.ROOT_URL))
+            .logFailures(log)
+            .delayMillis(0);
+        Thread t = new Thread(monitor);
+        t.start();
+
+        try {
+            Thread.sleep(1*1000);
+            log.info("service continuity test, startup, "+monitor.getAttempts()+" requests made");
+            monitor.assertAttemptsMade(10, "startup").assertNoFailures("startup").resetCounts();
+            
+            for (int i=0; i<20; i++) {
+                nginx.reload();
+                Thread.sleep(500);
+                log.info("service continuity test, iteration "+i+", "+monitor.getAttempts()+" requests made");
+                monitor.assertAttemptsMade(10, "reloaded").assertNoFailures("reloaded").resetCounts();
+            }
+            
+        } finally {
+            t.interrupt();
+        }
+        
+        app.stop();
+
+        // Services have stopped
+        assertFalse(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+        assertFalse(serverPool.getAttribute(SoftwareProcess.SERVICE_UP));
+        for (Entity member : serverPool.getMembers()) {
+            assertFalse(member.getAttribute(SoftwareProcess.SERVICE_UP));
+        }
+    }
+
+    // FIXME test disabled -- issue #365
+    /*
+     * This currently makes no assertions, but writes out the number of sequential reqs per sec
+     * supported with nginx and jboss.
+     * <p>
+     * jboss is (now) steady, at 6k+, since we close the connections in HttpTestUtils.getHttpStatusCode.
+     * but nginx still hits problems, after about 15k reqs, something is getting starved in nginx.
+     */
+    @Test(enabled=false, groups = "Integration")
+    public void testContinuityNginxAndJboss() throws Exception {
+        serverPool = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 1)
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool));
+        
+        app.start(ImmutableList.of(localLoc));
+
+        final String nginxUrl = nginx.getAttribute(WebAppService.ROOT_URL);
+
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                for (Entity member : serverPool.getMembers()) {
+                    String jbossUrl = member.getAttribute(WebAppService.ROOT_URL);
+                    assertHttpStatusCodeEquals(jbossUrl, 200);
+                }
+                assertHttpStatusCodeEquals(nginxUrl, 200);
+            }});
+
+        final String jbossUrl = Iterables.get(serverPool.getMembers(), 0).getAttribute(WebAppService.ROOT_URL);
+        
+        Thread t = new Thread(new Runnable() {
+            public void run() {
+                long lastReportTime = System.currentTimeMillis();
+                int num = 0;
+                while (true) {
+                    try {
+                        num++;
+                        int code = HttpTestUtils.getHttpStatusCode(nginxUrl);
+                        if (code!=200) log.info("NGINX GOT: "+code);
+                        else log.debug("NGINX GOT: "+code);
+                        if (System.currentTimeMillis()>=lastReportTime+1000) {
+                            log.info("NGINX DID "+num+" requests in last "+(System.currentTimeMillis()-lastReportTime)+"ms");
+                            num=0;
+                            lastReportTime = System.currentTimeMillis();
+                        }
+                    } catch (Exception e) {
+                        log.info("NGINX GOT: "+e);
+                    }
+                }
+            }});
+        t.start();
+        
+        Thread t2 = new Thread(new Runnable() {
+            public void run() {
+                long lastReportTime = System.currentTimeMillis();
+                int num = 0;
+                while (true) {
+                    try {
+                        num++;
+                        int code = HttpTestUtils.getHttpStatusCode(jbossUrl);
+                        if (code!=200) log.info("JBOSS GOT: "+code);
+                        else log.debug("JBOSS GOT: "+code);
+                        if (System.currentTimeMillis()>=1000+lastReportTime) {
+                            log.info("JBOSS DID "+num+" requests in last "+(System.currentTimeMillis()-lastReportTime)+"ms");
+                            num=0;
+                            lastReportTime = System.currentTimeMillis();
+                        }
+                    } catch (Exception e) {
+                        log.info("JBOSS GOT: "+e);
+                    }
+                }
+            }});
+        t2.start();
+        
+        t2.join();
+    }
+
+    /**
+     * Test that the Nginx proxy starts up and sets SERVICE_UP correctly.
+     */
+    @Test(groups = "Integration")
+    public void testCanRestart() {
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", serverPool)
+                .configure("domain", "localhost")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT));
+        
+        app.start(ImmutableList.of(localLoc));
+        
+        // App-servers and nginx has started
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                assertTrue(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+            }});
+
+        log.info("started, will restart soon");
+        Time.sleep(Duration.ONE_SECOND);
+        
+        nginx.restart();
+
+        Time.sleep(Duration.ONE_SECOND);
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                assertTrue(nginx.getAttribute(SoftwareProcess.SERVICE_UP));
+            }});
+        log.info("restarted and got service up");
+    }
+    
+//    public static void main(String[] args) {
+//        NginxIntegrationTest t = new NginxIntegrationTest();
+//        t.setup();
+//        t.testCanRestart();
+//        t.shutdown();        
+//    }
+
+}