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

[11/72] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - apply org.apache package prefix to software-base, tidying package names, and moving a few sensory things to core

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/AbstractSoftlayerLiveTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/AbstractSoftlayerLiveTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/AbstractSoftlayerLiveTest.java
new file mode 100644
index 0000000..9080e51
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/AbstractSoftlayerLiveTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.factory.ApplicationBuilder;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.text.StringShortener;
+import org.apache.brooklyn.util.text.Strings;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Runs a test with many different distros and versions.
+ */
+public abstract class AbstractSoftlayerLiveTest {
+    
+    public static final String PROVIDER = "softlayer";
+    public static final int MAX_TAG_LENGTH = 20;
+    public static final int MAX_VM_NAME_LENGTH = 30;
+
+    protected BrooklynProperties brooklynProperties;
+    protected ManagementContext ctx;
+    
+    protected TestApplication app;
+    protected Location jcloudsLocation;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        List<String> propsToRemove = ImmutableList.of("imageId", "imageDescriptionRegex", "imageNameRegex", "inboundPorts", "hardwareId", "minRam");
+
+        // Don't let any defaults from brooklyn.properties (except credentials) interfere with test
+        brooklynProperties = BrooklynProperties.Factory.newDefault();
+        for (String propToRemove : propsToRemove) {
+            for (String propVariant : ImmutableList.of(propToRemove, CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, propToRemove))) {
+                brooklynProperties.remove("brooklyn.locations.jclouds."+PROVIDER+"."+propVariant);
+                brooklynProperties.remove("brooklyn.locations."+propVariant);
+                brooklynProperties.remove("brooklyn.jclouds."+PROVIDER+"."+propVariant);
+                brooklynProperties.remove("brooklyn.jclouds."+propVariant);
+            }
+        }
+
+        // Also removes scriptHeader (e.g. if doing `. ~/.bashrc` and `. ~/.profile`, then that can cause "stdin: is not a tty")
+        brooklynProperties.remove("brooklyn.ssh.config.scriptHeader");
+        
+        ctx = new LocalManagementContext(brooklynProperties);
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, ctx);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test(groups = {"Live"})
+    public void test_Default() throws Exception {
+        runTest(ImmutableMap.<String,Object>of());
+    }
+
+    @Test(groups = {"Live"})
+    public void test_Ubuntu_12_0_4() throws Exception {
+        // Image: {id=UBUNTU_12_64, providerId=UBUNTU_12_64, os={family=ubuntu, version=12.04, description=Ubuntu / Ubuntu / 12.04.0-64 Minimal, is64Bit=true}, description=UBUNTU_12_64, status=AVAILABLE, loginUser=root}
+        runTest(ImmutableMap.<String,Object>of("imageId", "UBUNTU_12_64"));
+    }
+
+    @Test(groups = {"Live"})
+    public void test_Centos_6_0() throws Exception {
+      // Image: {id=CENTOS_6_64, providerId=CENTOS_6_64, os={family=centos, version=6.5, description=CentOS / CentOS / 6.5-64 LAMP for Bare Metal, is64Bit=true}, description=CENTOS_6_64, status=AVAILABLE, loginUser=root}
+        runTest(ImmutableMap.<String,Object>of("imageId", "CENTOS_6_64"));
+    }
+    
+    protected void runTest(Map<String,?> flags) throws Exception {
+        StringShortener shortener = Strings.shortener().separator("-");
+        shortener.canTruncate(getClass().getSimpleName(), MAX_TAG_LENGTH);
+        Map<String,?> allFlags = MutableMap.<String,Object>builder()
+                .put("tags", ImmutableList.of(shortener.getStringOfMaxLength(MAX_TAG_LENGTH)))
+                .put("vmNameMaxLength", MAX_VM_NAME_LENGTH)
+                .putAll(flags)
+                .build();
+        jcloudsLocation = ctx.getLocationRegistry().resolve(PROVIDER, allFlags);
+
+        doTest(jcloudsLocation);
+    }
+    
+    protected abstract void doTest(Location loc) throws Exception;
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynClusterIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynClusterIntegrationTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynClusterIntegrationTest.java
new file mode 100644
index 0000000..1879e4b
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynClusterIntegrationTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.brooklynnode;
+
+import java.io.File;
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.ExistingFileBehaviour;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.net.Networking;
+import org.apache.brooklyn.util.os.Os;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class BrooklynClusterIntegrationTest extends BrooklynAppUnitTestSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BrooklynNodeIntegrationTest.class);
+
+    private File pseudoBrooklynPropertiesFile;
+    private File pseudoBrooklynCatalogFile;
+    private File persistenceDir;
+    private LocalhostMachineProvisioningLocation loc;
+    private List<LocalhostMachineProvisioningLocation> locs;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        pseudoBrooklynPropertiesFile = Os.newTempFile("brooklynnode-test", ".properties");
+        pseudoBrooklynPropertiesFile.delete();
+
+        pseudoBrooklynCatalogFile = Os.newTempFile("brooklynnode-test", ".catalog");
+        pseudoBrooklynCatalogFile.delete();
+
+        loc = app.newLocalhostProvisioningLocation();
+        locs = ImmutableList.of(loc);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        try {
+            super.tearDown();
+        } finally {
+            if (pseudoBrooklynPropertiesFile != null) pseudoBrooklynPropertiesFile.delete();
+            if (pseudoBrooklynCatalogFile != null) pseudoBrooklynCatalogFile.delete();
+            if (persistenceDir != null) Os.deleteRecursively(persistenceDir);
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testCanStartAndStop() throws Exception {
+        BrooklynCluster cluster = app.createAndManageChild(EntitySpec.create(BrooklynCluster.class)
+                .configure(BrooklynCluster.INITIAL_SIZE, 1)
+                .configure(BrooklynNode.WEB_CONSOLE_BIND_ADDRESS, Networking.ANY_NIC)
+                .configure(BrooklynNode.ON_EXISTING_PROPERTIES_FILE, ExistingFileBehaviour.DO_NOT_USE));
+        app.start(locs);
+        Entity brooklynNode = Iterables.find(cluster.getMembers(), Predicates.instanceOf(BrooklynNode.class));
+        LOG.info("started "+app+" containing "+cluster+" for "+JavaClassNames.niceClassAndMethod());
+
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, BrooklynNode.SERVICE_UP, true);
+        EntityTestUtils.assertAttributeEqualsEventually(brooklynNode, BrooklynNode.SERVICE_UP, true);
+
+        cluster.stop();
+        EntityTestUtils.assertAttributeEquals(cluster, BrooklynNode.SERVICE_UP, false);
+    }    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeIntegrationTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeIntegrationTest.java
new file mode 100644
index 0000000..948a42a
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeIntegrationTest.java
@@ -0,0 +1,632 @@
+/*
+ * 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.brooklynnode;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.objs.proxy.EntityProxyImpl;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynEntityMirror;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeImpl;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeSshDriver;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.DeployBlueprintEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.ExistingFileBehaviour;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.StopNodeAndKillAppsEffector;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
+import org.apache.brooklyn.entity.stock.BasicApplication;
+import org.apache.brooklyn.entity.stock.BasicApplicationImpl;
+import org.apache.brooklyn.sensor.feed.http.JsonFunctions;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.http.HttpTool;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Functionals;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.net.Networking;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.basic.Locations;
+import org.apache.brooklyn.location.basic.PortRanges;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.io.Files;
+
+/**
+ * This test needs to able to access the binary artifact in order to run.
+ * The default behaviour is to take this from maven, which works pretty well if you're downloading from hosted maven.
+ * <p>
+ * This class has been updated so that it does not effect or depend on the contents of ~/.brooklyn/brooklyn.properties .
+ * <p>
+ * If you wish to supply your own version (useful if testing changes locally!), you'll need to force download of this file.
+ * The simplest way is to install:
+ * <ul>
+ * <li>file://$HOME/.brooklyn/repository/BrooklynNode/${VERSION}/BrooklynNode-${VERSION}.tar.gz - for snapshot versions (filename is default format due to lack of filename in sonatype inferencing; 
+ *     note on case-sensitive systems it might have to be all in lower case!)
+ * <li>file://$HOME/.brooklyn/repository/BrooklynNode/${VERSION}/brooklyn-${VERSION}-dist.tar.gz - for release versions, filename should match that in maven central
+ * </ul>
+ * In both cases, remember that you may also need to wipe the local apps cache ($BROOKLYN_DATA_DIR/installs/BrooklynNode).
+ * The following commands may be useful:
+ * <p>
+ * <code>
+ * cp ~/.m2/repository/org/apache/brooklyn/brooklyn-dist/0.7.0-SNAPSHOT/brooklyn-dist-0.7.0-SNAPSHOT-dist.tar.gz ~/.brooklyn/repository/BrooklynNode/0.7.0-SNAPSHOT/BrooklynNode-0.7.0-SNAPSHOT.tar.gz
+ * rm -rf /tmp/brooklyn-`whoami`/installs/BrooklynNode*
+ * </code>
+ */
+public class BrooklynNodeIntegrationTest extends BrooklynAppUnitTestSupport {
+
+    private static final Logger log = LoggerFactory.getLogger(BrooklynNodeIntegrationTest.class);
+    
+    private File pseudoBrooklynPropertiesFile;
+    private File pseudoBrooklynCatalogFile;
+    private File persistenceDir;
+    private LocalhostMachineProvisioningLocation loc;
+    private List<LocalhostMachineProvisioningLocation> locs;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        pseudoBrooklynPropertiesFile = Os.newTempFile("brooklynnode-test", ".properties");
+        pseudoBrooklynPropertiesFile.delete();
+
+        pseudoBrooklynCatalogFile = Os.newTempFile("brooklynnode-test", ".catalog");
+        pseudoBrooklynCatalogFile.delete();
+
+        loc = app.newLocalhostProvisioningLocation();
+        locs = ImmutableList.of(loc);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        try {
+            super.tearDown();
+        } finally {
+            if (pseudoBrooklynPropertiesFile != null) pseudoBrooklynPropertiesFile.delete();
+            if (pseudoBrooklynCatalogFile != null) pseudoBrooklynCatalogFile.delete();
+            if (persistenceDir != null) Os.deleteRecursively(persistenceDir);
+        }
+    }
+
+    protected EntitySpec<BrooklynNode> newBrooklynNodeSpecForTest() {
+        // poor man's way to output which test is running
+        log.info("Creating entity spec for "+JavaClassNames.callerNiceClassAndMethod(1));
+        
+        return EntitySpec.create(BrooklynNode.class)
+                .configure(BrooklynNode.WEB_CONSOLE_BIND_ADDRESS, Networking.ANY_NIC)
+                .configure(BrooklynNode.ON_EXISTING_PROPERTIES_FILE, ExistingFileBehaviour.DO_NOT_USE);
+        
+        /* yaml equivalent, for testing:
+
+location: localhost
+services:
+- type: brooklyn.entity.brooklynnode.BrooklynNode
+  bindAddress: 127.0.0.1
+  onExistingProperties: do_not_use
+
+# some other options
+  enabledHttpProtocols: [ https ]
+  managementPassword: s3cr3t
+  brooklynLocalPropertiesContents: |
+    brooklyn.webconsole.security.https.required=true
+    brooklyn.webconsole.security.users=admin
+    brooklyn.webconsole.security.user.admin.password=s3cr3t
+    brooklyn.location.localhost.enabled=false
+
+         */
+    }
+
+    @Test(groups="Integration")
+    public void testCanStartAndStop() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest());
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        EntityTestUtils.assertAttributeEqualsEventually(brooklynNode, BrooklynNode.SERVICE_UP, true);
+
+        brooklynNode.stop();
+        EntityTestUtils.assertAttributeEquals(brooklynNode, BrooklynNode.SERVICE_UP, false);
+    }
+
+    @Test(groups="Integration")
+    public void testSetsGlobalBrooklynPropertiesFromContents() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_REMOTE_PATH, pseudoBrooklynPropertiesFile.getAbsolutePath())
+                .configure(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_CONTENTS, "abc=def"));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        assertEquals(Files.readLines(pseudoBrooklynPropertiesFile, Charsets.UTF_8), ImmutableList.of("abc=def"));
+    }
+
+    @Test(groups="Integration")
+    public void testSetsLocalBrooklynPropertiesFromContents() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_REMOTE_PATH, pseudoBrooklynPropertiesFile.getAbsolutePath())
+                .configure(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS, "abc=def"));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        assertEquals(Files.readLines(pseudoBrooklynPropertiesFile, Charsets.UTF_8), ImmutableList.of("abc=def"));
+    }
+
+    @Test(groups="Integration")
+    public void testSetsBrooklynPropertiesFromUri() throws Exception {
+        File brooklynPropertiesSourceFile = File.createTempFile("brooklynnode-test", ".properties");
+        Files.write("abc=def", brooklynPropertiesSourceFile, Charsets.UTF_8);
+
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_REMOTE_PATH, pseudoBrooklynPropertiesFile.getAbsolutePath())
+                .configure(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_URI, brooklynPropertiesSourceFile.toURI().toString()));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        assertEquals(Files.readLines(pseudoBrooklynPropertiesFile, Charsets.UTF_8), ImmutableList.of("abc=def"));
+    }
+
+    @Test(groups="Integration")
+    public void testSetsBrooklynCatalogFromContents() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.BROOKLYN_CATALOG_REMOTE_PATH, pseudoBrooklynCatalogFile.getAbsolutePath())
+                .configure(BrooklynNode.BROOKLYN_CATALOG_CONTENTS, "<catalog/>"));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        assertEquals(Files.readLines(pseudoBrooklynCatalogFile, Charsets.UTF_8), ImmutableList.of("<catalog/>"));
+    }
+
+    @Test(groups="Integration")
+    public void testSetsBrooklynCatalogFromUri() throws Exception {
+        File brooklynCatalogSourceFile = File.createTempFile("brooklynnode-test", ".catalog");
+        Files.write("abc=def", brooklynCatalogSourceFile, Charsets.UTF_8);
+
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.BROOKLYN_CATALOG_REMOTE_PATH, pseudoBrooklynCatalogFile.getAbsolutePath())
+                .configure(BrooklynNode.BROOKLYN_CATALOG_URI, brooklynCatalogSourceFile.toURI().toString()));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        assertEquals(Files.readLines(pseudoBrooklynCatalogFile, Charsets.UTF_8), ImmutableList.of("abc=def"));
+    }
+
+    @Test(groups="Integration")
+    public void testCopiesResources() throws Exception {
+        File sourceFile = File.createTempFile("brooklynnode-test", ".properties");
+        Files.write("abc=def", sourceFile, Charsets.UTF_8);
+        File tempDir = Files.createTempDir();
+        File expectedFile = new File(tempDir, "myfile.txt");
+
+        try {
+            BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                    .configure(BrooklynNode.RUN_DIR, tempDir.getAbsolutePath())
+                    .configure(BrooklynNode.COPY_TO_RUNDIR, ImmutableMap.of(sourceFile.getAbsolutePath(), "${RUN}/myfile.txt")));
+            app.start(locs);
+            log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+            assertEquals(Files.readLines(expectedFile, Charsets.UTF_8), ImmutableList.of("abc=def"));
+        } finally {
+            expectedFile.delete();
+            tempDir.delete();
+            sourceFile.delete();
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testCopiesClasspathEntriesInConfigKey() throws Exception {
+        String content = "abc=def";
+        File classpathEntry1 = File.createTempFile("first", ".properties");
+        File classpathEntry2 = File.createTempFile("second", ".properties");
+        Files.write(content, classpathEntry1, Charsets.UTF_8);
+        Files.write(content, classpathEntry2, Charsets.UTF_8);
+        File tempDir = Files.createTempDir();
+        File expectedFile1 = new File(new File(tempDir, "lib"), classpathEntry1.getName());
+        File expectedFile2 = new File(new File(tempDir, "lib"), classpathEntry2.getName());
+
+        try {
+            BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                    .configure(BrooklynNode.RUN_DIR, tempDir.getAbsolutePath())
+                    .configure(BrooklynNode.CLASSPATH, ImmutableList.of(classpathEntry1.getAbsolutePath(), classpathEntry2.getAbsolutePath()))
+                    );
+            app.start(locs);
+            log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+            assertEquals(Files.readLines(expectedFile1, Charsets.UTF_8), ImmutableList.of(content));
+            assertEquals(Files.readLines(expectedFile2, Charsets.UTF_8), ImmutableList.of(content));
+        } finally {
+            expectedFile1.delete();
+            expectedFile2.delete();
+            tempDir.delete();
+            classpathEntry1.delete();
+            classpathEntry2.delete();
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testCopiesClasspathEntriesInBrooklynProperties() throws Exception {
+        String content = "abc=def";
+        File classpathEntry1 = File.createTempFile("first", ".properties");
+        File classpathEntry2 = File.createTempFile("second", ".properties");
+        Files.write(content, classpathEntry1, Charsets.UTF_8);
+        Files.write(content, classpathEntry2, Charsets.UTF_8);
+        File tempDir = Files.createTempDir();
+        File expectedFile1 = new File(new File(tempDir, "lib"), classpathEntry1.getName());
+        File expectedFile2 = new File(new File(tempDir, "lib"), classpathEntry2.getName());
+
+        try {
+            String propName = BrooklynNode.CLASSPATH.getName();
+            String propValue = classpathEntry1.toURI().toString() + "," + classpathEntry2.toURI().toString();
+            ((BrooklynProperties)app.getManagementContext().getConfig()).put(propName, propValue);
+    
+            BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                    .configure(BrooklynNode.RUN_DIR, tempDir.getAbsolutePath())
+                    );
+            app.start(locs);
+            log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+            assertEquals(Files.readLines(expectedFile1, Charsets.UTF_8), ImmutableList.of(content));
+            assertEquals(Files.readLines(expectedFile2, Charsets.UTF_8), ImmutableList.of(content));
+        } finally {
+            expectedFile1.delete();
+            expectedFile2.delete();
+            tempDir.delete();
+            classpathEntry1.delete();
+            classpathEntry2.delete();
+        }
+    }
+    
+    // TODO test that the classpath set above is actually used
+
+    @Test(groups="Integration")
+    public void testSetsBrooklynWebConsolePort() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.HTTP_PORT, PortRanges.fromString("45000+")));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        Integer httpPort = brooklynNode.getAttribute(BrooklynNode.HTTP_PORT);
+        URI webConsoleUri = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI);
+        assertTrue(httpPort >= 45000 && httpPort < 54100, "httpPort="+httpPort);
+        assertEquals((Integer)webConsoleUri.getPort(), httpPort);
+        HttpTestUtils.assertHttpStatusCodeEquals(webConsoleUri.toString(), 200, 401);
+    }
+
+    @Test(groups="Integration")
+    public void testStartsAppOnStartup() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.APP, BasicApplicationImpl.class.getName()));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        URI webConsoleUri = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI);
+        waitForApps(webConsoleUri, 1);
+        String apps = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications");
+        List<String> appType = parseJsonList(apps, ImmutableList.of("spec", "type"), String.class);
+        assertEquals(appType, ImmutableList.of(BasicApplication.class.getName()));
+    }
+
+    protected static void waitForApps(String webConsoleUri) {
+        HttpTestUtils.assertHttpStatusCodeEquals(webConsoleUri+"/v1/applications", 200, 403);
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals(webConsoleUri+"/v1/applications", 200);
+    }
+
+    // TODO Should introduce startup stages and let the client select which stage it expects to be complete
+    protected void waitForApps(final URI webConsoleUri, final int num) {
+        waitForApps(webConsoleUri.toString());
+        
+        // e.g. [{"id":"UnBqPcqg","spec":{"name":"Application (UnBqPcqg)","type":"brooklyn.entity.basic.BasicApplication","locations":["pOL4NtiW"]},"status":"RUNNING","links":{"self":"/v1/applications/UnBqPcqg","entities":"/v1/applications/UnBqPcqg/entities"}}]
+        Asserts.succeedsEventually(new Runnable() {
+            @Override
+            public void run() {
+                //Wait all apps to become managed
+                String appsContent = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications");
+                List<String> appIds = parseJsonList(appsContent, ImmutableList.of("id"), String.class);
+                assertEquals(appIds.size(), num);
+                
+                // and then to start
+                List<String> statuses = parseJsonList(appsContent, ImmutableList.of("status"), String.class);
+                for (String status : statuses) {
+                    assertEquals(status, Lifecycle.RUNNING.toString().toUpperCase());
+                }
+            }});
+    }
+
+    @Test(groups="Integration")
+    public void testStartsAppViaEffector() throws Exception {
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest());
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+        
+        // note there is also a test for this in DeployApplication
+        final URI webConsoleUri = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI);
+        waitForApps(webConsoleUri.toString());
+
+        final String id = brooklynNode.invoke(BrooklynNode.DEPLOY_BLUEPRINT, ConfigBag.newInstance()
+            .configure(DeployBlueprintEffector.BLUEPRINT_TYPE, BasicApplication.class.getName())
+            .getAllConfig()).get();
+        
+        String apps = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications");
+        List<String> appType = parseJsonList(apps, ImmutableList.of("spec", "type"), String.class);
+        assertEquals(appType, ImmutableList.of(BasicApplication.class.getName()));
+        
+        HttpTestUtils.assertContentEventuallyMatches(
+            webConsoleUri.toString()+"/v1/applications/"+id+"/entities/"+id+"/sensors/service.state",
+            "\"?(running|RUNNING)\"?");
+    }
+    
+    @Test(groups="Integration")
+    public void testUsesLocation() throws Exception {
+        String brooklynPropertiesContents = 
+            "brooklyn.location.named.mynamedloc=localhost:(name=myname)\n"+
+                //force lat+long so test will work when offline
+                "brooklyn.location.named.mynamedloc.latitude=123\n"+ 
+                "brooklyn.location.named.mynamedloc.longitude=45.6\n";
+
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+            .configure(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS, brooklynPropertiesContents)
+            .configure(BrooklynNode.APP, BasicApplicationImpl.class.getName())
+            .configure(BrooklynNode.LOCATIONS, "named:mynamedloc"));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        URI webConsoleUri = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI);
+        waitForApps(webConsoleUri, 1);
+
+        // Check that "mynamedloc" has been picked up from the brooklyn.properties
+        String locsContent = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/locations");
+        List<String> locNames = parseJsonList(locsContent, ImmutableList.of("name"), String.class);
+        assertTrue(locNames.contains("mynamedloc"), "locNames="+locNames);
+
+        // Find the id of the concrete location instance of the app
+        String appsContent = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications");
+        List<String[]> appLocationIds = parseJsonList(appsContent, ImmutableList.of("spec", "locations"), String[].class);
+        String appLocationId = Iterables.getOnlyElement(appLocationIds)[0];  // app.getManagementContext().getLocationRegistry()
+
+        // Check that the concrete location is of the required type
+        String locatedLocationsContent = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/locations/usage/LocatedLocations");
+        assertEquals(parseJson(locatedLocationsContent, ImmutableList.of(appLocationId, "name"), String.class), "myname");
+        assertEquals(parseJson(locatedLocationsContent, ImmutableList.of(appLocationId, "longitude"), Double.class), 45.6, 0.00001);
+    }
+
+    @Test(groups="Integration")
+    public void testAuthenticationAndHttps() throws Exception {
+        String adminPassword = "p4ssw0rd";
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+            .configure(BrooklynNode.ENABLED_HTTP_PROTOCOLS, ImmutableList.of("https"))
+            .configure(BrooklynNode.MANAGEMENT_PASSWORD, adminPassword)
+            .configure(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS,
+                Strings.lines(
+                    "brooklyn.webconsole.security.https.required=true",
+                    "brooklyn.webconsole.security.users=admin",
+                    "brooklyn.webconsole.security.user.admin.password="+adminPassword,
+                    "brooklyn.location.localhost.enabled=false") )
+            );
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        URI webConsoleUri = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI);
+        Assert.assertTrue(webConsoleUri.toString().startsWith("https://"), "web console not https: "+webConsoleUri);
+        Integer httpsPort = brooklynNode.getAttribute(BrooklynNode.HTTPS_PORT);
+        Assert.assertTrue(httpsPort!=null && httpsPort >= 8443 && httpsPort <= 8500);
+        Assert.assertTrue(webConsoleUri.toString().contains(""+httpsPort), "web console not using right https port ("+httpsPort+"): "+webConsoleUri);
+        HttpTestUtils.assertHttpStatusCodeEquals(webConsoleUri.toString(), 401);
+
+        HttpClient http = HttpTool.httpClientBuilder()
+            .trustAll()
+            .uri(webConsoleUri)
+            .laxRedirect(true)
+            .credentials(new UsernamePasswordCredentials("admin", adminPassword))
+            .build();
+        HttpToolResponse response = HttpTool.httpGet(http, webConsoleUri, MutableMap.<String,String>of());
+        Assert.assertEquals(response.getResponseCode(), 200);
+    }
+
+    @Test(groups="Integration")
+    public void testStopPlainThrowsException() throws Exception {
+        BrooklynNode brooklynNode = setUpBrooklynNodeWithApp();
+
+        // Not using annotation with `expectedExceptions = PropagatedRuntimeException.class` because want to 
+        // ensure exception comes from stop. On jenkins, was seeing setUpBrooklynNodeWithApp fail in 
+        // testStopAndKillAppsEffector; so can't tell if this method was really passing!
+        try {
+            brooklynNode.stop();
+            fail("Expected "+brooklynNode+" stop to fail, because has app");
+        } catch (Exception e) {
+            IllegalStateException ise = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
+            if (ise != null && ise.toString().contains("Can't stop instance with running applications")) {
+                // success
+            } else {
+                throw e;
+            }
+        } finally {
+            try {
+                brooklynNode.invoke(BrooklynNode.STOP_NODE_AND_KILL_APPS, ImmutableMap.of(StopNodeAndKillAppsEffector.TIMEOUT.getName(), Duration.THIRTY_SECONDS)).getUnchecked();
+            } catch (Exception e) {
+                log.warn("Error in stopNodeAndKillApps for "+brooklynNode+" (continuing)", e);
+            }
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testStopAndKillAppsEffector() throws Exception {
+        createNodeAndExecStopEffector(BrooklynNode.STOP_NODE_AND_KILL_APPS);
+    }
+
+    @Test(groups="Integration")
+    public void testStopButLeaveAppsEffector() throws Exception {
+        createNodeAndExecStopEffector(BrooklynNode.STOP_NODE_BUT_LEAVE_APPS);
+    }
+    
+    @Test(groups="Integration")
+    public void testStopAndRestartProcess() throws Exception {
+        persistenceDir = Files.createTempDir();
+        BrooklynNode brooklynNode = app.createAndManageChild(newBrooklynNodeSpecForTest()
+                .configure(BrooklynNode.EXTRA_LAUNCH_PARAMETERS, "--persist auto --persistenceDir "+persistenceDir.getAbsolutePath())
+                .configure(BrooklynNode.APP, BasicApplicationImpl.class.getName()));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+        File pidFile = new File(getDriver(brooklynNode).getPidFile());
+        URI webConsoleUri = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI);
+
+        waitForApps(webConsoleUri, 1);
+
+        // Stop just the process; will not have unmanaged entity unless machine was being terminated 
+        brooklynNode.invoke(BrooklynNode.STOP, ImmutableMap.<String, Object>of(
+                BrooklynNode.StopSoftwareParameters.STOP_MACHINE_MODE.getName(), StopMode.NEVER,
+                BrooklynNode.StopSoftwareParameters.STOP_PROCESS_MODE.getName(), StopMode.ALWAYS)).getUnchecked();
+
+        assertTrue(Entities.isManaged(brooklynNode));
+        assertFalse(isPidRunning(pidFile), "pid in "+pidFile+" still running");
+        
+        // Clear the startup app so it's not started second time, in addition to the rebind state
+        // TODO remove this once the startup app is created only if no previous persistence state
+        brooklynNode.config().set(BrooklynNode.APP, (String)null);
+        ((EntityLocal)brooklynNode).setAttribute(BrooklynNode.APP, null);
+        
+        // Restart the process; expect persisted state to have been restored, so apps still known about
+        brooklynNode.invoke(BrooklynNode.RESTART, ImmutableMap.<String, Object>of(
+                BrooklynNode.RestartSoftwareParameters.RESTART_MACHINE.getName(), "false")).getUnchecked();
+
+        waitForApps(webConsoleUri.toString());
+        String apps = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications");
+        List<String> appType = parseJsonList(apps, ImmutableList.of("spec", "type"), String.class);
+        assertEquals(appType, ImmutableList.of(BasicApplication.class.getName()));
+    }
+
+    private void createNodeAndExecStopEffector(Effector<?> eff) throws Exception {
+        BrooklynNode brooklynNode = setUpBrooklynNodeWithApp();
+        File pidFile = new File(getDriver(brooklynNode).getPidFile());
+        assertTrue(isPidRunning(pidFile));
+        
+        brooklynNode.invoke(eff, Collections.<String, Object>emptyMap()).getUnchecked();
+
+        // Note can't use driver.isRunning to check shutdown; can't invoke scripts on an unmanaged entity
+        EntityTestUtils.assertAttributeEquals(brooklynNode, BrooklynNode.SERVICE_UP, false);
+        
+        // unmanaged if the machine is destroyed - ie false on localhost (this test by default), but true in the cloud 
+//        assertFalse(Entities.isManaged(brooklynNode));
+        
+        assertFalse(isPidRunning(pidFile), "pid in "+pidFile+" still running");
+    }
+
+    private boolean isPidRunning(File pidFile) throws Exception {
+        SshMachineLocation machine = loc.obtain();
+        try {
+            int result = machine.execScript("check-pid", ImmutableList.of(
+                    "test -f "+pidFile+" || exit 1",
+                    "ps -p `cat "+pidFile+"`"));
+            return result == 0;
+        } finally {
+            loc.release(machine);
+            Locations.unmanage(machine);
+        }
+    }
+    
+    private BrooklynNodeSshDriver getDriver(BrooklynNode brooklynNode) {
+        try {
+            EntityProxyImpl entityProxy = (EntityProxyImpl)Proxy.getInvocationHandler(brooklynNode);
+            Method getDriver = BrooklynNodeImpl.class.getMethod("getDriver");
+            return (BrooklynNodeSshDriver)entityProxy.invoke(brooklynNode, getDriver, new Object[]{});
+        } catch (Throwable e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    private BrooklynNode setUpBrooklynNodeWithApp() throws InterruptedException,
+            ExecutionException {
+        BrooklynNode brooklynNode = app.createAndManageChild(EntitySpec.create(BrooklynNode.class)
+                .configure(BrooklynNode.NO_WEB_CONSOLE_AUTHENTICATION, Boolean.TRUE));
+        app.start(locs);
+        log.info("started "+app+" containing "+brooklynNode+" for "+JavaClassNames.niceClassAndMethod());
+
+        EntityTestUtils.assertAttributeEqualsEventually(brooklynNode, BrooklynNode.SERVICE_UP, true);
+
+        String baseUrl = brooklynNode.getAttribute(BrooklynNode.WEB_CONSOLE_URI).toString();
+        waitForApps(baseUrl);
+        
+        final String id = brooklynNode.invoke(BrooklynNode.DEPLOY_BLUEPRINT, ConfigBag.newInstance()
+                .configure(DeployBlueprintEffector.BLUEPRINT_TYPE, BasicApplication.class.getName())
+                .getAllConfig()).get();
+
+        String entityUrl = Urls.mergePaths(baseUrl, "v1/applications/", id, "entities", id);
+        
+        Entity mirror = brooklynNode.addChild(EntitySpec.create(BrooklynEntityMirror.class)
+                .configure(BrooklynEntityMirror.MIRRORED_ENTITY_URL, entityUrl)
+                .configure(BrooklynEntityMirror.MIRRORED_ENTITY_ID, id));
+        Entities.manage(mirror);
+
+        assertEquals(brooklynNode.getChildren().size(), 1);
+        return brooklynNode;
+    }
+
+    private <T> T parseJson(String json, List<String> elements, Class<T> clazz) {
+        Function<String, T> func = Functionals.chain(
+                JsonFunctions.asJson(),
+                JsonFunctions.walk(elements),
+                JsonFunctions.cast(clazz));
+        return func.apply(json);
+    }
+
+    private <T> List<T> parseJsonList(String json, List<String> elements, Class<T> clazz) {
+        Function<String, List<T>> func = Functionals.chain(
+                JsonFunctions.asJson(),
+                JsonFunctions.forEach(Functionals.chain(
+                        JsonFunctions.walk(elements),
+                        JsonFunctions.cast(clazz))));
+        return func.apply(json);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeTest.java
new file mode 100644
index 0000000..60d2c6b
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynNodeTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.brooklynnode;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolver;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeImpl;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeSshDriver;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.trait.Startable;
+import org.apache.brooklyn.sensor.feed.ConfigToAttributes;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+public class BrooklynNodeTest {
+
+    // TODO Need test for copying/setting classpath
+    
+    private TestApplication app;
+    private SshMachineLocation loc;
+    
+    public static class SlowStopBrooklynNode extends BrooklynNodeImpl {
+        public SlowStopBrooklynNode() {}
+        
+        @Override
+        protected void postStop() {
+            super.postStop();
+            
+            //Make sure UnmanageTask will wait for the STOP effector to complete.
+            Time.sleep(Duration.FIVE_SECONDS);
+        }
+        
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        loc = new SshMachineLocation(MutableMap.of("address", "localhost"));
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+    
+    @Test
+    public void testGeneratesCorrectSnapshotDownload() throws Exception {
+        String version = "0.0.1-SNAPSHOT";
+        String expectedUrl = "https://repository.apache.org/service/local/artifact/maven/redirect?r=snapshots&g=org.apache.brooklyn&v="+version+"&a=brooklyn-dist&c=dist&e=tar.gz";
+        runTestGeneratesCorrectDownloadUrl(version, expectedUrl);
+    }
+    
+    @Test
+    public void testGeneratesCorrectReleaseDownload() throws Exception {
+        String version = "0.0.1";
+        String expectedUrl = "http://search.maven.org/remotecontent?filepath=org/apache/brooklyn/brooklyn-dist/"+version+"/brooklyn-dist-"+version+"-dist.tar.gz";
+        runTestGeneratesCorrectDownloadUrl(version, expectedUrl);
+    }
+    
+    private void runTestGeneratesCorrectDownloadUrl(String version, String expectedUrl) throws Exception {
+        // TODO Using BrooklynNodeImpl directly, because want to instantiate a BroolynNodeSshDriver.
+        //      Really want to make that easier to test, without going through "wrong" code path for creating entity.
+        BrooklynNodeImpl entity = new BrooklynNodeImpl();
+        entity.setConfig(BrooklynNode.SUGGESTED_VERSION, version);
+        entity.setParent(app);
+        Entities.manage(entity);
+        ConfigToAttributes.apply(entity);
+        BrooklynNodeSshDriver driver = new BrooklynNodeSshDriver(entity, loc);
+        
+        DownloadResolver resolver = Entities.newDownloader(driver);
+        List<String> urls = resolver.getTargets();
+        
+        System.out.println("urls="+urls);
+        assertTrue(urls.contains(expectedUrl), "urls="+urls);
+    }
+    
+    @Test(groups = "Integration")
+    public void testUnmanageOnStop() throws Exception {
+        final BrooklynNode node = app.addChild(EntitySpec.create(BrooklynNode.class).impl(SlowStopBrooklynNode.class));
+        Entities.manage(node);
+        assertTrue(Entities.isManaged(node), "Entity " + node + " must be managed.");
+        node.invoke(Startable.STOP, ImmutableMap.<String,Object>of()).asTask().getUnchecked();
+        //The UnmanageTask will unblock after the STOP effector completes, so we are competing with it here.
+        Asserts.succeedsEventually(new Runnable() {
+            @Override
+            public void run() {
+                assertFalse(Entities.isManaged(node));
+            }
+        });
+    }
+    
+
+    @Test
+    public void testCanStartSameNode() throws Exception {
+        // not very interesting as do not have REST when run in this project
+        // but test BrooklynNodeRestTest in downstream project does
+        BrooklynNode bn = app.createAndManageChild(EntitySpec.create(BrooklynNode.class, SameBrooklynNodeImpl.class));
+        bn.start(MutableSet.<Location>of());
+        
+        Assert.assertEquals(bn.getAttribute(Attributes.SERVICE_UP), (Boolean)true);
+        // no URI
+        Assert.assertNull(bn.getAttribute(BrooklynNode.WEB_CONSOLE_URI));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/CallbackEntityHttpClient.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/CallbackEntityHttpClient.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/CallbackEntityHttpClient.java
new file mode 100644
index 0000000..c933b9d
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/CallbackEntityHttpClient.java
@@ -0,0 +1,100 @@
+/*
+ * 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.brooklynnode;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.brooklyn.util.core.http.HttpTool.HttpClientBuilder;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+
+public class CallbackEntityHttpClient implements EntityHttpClient {
+    public static class Request {
+        private Entity entity;
+        private String method;
+        private String path;
+        private Map<String, String> params;
+        public Request(Entity entity, String method, String path, Map<String, String> params) {
+            this.entity = entity;
+            this.method = method;
+            this.path = path;
+            this.params = params;
+        }
+        public Entity getEntity() {
+            return entity;
+        }
+        public String getMethod() {
+            return method;
+        }
+        public String getPath() {
+            return path;
+        }
+        public Map<String, String> getParams() {
+            return params;
+        }
+    }
+    private Function<Request, String> callback;
+    private Entity entity;
+
+    public CallbackEntityHttpClient(Entity entity, Function<Request, String> callback) {
+        this.entity = entity;
+        this.callback = callback;
+    }
+
+    @Override
+    public HttpClientBuilder getHttpClientForBrooklynNode() {
+        throw new IllegalStateException("Method call not expected");
+    }
+
+    @Override
+    public HttpToolResponse get(String path) {
+        String result = callback.apply(new Request(entity, HttpGet.METHOD_NAME, path, Collections.<String, String>emptyMap()));
+        return new HttpToolResponse(HttpStatus.SC_OK, null, result.getBytes(), 0, 0, 0);
+    }
+
+    @Override
+    public HttpToolResponse post(String path, Map<String, String> headers, byte[] body) {
+        throw new IllegalStateException("Method call not expected");
+    }
+
+    @Override
+    public HttpToolResponse post(String path, Map<String, String> headers, Map<String, String> formParams) {
+        String result = callback.apply(new Request(entity, HttpPost.METHOD_NAME, path, formParams));
+        return new HttpToolResponse(HttpStatus.SC_OK, Collections.<String, List<String>>emptyMap(), result.getBytes(), 0, 0, 0);
+    }
+    
+    @Override
+    public HttpToolResponse delete(String path, Map<String, String> headers) {
+        throw new IllegalStateException("Method call not expected");
+    }
+
+    @Override
+    public EntityHttpClient responseSuccess(Predicate<Integer> successPredicate) {
+        throw new IllegalStateException("Method call not expected");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/MockBrooklynNode.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/MockBrooklynNode.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/MockBrooklynNode.java
new file mode 100644
index 0000000..fcc939f
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/MockBrooklynNode.java
@@ -0,0 +1,72 @@
+/*
+ * 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.brooklynnode;
+
+import java.util.Collection;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.entity.brooklynnode.CallbackEntityHttpClient.Request;
+import org.apache.brooklyn.entity.brooklynnode.effector.SetHighAvailabilityModeEffectorBody;
+import org.apache.brooklyn.entity.brooklynnode.effector.SetHighAvailabilityPriorityEffectorBody;
+import org.apache.brooklyn.entity.core.AbstractEntity;
+import org.apache.brooklyn.sensor.core.BasicAttributeSensor;
+
+import com.google.common.base.Function;
+import com.google.common.reflect.TypeToken;
+
+public class MockBrooklynNode extends AbstractEntity implements BrooklynNode {
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Function<Request, String>> HTTP_CLIENT_CALLBACK = ConfigKeys.newConfigKey(new TypeToken<Function<Request, String>>(){}, "httpClientCallback");
+    public static final AttributeSensor<Integer> HA_PRIORITY = new BasicAttributeSensor<Integer>(Integer.class, "priority");
+    
+    @Override
+    public void init() {
+        super.init();
+        getMutableEntityType().addEffector(SetHighAvailabilityPriorityEffectorBody.SET_HIGH_AVAILABILITY_PRIORITY);
+        getMutableEntityType().addEffector(SetHighAvailabilityModeEffectorBody.SET_HIGH_AVAILABILITY_MODE);
+        setAttribute(HA_PRIORITY, 0);
+    }
+
+    @Override
+    public EntityHttpClient http() {
+        return new CallbackEntityHttpClient(this, getConfig(HTTP_CLIENT_CALLBACK));
+    }
+
+    @Override
+    public void start(Collection<? extends Location> locations) {
+    }
+
+    @Override
+    public void stop() {
+    }
+
+    @Override
+    public void restart() {
+    }
+
+    @Override
+    public void populateServiceNotUpDiagnostics() {
+        // no-op
+    }    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SameBrooklynNodeImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SameBrooklynNodeImpl.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SameBrooklynNodeImpl.java
new file mode 100644
index 0000000..84d2c33
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SameBrooklynNodeImpl.java
@@ -0,0 +1,97 @@
+/*
+ * 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.brooklynnode;
+
+import java.net.URI;
+import java.util.Collection;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeImpl.DeployBlueprintEffectorBody;
+import org.apache.brooklyn.entity.core.AbstractEntity;
+import org.apache.brooklyn.sensor.feed.http.HttpFeed;
+import org.apache.brooklyn.sensor.feed.http.HttpPollConfig;
+import org.apache.brooklyn.sensor.feed.http.HttpValueFunctions;
+
+/** Implementation of BrooklynNode which just presents the node where this is running, for convenience;
+ * 
+ *  start/stop/restart have no effect; 
+ *  sensors are connected;
+ *  deploy blueprint assumes that a REST endpoint is available */
+public class SameBrooklynNodeImpl extends AbstractEntity implements BrooklynNode {
+    
+    private HttpFeed httpFeed;
+
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        connectSensors();
+    }
+
+    @Override
+    public void stop() {
+        disconnectSensors();
+    }
+
+    @Override
+    public void restart() {
+        return;
+    }
+
+    
+    @Override
+    public void init() {
+        super.init();
+        getMutableEntityType().addEffector(DeployBlueprintEffectorBody.DEPLOY_BLUEPRINT);
+    }
+    
+    protected void connectSensors() {
+        URI webConsoleUri = getManagementContext().getManagementNodeUri().orNull();
+        setAttribute(WEB_CONSOLE_URI, webConsoleUri);
+
+        if (webConsoleUri != null) {
+            httpFeed = HttpFeed.builder()
+                    .entity(this)
+                    .period(200)
+                    .baseUri(webConsoleUri)
+                    .credentialsIfNotNull(getConfig(MANAGEMENT_USER), getConfig(MANAGEMENT_PASSWORD))
+                    .poll(new HttpPollConfig<Boolean>(SERVICE_UP)
+                            .onSuccess(HttpValueFunctions.responseCodeEquals(200))
+                            .setOnFailureOrException(false))
+                    .build();
+
+        } else {
+            setAttribute(SERVICE_UP, true);
+        }
+    }
+    
+    protected void disconnectSensors() {
+        if (httpFeed != null) httpFeed.stop();
+    }
+
+    @Override
+    public EntityHttpClient http() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void populateServiceNotUpDiagnostics() {
+        // no-op
+    }    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SelectMasterEffectorTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SelectMasterEffectorTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SelectMasterEffectorTest.java
new file mode 100644
index 0000000..60cc6e9
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/brooklynnode/SelectMasterEffectorTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.brooklynnode;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynClusterImpl;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.SelectMasterEffector;
+import org.apache.brooklyn.entity.brooklynnode.CallbackEntityHttpClient.Request;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.sensor.feed.AttributePollHandler;
+import org.apache.brooklyn.sensor.feed.DelegatingPollHandler;
+import org.apache.brooklyn.sensor.feed.Poller;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.http.client.methods.HttpPost;
+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 com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+
+public class SelectMasterEffectorTest extends BrooklynAppUnitTestSupport {
+    private static final Logger LOG = LoggerFactory.getLogger(BrooklynClusterImpl.class);
+
+    protected BrooklynCluster cluster;
+    protected HttpCallback http; 
+    protected Poller<Void> poller;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // because the effector calls wait for a state change, use a separate thread to drive that 
+        poller = new Poller<Void>((EntityLocal)app, false);
+        poller.scheduleAtFixedRate(
+            new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    masterFailoverIfNeeded();
+                    return null;
+                }
+            },
+            new DelegatingPollHandler<Void>(Collections.<AttributePollHandler<? super Void>>emptyList()),
+            Duration.millis(20));
+        poller.start();
+    }
+
+    @Override
+    protected void setUpApp() {
+        super.setUpApp();
+        http = new HttpCallback();
+        cluster = app.createAndManageChild(EntitySpec.create(BrooklynCluster.class)
+            .location(app.newLocalhostProvisioningLocation())
+            .configure(BrooklynCluster.MEMBER_SPEC, EntitySpec.create(BrooklynNode.class)
+                .impl(MockBrooklynNode.class)
+                .configure(MockBrooklynNode.HTTP_CLIENT_CALLBACK, http)));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        poller.stop();
+        super.tearDown();
+    }
+
+    @Test
+    public void testInvalidNewMasterIdFails() {
+        try {
+            selectMaster(cluster, "1234");
+            fail("Non-existend entity ID provided.");
+        } catch (Exception e) {
+            assertTrue(e.toString().contains("1234 is not an ID of brooklyn node in this cluster"));
+        }
+    }
+
+    @Test(groups="Integration") // because slow, due to sensor feeds
+    public void testSelectMasterAfterChange() {
+        List<Entity> nodes = makeTwoNodes();
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, BrooklynCluster.MASTER_NODE, (BrooklynNode)nodes.get(0));
+
+        selectMaster(cluster, nodes.get(1).getId());
+        checkMaster(cluster, nodes.get(1));
+    }
+
+    @Test
+    public void testFindMaster() {
+        List<Entity> nodes = makeTwoNodes();
+        Assert.assertEquals(((BrooklynClusterImpl)Entities.deproxy(cluster)).findMasterChild(), nodes.get(0));
+    }
+    
+    @Test(groups="Integration") // because slow, due to sensor feeds
+    public void testSelectMasterFailsAtChangeState() {
+        http.setFailAtStateChange(true);
+
+        List<Entity> nodes = makeTwoNodes();
+        
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, BrooklynCluster.MASTER_NODE, (BrooklynNode)nodes.get(0));
+
+        try {
+            selectMaster(cluster, nodes.get(1).getId());
+            fail("selectMaster should have failed");
+        } catch (Exception e) {
+            // expected
+        }
+        checkMaster(cluster, nodes.get(0));
+    }
+
+    private List<Entity> makeTwoNodes() {
+        List<Entity> nodes = MutableList.copyOf(cluster.resizeByDelta(2));
+        setManagementState(nodes.get(0), ManagementNodeState.MASTER);
+        setManagementState(nodes.get(1), ManagementNodeState.HOT_STANDBY);
+        return nodes;
+    }
+
+    private void checkMaster(Group cluster, Entity node) {
+        assertEquals(node.getAttribute(BrooklynNode.MANAGEMENT_NODE_STATE), ManagementNodeState.MASTER);
+        assertEquals(cluster.getAttribute(BrooklynCluster.MASTER_NODE), node);
+        for (Entity member : cluster.getMembers()) {
+            if (member != node) {
+                assertEquals(member.getAttribute(BrooklynNode.MANAGEMENT_NODE_STATE), ManagementNodeState.HOT_STANDBY);
+            }
+            assertEquals((int)member.getAttribute(MockBrooklynNode.HA_PRIORITY), 0);
+        }
+    }
+
+    private static class HttpCallback implements Function<CallbackEntityHttpClient.Request, String> {
+        private enum State {
+            INITIAL,
+            PROMOTED
+        }
+        private State state = State.INITIAL;
+        private boolean failAtStateChange;
+
+        @Override
+        public String apply(Request input) {
+            if ("/v1/server/ha/state".equals(input.getPath())) {
+                if (failAtStateChange) {
+                    throw new RuntimeException("Testing failure at changing node state");
+                }
+
+                checkRequest(input, HttpPost.METHOD_NAME, "/v1/server/ha/state", "mode", "HOT_STANDBY");
+                Entity entity = input.getEntity();
+                EntityTestUtils.assertAttributeEquals(entity, BrooklynNode.MANAGEMENT_NODE_STATE, ManagementNodeState.MASTER);
+                EntityTestUtils.assertAttributeEquals(entity, MockBrooklynNode.HA_PRIORITY, 0);
+
+                setManagementState(entity, ManagementNodeState.HOT_STANDBY);
+
+                return "MASTER";
+            } else {
+                switch(state) {
+                case INITIAL:
+                    checkRequest(input, HttpPost.METHOD_NAME, "/v1/server/ha/priority", "priority", "1");
+                    state = State.PROMOTED;
+                    setPriority(input.getEntity(), Integer.parseInt(input.getParams().get("priority")));
+                    return "0";
+                case PROMOTED:
+                    checkRequest(input, HttpPost.METHOD_NAME, "/v1/server/ha/priority", "priority", "0");
+                    state = State.INITIAL;
+                    setPriority(input.getEntity(), Integer.parseInt(input.getParams().get("priority")));
+                    return "1";
+                default: throw new IllegalStateException("Illegal call at state " + state + ". Request = " + input.getMethod() + " " + input.getPath());
+                }
+            }
+        }
+
+        public void checkRequest(Request input, String methodName, String path, String key, String value) {
+            if (!input.getMethod().equals(methodName) || !input.getPath().equals(path)) {
+                throw new IllegalStateException("Request doesn't match expected state. Expected = " + input.getMethod() + " " + input.getPath() + ". " +
+                        "Actual = " + methodName + " " + path);
+            }
+
+            String inputValue = input.getParams().get(key);
+            if(!Objects.equal(value, inputValue)) {
+                throw new IllegalStateException("Request doesn't match expected parameter " + methodName + " " + path + ". Parameter " + key + 
+                    " expected = " + value + ", actual = " + inputValue);
+            }
+        }
+
+        public void setFailAtStateChange(boolean failAtStateChange) {
+            this.failAtStateChange = failAtStateChange;
+        }
+
+    }
+
+    private void masterFailoverIfNeeded() {
+        if (!Entities.isManaged(cluster)) return;
+        if (cluster.getAttribute(BrooklynCluster.MASTER_NODE) == null) {
+            Collection<Entity> members = cluster.getMembers();
+            if (members.size() > 0) {
+                for (Entity member : members) {
+                    if (member.getAttribute(MockBrooklynNode.HA_PRIORITY) == 1) {
+                        masterFailover(member);
+                        return;
+                    }
+                }
+                masterFailover(members.iterator().next());
+            }
+        }
+    }
+
+    private void masterFailover(Entity member) {
+        LOG.debug("Master failover to " + member);
+        setManagementState(member, ManagementNodeState.MASTER);
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, BrooklynCluster.MASTER_NODE, (BrooklynNode)member);
+        return;
+    }
+
+    public static void setManagementState(Entity entity, ManagementNodeState state) {
+        ((EntityLocal)entity).setAttribute(BrooklynNode.MANAGEMENT_NODE_STATE, state);
+    }
+
+    public static void setPriority(Entity entity, int priority) {
+        ((EntityLocal)entity).setAttribute(MockBrooklynNode.HA_PRIORITY, priority);
+    }
+
+    private void selectMaster(DynamicCluster cluster, String id) {
+        app.getExecutionContext().submit(Effectors.invocation(cluster, BrooklynCluster.SELECT_MASTER, ImmutableMap.of(SelectMasterEffector.NEW_MASTER_ID.getName(), id))).asTask().getUnchecked();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java
new file mode 100644
index 0000000..6cc8e27
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.chef;
+
+import java.util.Set;
+
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefConfigs;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.factory.ApplicationBuilder;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+public class ChefConfigsTest {
+
+    private TestApplication app = null;
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() {
+        if (app!=null) Entities.destroyAll(app.getManagementContext());
+        app = null;
+    }
+    
+    @Test
+    public void testAddToRunList() {
+        app = ApplicationBuilder.newManagedApp(TestApplication.class);
+        ChefConfigs.addToLaunchRunList(app, "a", "b");
+        Set<? extends String> runs = app.getConfig(ChefConfig.CHEF_LAUNCH_RUN_LIST);
+        Assert.assertEquals(runs.size(), 2, "runs="+runs);
+        Assert.assertTrue(runs.contains("a"));
+        Assert.assertTrue(runs.contains("b"));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefLiveTestSupport.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefLiveTestSupport.java b/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefLiveTestSupport.java
new file mode 100644
index 0000000..5ea8315
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefLiveTestSupport.java
@@ -0,0 +1,99 @@
+/*
+ * 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.chef;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.core.EntityInternal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.io.FileUtil;
+import org.apache.brooklyn.util.stream.InputStreamSupplier;
+
+import com.google.common.base.Throwables;
+import com.google.common.io.Files;
+
+public class ChefLiveTestSupport extends BrooklynAppLiveTestSupport {
+
+    private static final Logger log = LoggerFactory.getLogger(ChefLiveTestSupport.class);
+    
+    protected MachineProvisioningLocation<? extends SshMachineLocation> targetLocation;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        
+        targetLocation = createLocation();
+    }
+
+    protected MachineProvisioningLocation<? extends SshMachineLocation> createLocation() {
+        return createLocation(mgmt);
+    }
+    
+    /** convenience for setting up a pre-built / fixed IP machine
+     * (because you might not want to set up Chef on localhost) 
+     * and ensuring tests against Chef use the same configured location 
+     **/
+    @SuppressWarnings("unchecked")
+    public static MachineProvisioningLocation<? extends SshMachineLocation> createLocation(ManagementContext mgmt) {
+        Location bestLocation = mgmt.getLocationRegistry().resolve("named:ChefTests", true, null).orNull();
+        if (bestLocation==null) {
+            log.info("using AWS for chef tests because named:ChefTests does not exist");
+            bestLocation = mgmt.getLocationRegistry().resolve("jclouds:aws-ec2:us-east-1");
+        }
+        if (bestLocation==null) {
+            throw new IllegalStateException("Need a location called named:ChefTests or AWS configured for these tests");
+        }
+        return (MachineProvisioningLocation<? extends SshMachineLocation>)bestLocation; 
+    }
+    
+    private static String defaultConfigFile = null; 
+    public synchronized static String installBrooklynChefHostedConfig() {
+        if (defaultConfigFile!=null) return defaultConfigFile;
+        File tempDir = Files.createTempDir();
+        ResourceUtils r = ResourceUtils.create(ChefServerTasksIntegrationTest.class);
+        try {
+            for (String f: new String[] { "knife.rb", "brooklyn-tests.pem", "brooklyn-validator.pem" }) {
+                String contents = r.getResourceAsString("classpath:///org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/"+f);
+                FileUtil.copyTo(InputStreamSupplier.fromString(contents).getInput(), new File(tempDir, f));
+            }
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+        File knifeConfig = new File(tempDir, "knife.rb");
+        defaultConfigFile = knifeConfig.getPath();
+        return defaultConfigFile;
+    }
+
+    public static void installBrooklynChefHostedConfig(Entity entity) {
+        ((EntityInternal)entity).setConfig(ChefConfig.KNIFE_CONFIG_FILE, ChefLiveTestSupport.installBrooklynChefHostedConfig());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java
new file mode 100644
index 0000000..79c7be0
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.chef;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefServerTasks;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.factory.ApplicationBuilder;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.stream.StreamGobbler;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** Many tests expect knife on the path, but none require any configuration beyond that.
+ * They will use the Brooklyn registered account (which has been set up with mysql cookbooks and more).
+ * <p>
+ * Note this is a free account so cannot manage many nodes. 
+ * You can use the credentials in src/test/resources/hosted-chef-brooklyn-credentials/
+ * to log in and configure the settings for our tests using knife. You can also log in at:
+ * <p>
+ * https://manage.opscode.com/
+ * <p>
+ * with credentials for those with need to know (which is a lot of people, but not everyone
+ * with access to this github repo!).
+ * <p>
+ * You can easily set up your own new account, for free; download the starter kit and
+ * point {@link ChefConfig#KNIFE_CONFIG_FILE} at the knife.rb.
+ * <p>
+ * Note that if you are porting an existing machine to be managed by a new chef account, you may need to do the following:
+ * <p>
+ * ON management machine:
+ * <li>knife client delete HOST   # or bulk delete, but don't delete your validator! it is a PITA recreating and adding back all the permissions! 
+ * <li>knife node delete HOST
+ * <p>
+ * ON machine being managed:
+ * <li>rm -rf /{etc,var}/chef
+ * <p>
+ * Note also that some tests require a location  named:ChefLive  to be set up in your brooklyn.properties.
+ * This can be a cloud (but will require frequent chef-node pruning) or a permanently set-up machine.
+ **/
+public class ChefServerTasksIntegrationTest {
+
+    private static final Logger log = LoggerFactory.getLogger(ChefServerTasksIntegrationTest.class);
+    
+    protected TestApplication app;
+    protected ManagementContext mgmt;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setup() throws Exception {
+        app = ApplicationBuilder.newManagedApp(TestApplication.class);
+        mgmt = app.getManagementContext();
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (mgmt != null) Entities.destroyAll(mgmt);
+        mgmt = null;
+    }
+
+    /** @deprecated use {@link ChefLiveTestSupport} */
+    @Deprecated
+    public synchronized static String installBrooklynChefHostedConfig() {
+        return ChefLiveTestSupport.installBrooklynChefHostedConfig();
+    }
+    
+    @Test(groups="Integration")
+    @SuppressWarnings("resource")
+    public void testWhichKnife() throws IOException, InterruptedException {
+        // requires that knife is installed on the path of login shells
+        Process p = Runtime.getRuntime().exec(new String[] { "bash", "-l", "-c", "which knife" });
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        new StreamGobbler(p.getInputStream(), out, log).start();
+        new StreamGobbler(p.getErrorStream(), out, log).start();
+        log.info("bash -l -c 'which knife' gives exit code: "+p.waitFor());
+        Time.sleep(Duration.millis(1000));
+        log.info("output:\n"+out);
+        Assert.assertEquals(p.exitValue(), 0);
+    }
+
+    @Test(groups="Integration")
+    public void testKnifeWithoutConfig() {
+        // without config it shouldn't pass
+        // (assumes that knife global config is *not* installed on your machine)
+        ProcessTaskWrapper<Boolean> t = Entities.submit(app, ChefServerTasks.isKnifeInstalled());
+        log.info("isKnifeInstalled without config returned: "+t.get()+" ("+t.getExitCode()+")\n"+t.getStdout()+"\nERR:\n"+t.getStderr());
+        Assert.assertFalse(t.get());
+    }
+
+    @Test(groups="Integration")
+    public void testKnifeWithConfig() {
+        // requires that knife is installed on the path of login shells
+        // (creates the config in a temp space)
+        ChefLiveTestSupport.installBrooklynChefHostedConfig(app);
+        ProcessTaskWrapper<Boolean> t = Entities.submit(app, ChefServerTasks.isKnifeInstalled());
+        log.info("isKnifeInstalled *with* config returned: "+t.get()+" ("+t.getExitCode()+")\n"+t.getStdout()+"\nERR:\n"+t.getStderr());
+        Assert.assertTrue(t.get());
+    }
+
+}