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:44 UTC

[26/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/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.java
new file mode 100644
index 0000000..8d8a396
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.java
@@ -0,0 +1,162 @@
+/*
+ * 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 com.google.common.base.Preconditions.checkNotNull;
+
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.http.HttpTool;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+
+public class EntityHttpClientImpl implements EntityHttpClient {
+    private static final Logger LOG = LoggerFactory.getLogger(EntityHttpClientImpl.class);
+
+    protected static interface HttpCall {
+        public HttpToolResponse call(HttpClient client, URI uri);
+    }
+
+    protected Entity entity;
+    protected AttributeSensor<?> urlSensor;
+    protected ConfigKey<?> urlConfig;
+    protected Predicate<Integer> responseSuccess = ResponseCodePredicates.success();
+
+    protected EntityHttpClientImpl(Entity entity, AttributeSensor<?> urlSensor) {
+        this.entity = entity;
+        this.urlSensor = urlSensor;
+    }
+
+    protected EntityHttpClientImpl(Entity entity, ConfigKey<?> urlConfig) {
+        this.entity = entity;
+        this.urlConfig = urlConfig;
+    }
+
+    @Override
+    public HttpTool.HttpClientBuilder getHttpClientForBrooklynNode() {
+        String baseUrl = getEntityUrl();
+        HttpTool.HttpClientBuilder builder = HttpTool.httpClientBuilder()
+                .trustAll()
+                .laxRedirect(true)
+                .uri(baseUrl);
+        if (entity.getConfig(BrooklynNode.MANAGEMENT_USER) != null) {
+            UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
+                    entity.getConfig(BrooklynNode.MANAGEMENT_USER),
+                    entity.getConfig(BrooklynNode.MANAGEMENT_PASSWORD));
+            builder.credentials(credentials);
+        }
+        return builder;
+    }
+
+    @Override
+    public EntityHttpClient responseSuccess(Predicate<Integer> responseSuccess) {
+        this.responseSuccess = checkNotNull(responseSuccess, "responseSuccess");
+        return this;
+    }
+
+    protected HttpToolResponse exec(String path, HttpCall httpCall) {
+        HttpClient client = Preconditions.checkNotNull(getHttpClientForBrooklynNode(), "No address info for "+entity)
+                .build();
+        String baseUri = getEntityUrl();
+        URI uri = URI.create(Urls.mergePaths(baseUri, path));
+
+        HttpToolResponse result;
+        try {
+            result = httpCall.call(client, uri);
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            throw new IllegalStateException("Invalid response invoking " + uri + ": " + e, e);
+        }
+        Tasks.addTagDynamically(BrooklynTaskTags.tagForStream("http_response", Streams.byteArray(result.getContent())));
+        if (!responseSuccess.apply(result.getResponseCode())) {
+            LOG.warn("Invalid response invoking {}: response code {}\n{}: {}",
+                    new Object[]{uri, result.getResponseCode(), result, new String(result.getContent())});
+            throw new IllegalStateException("Invalid response invoking " + uri + ": response code " + result.getResponseCode());
+        }
+        return result;
+    }
+
+    @Override
+    public HttpToolResponse get(String path) {
+        return exec(path, new HttpCall() {
+            @Override
+            public HttpToolResponse call(HttpClient client, URI uri) {
+                return HttpTool.httpGet(client, uri, MutableMap.<String, String>of());
+            }
+        });
+    }
+
+    @Override
+    public HttpToolResponse post(String path, final Map<String, String> headers, final byte[] body) {
+        return exec(path, new HttpCall() {
+            @Override
+            public HttpToolResponse call(HttpClient client, URI uri) {
+                return HttpTool.httpPost(client, uri, headers, body);
+            }
+        });
+    }
+
+    @Override
+    public HttpToolResponse post(String path, final Map<String, String> headers, final Map<String, String> formParams) {
+        return exec(path, new HttpCall() {
+            @Override
+            public HttpToolResponse call(HttpClient client, URI uri) {
+                return HttpTool.httpPost(client, uri, headers, formParams);
+            }
+        });
+    }
+
+    protected String getEntityUrl() {
+        Preconditions.checkState(urlSensor == null ^ urlConfig == null, "Exactly one of urlSensor and urlConfig should be non-null for entity " + entity);
+        Object url = null;
+        if (urlSensor != null) {
+            url = entity.getAttribute(urlSensor);
+        } else if (urlConfig != null) {
+            url = entity.getConfig(urlConfig);
+        }
+        Preconditions.checkNotNull(url, "URL sensor " + urlSensor + " for entity " + entity + " is empty");
+        return url.toString();
+    }
+
+    @Override
+    public HttpToolResponse delete(String path, final Map<String, String> headers) {
+        return exec(path, new HttpCall() {
+            @Override
+            public HttpToolResponse call(HttpClient client, URI uri) {
+                return HttpTool.httpDelete(client, uri, headers);
+            }
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java
new file mode 100644
index 0000000..38e8cfe
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.apache.brooklyn.api.entity.ImplementedBy;
+
+/**
+ * A {@link BrooklynNode} entity that represents the local Brooklyn service.
+ * <p>
+ * Management username and password can be specified in the {@code brooklyn.properties} file, as
+ * either specific username and password (useful when the credentials are set with SHA-256 hashes
+ * or via LDAP) or a username with separately configured webconsole plaintext password.
+ * <pre>
+ * brooklyn.entity.brooklynnode.local.user=admin
+ * brooklyn.entity.brooklynnode.local.password=password
+ * brooklyn.webconsole.security.user.admin.password=password
+ * </pre>
+ */
+@ImplementedBy(LocalBrooklynNodeImpl.class)
+public interface LocalBrooklynNode extends BrooklynNode {
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java
new file mode 100644
index 0000000..9f85ebe
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.util.text.Strings;
+
+public class LocalBrooklynNodeImpl extends BrooklynNodeImpl implements LocalBrooklynNode {
+
+    private static final String LOCAL_BROOKLYN_NODE_KEY = "brooklyn.entity.brooklynnode.local.%s";
+    private static final String BROOKLYN_WEBCONSOLE_PASSWORD_KEY = "brooklyn.webconsole.security.user.%s.password";
+
+    @Override
+    protected void connectSensors() {
+        // Override management username and password from brooklyn.properties
+        BrooklynProperties properties = (BrooklynProperties) getManagementContext().getConfig();
+        String user = (String) properties.get(String.format(LOCAL_BROOKLYN_NODE_KEY, "user"));
+        String password = (String) properties.get(String.format(LOCAL_BROOKLYN_NODE_KEY, "password"));
+        if (Strings.isBlank(password)) {
+            if (Strings.isBlank(user)) user = "admin";
+            password = (String) properties.get(String.format(BROOKLYN_WEBCONSOLE_PASSWORD_KEY, user));
+        }
+        if (Strings.isNonBlank(user) && Strings.isNonBlank(password)) {
+            setConfig(MANAGEMENT_USER, user);
+            setConfig(MANAGEMENT_PASSWORD, password);
+        }
+        super.connectSensors();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java
new file mode 100644
index 0000000..a031930
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java
@@ -0,0 +1,84 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.effector.core.Effectors.EffectorBuilder;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynEntityMirrorImpl.RemoteEffector;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+
+import com.google.common.base.Function;
+
+public class RemoteEffectorBuilder {
+    private static class ResultParser implements Function<HttpToolResponse, String> {
+        @Override
+        public String apply(HttpToolResponse input) {
+            return input.getContentAsString();
+        }
+    }
+    
+
+    public static Collection<Effector<String>> of(Collection<?> cfgEffectors) {
+        Collection<Effector<String>> effectors = new ArrayList<Effector<String>>();
+        for (Object objEff : cfgEffectors) {
+            Map<?, ?> cfgEff = (Map<?, ?>)objEff;
+            String effName = (String)cfgEff.get("name");
+            String description = (String)cfgEff.get("description");
+
+            EffectorBuilder<String> eff = Effectors.effector(String.class, effName);
+            Collection<?> params = (Collection<?>)cfgEff.get("parameters");
+
+            /* The *return type* should NOT be included in the signature here.
+             * It might be a type known only at the mirrored brooklyn node
+             * (in which case loading it here would fail); or possibly it could
+             * be a different version of the type here, in which case the signature
+             * would look valid here, but deserializing it would fail.
+             * 
+             * Best to just pass the json representation back to the caller.
+             * (They won't be able to tell the difference between that and deserialize-then-serialize!)
+             */
+            
+            if (description != null) {
+                eff.description(description);
+            }
+
+            for (Object objParam : params) {
+                buildParam(eff, (Map<?, ?>)objParam);
+            }
+
+            eff.impl(new RemoteEffector<String>(effName, new ResultParser()));
+            effectors.add(eff.build());
+        }
+        return effectors;
+    }
+
+    private static void buildParam(EffectorBuilder<String> eff, Map<?, ?> cfgParam) {
+        String name = (String)cfgParam.get("name");
+        String description = (String)cfgParam.get("description");
+        String defaultValue = (String)cfgParam.get("defaultValue");
+
+        eff.parameter(Object.class, name, description, defaultValue /*TypeCoercions.coerce(defaultValue, paramType)*/);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java
new file mode 100644
index 0000000..c6ec7b4
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java
@@ -0,0 +1,206 @@
+/*
+ * 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.effector;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+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.entity.Group;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.SelectMasterEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.UpgradeClusterEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityModeEffector;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.EntityPredicates;
+import org.apache.brooklyn.entity.core.EntityTasks;
+import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+
+public class BrooklynClusterUpgradeEffectorBody extends EffectorBody<Void> implements UpgradeClusterEffector {
+    
+    private static final Logger log = LoggerFactory.getLogger(BrooklynClusterUpgradeEffectorBody.class);
+    
+    public static final Effector<Void> UPGRADE_CLUSTER = Effectors.effector(UpgradeClusterEffector.UPGRADE_CLUSTER)
+        .impl(new BrooklynClusterUpgradeEffectorBody()).build();
+
+    private final AtomicBoolean upgradeInProgress = new AtomicBoolean();
+
+    @Override
+    public Void call(ConfigBag parameters) {
+        if (!upgradeInProgress.compareAndSet(false, true)) {
+            throw new IllegalStateException("An upgrade is already in progress.");
+        }
+
+        EntitySpec<?> origMemberSpec = entity().getConfig(BrooklynCluster.MEMBER_SPEC);
+        Preconditions.checkNotNull(origMemberSpec, BrooklynCluster.MEMBER_SPEC.getName() + " is required for " + UpgradeClusterEffector.class.getName());
+
+        log.debug("Upgrading "+entity()+", changing "+BrooklynCluster.MEMBER_SPEC+" from "+origMemberSpec+" / "+origMemberSpec.getConfig());
+
+        boolean success = false;
+        try {
+            String newDownloadUrl = parameters.get(DOWNLOAD_URL);
+            
+            EntitySpec<?> newMemberSpec = EntitySpec.create(origMemberSpec);
+            
+            ConfigBag newConfig = ConfigBag.newInstance();
+            newConfig.putIfNotNull(DOWNLOAD_URL, newDownloadUrl);
+            newConfig.put(BrooklynNode.DISTRO_UPLOAD_URL, inferUploadUrl(newDownloadUrl));
+            newConfig.putAll(ConfigBag.newInstance(parameters.get(EXTRA_CONFIG)).getAllConfigAsConfigKeyMap());
+            newMemberSpec.configure(newConfig.getAllConfigAsConfigKeyMap());
+            
+            entity().setConfig(BrooklynCluster.MEMBER_SPEC, newMemberSpec);
+            
+            log.debug("Upgrading "+entity()+", new "+BrooklynCluster.MEMBER_SPEC+": "+newMemberSpec+" / "+newMemberSpec.getConfig()+" (adding: "+newConfig+")");
+            
+            upgrade(parameters);
+
+            success = true;
+        } finally {
+            if (!success) {
+                log.debug("Upgrading "+entity()+" failed, will rethrow after restoring "+BrooklynCluster.MEMBER_SPEC+" to: "+origMemberSpec);
+                entity().setConfig(BrooklynCluster.MEMBER_SPEC, origMemberSpec);
+            }
+            
+            upgradeInProgress.set(false);
+        }
+        return null;
+    }
+
+    private String inferUploadUrl(String newDownloadUrl) {
+        if (newDownloadUrl==null) return null;
+        boolean isLocal = "file".equals(Urls.getProtocol(newDownloadUrl)) || new File(newDownloadUrl).exists();
+        if (isLocal) {
+            return newDownloadUrl;
+        } else {
+            return null;
+        }
+    }
+
+    protected void upgrade(ConfigBag parameters) {
+        //TODO currently this will fight with auto-scaler policies; they must be turned off for upgrade to work
+
+        Group cluster = (Group)entity();
+        Collection<Entity> initialMembers = cluster.getMembers();
+        int initialClusterSize = initialMembers.size();
+        
+        if (!BrooklynNodeUpgradeEffectorBody.isPersistenceModeEnabled(cluster)) {
+            // would could try a `forcePersistNow`, but that's sloppy; 
+            // for now, require HA/persistence for upgrading 
+            DynamicTasks.queue( Tasks.warning("Check persistence", 
+                new IllegalStateException("Persistence does not appear to be enabled at this cluster. "
+                + "Cluster upgrade will not succeed unless a custom launch script enables it.")) );
+        }
+       
+        //TODO we'd like to disable these nodes as standby targets, ie in some 'hot standby but not available for failover' mode
+        //currently if failover happens to a new node, assumptions below may fail and the cluster may require manual repair
+
+        //1. Initially create a single node to check if it will launch successfully
+        TaskAdaptable<Collection<Entity>> initialNodeTask = DynamicTasks.queue(newCreateNodesTask(1, "Creating first upgraded version node"));
+
+        //2. If everything is OK with the first node launch the rest as well
+        @SuppressWarnings("unused")
+        TaskAdaptable<Collection<Entity>> remainingNodesTask = DynamicTasks.queue(newCreateNodesTask(initialClusterSize - 1, "Creating remaining upgraded version nodes ("+(initialClusterSize - 1)+")"));
+
+        //3. Once we have all nodes running without errors switch master
+        DynamicTasks.queue(Effectors.invocation(cluster, BrooklynCluster.SELECT_MASTER, MutableMap.of(SelectMasterEffector.NEW_MASTER_ID, 
+            Iterables.getOnlyElement(initialNodeTask.asTask().getUnchecked()).getId()))).asTask().getUnchecked();
+
+        //4. Stop the nodes which were running at the start of the upgrade call, but keep them around.
+        //   Should we create a quarantine-like zone for old stopped version?
+        //   For members that were created meanwhile - they will be using the new version already. If the new version
+        //   isn't good then they will fail to start as well, forcing the policies to retry (and succeed once the
+        //   URL is reverted).
+        
+        //any other nodes created via other means should also be using the new spec, so initialMembers will be all the old version nodes
+        DynamicTasks.queue(Effectors.invocation(BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, Collections.emptyMap(), initialMembers)).asTask().getUnchecked();
+    }
+
+    private TaskAdaptable<Collection<Entity>> newCreateNodesTask(int size, String name) {
+        return Tasks.<Collection<Entity>>builder().name(name).body(new CreateNodesCallable(size)).build();
+    }
+
+    protected class CreateNodesCallable implements Callable<Collection<Entity>> {
+        private final int size;
+        public CreateNodesCallable(int size) {
+            this.size = size;
+        }
+        @Override
+        public Collection<Entity> call() throws Exception {
+            return createNodes(size);
+        }
+    }
+
+    protected Collection<Entity> createNodes(int nodeCnt) {
+        DynamicCluster cluster = (DynamicCluster)entity();
+
+        //1. Create the nodes
+        Collection<Entity> newNodes = cluster.resizeByDelta(nodeCnt);
+
+        //2. Wait for them to be RUNNING (or at least STARTING to have completed)
+        // (should already be the case, because above is synchronous and, we think, it will fail if start does not succeed)
+        DynamicTasks.queue(EntityTasks.requiringAttributeEventually(newNodes, Attributes.SERVICE_STATE_ACTUAL, 
+                Predicates.not(Predicates.equalTo(Lifecycle.STARTING)), Duration.minutes(30)));
+
+        //3. Set HOT_STANDBY in case it is not enabled on the command line ...
+        // TODO support via EntitySpec
+        DynamicTasks.queue(Effectors.invocation(
+                BrooklynNode.SET_HIGH_AVAILABILITY_MODE,
+                MutableMap.of(SetHighAvailabilityModeEffector.MODE, HighAvailabilityMode.HOT_STANDBY), 
+                newNodes)).asTask().getUnchecked();
+        //... and wait until all of the nodes change state
+        // TODO fail quicker if state changes to FAILED
+        DynamicTasks.queue(EntityTasks.requiringAttributeEventually(newNodes, BrooklynNode.MANAGEMENT_NODE_STATE, 
+                Predicates.equalTo(ManagementNodeState.HOT_STANDBY), Duration.FIVE_MINUTES));
+
+        // TODO also check that the nodes created all report the original master, in case persistence changes it
+        
+        //5. Just in case check if all of the nodes are SERVICE_UP (which would rule out ON_FIRE as well)
+        Collection<Entity> failedNodes = Collections2.filter(newNodes, EntityPredicates.attributeEqualTo(BrooklynNode.SERVICE_UP, Boolean.FALSE));
+        if (!failedNodes.isEmpty()) {
+            throw new IllegalStateException("Nodes " + failedNodes + " are not " + BrooklynNode.SERVICE_UP + " though successfully in " + ManagementNodeState.HOT_STANDBY);
+        }
+        return newNodes;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java
new file mode 100644
index 0000000..baf4ae9
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java
@@ -0,0 +1,229 @@
+/*
+ * 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.effector;
+
+import java.util.Map;
+
+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.entity.drivers.DriverDependentEntity;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeDriver;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.core.EntityInternal;
+import org.apache.brooklyn.entity.core.EntityTasks;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
+import org.apache.brooklyn.sensor.ssh.SshEffectorTasks;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.reflect.TypeToken;
+
+@SuppressWarnings("serial")
+/** Upgrades a brooklyn node in-place on the box, 
+ * by creating a child brooklyn node and ensuring it can rebind in HOT_STANDBY
+ * <p>
+ * Requires the target node to have persistence enabled. 
+ */
+public class BrooklynNodeUpgradeEffectorBody extends EffectorBody<Void> {
+
+    private static final Logger log = LoggerFactory.getLogger(BrooklynNodeUpgradeEffectorBody.class);
+    
+    public static final ConfigKey<String> DOWNLOAD_URL = BrooklynNode.DOWNLOAD_URL.getConfigKey();
+    public static final ConfigKey<Boolean> DO_DRY_RUN_FIRST = ConfigKeys.newBooleanConfigKey(
+            "doDryRunFirst", "Test rebinding with a temporary instance before stopping the entity for upgrade.", true);
+    public static final ConfigKey<Map<String,Object>> EXTRA_CONFIG = MapConfigKey.builder(new TypeToken<Map<String,Object>>() {})
+        .name("extraConfig")
+        .description("Additional new config to set on the BrooklynNode as part of upgrading")
+        .build();
+
+    public static final Effector<Void> UPGRADE = Effectors.effector(Void.class, "upgrade")
+        .description("Changes the Brooklyn build used to run this node, "
+            + "by spawning a dry-run node then copying the installed files across. "
+            + "This node must be running for persistence for in-place upgrading to work.")
+        .parameter(BrooklynNode.SUGGESTED_VERSION)
+        .parameter(DOWNLOAD_URL)
+        .parameter(DO_DRY_RUN_FIRST)
+        .parameter(EXTRA_CONFIG)
+        .impl(new BrooklynNodeUpgradeEffectorBody()).build();
+    
+    @Override
+    public Void call(ConfigBag parametersO) {
+        if (!isPersistenceModeEnabled(entity())) {
+            // would could try a `forcePersistNow`, but that's sloppy; 
+            // for now, require HA/persistence for upgrading
+            DynamicTasks.queue( Tasks.warning("Check persistence", 
+                new IllegalStateException("Persistence does not appear to be enabled at this cluster. "
+                + "In-place node upgrade will not succeed unless a custom launch script enables it.")) );
+        }
+
+        final ConfigBag parameters = ConfigBag.newInstanceCopying(parametersO);
+
+        /*
+         * all parameters are passed to children, apart from EXTRA_CONFIG
+         * whose value (as a map) is so passed; it provides an easy way to set extra config in the gui.
+         * (IOW a key-value mapping can be passed either inside EXTRA_CONFIG or as a sibling to EXTRA_CONFIG)  
+         */
+        if (parameters.containsKey(EXTRA_CONFIG)) {
+            Map<String, Object> extra = parameters.get(EXTRA_CONFIG);
+            parameters.remove(EXTRA_CONFIG);
+            parameters.putAll(extra);
+        }
+        log.debug(this+" upgrading, using "+parameters);
+        
+        final String bkName;
+        boolean doDryRunFirst = parameters.get(DO_DRY_RUN_FIRST);
+        if(doDryRunFirst) {
+            bkName = dryRunUpdate(parameters);
+        } else {
+            bkName = "direct-"+Identifiers.makeRandomId(4);
+        }
+        
+        // Stop running instance
+        DynamicTasks.queue(Tasks.builder().name("shutdown node")
+                .add(Effectors.invocation(entity(), BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, ImmutableMap.of(StopSoftwareParameters.STOP_MACHINE_MODE, StopMode.NEVER)))
+                .build());
+
+        // backup old files
+        DynamicTasks.queue(Tasks.builder().name("backup old version").body(new Runnable() {
+            @Override
+            public void run() {
+                String runDir = entity().getAttribute(SoftwareProcess.RUN_DIR);
+                String bkDir = Urls.mergePaths(runDir, "..", Urls.getBasename(runDir)+"-backups", bkName);
+                log.debug(this+" storing backup of previous version in "+bkDir);
+                DynamicTasks.queue(SshEffectorTasks.ssh(
+                    "cd "+runDir,
+                    "mkdir -p "+bkDir,
+                    "mv * "+bkDir
+                    // By removing the run dir of the entity we force it to go through
+                    // the customize step again on start and re-generate local-brooklyn.properties.
+                    ).summary("move files"));
+            }
+        }).build());
+        
+        // Reconfigure entity
+        DynamicTasks.queue(Tasks.builder().name("reconfigure").body(new Runnable() {
+            @Override
+            public void run() {
+                DynamicTasks.waitForLast();
+                ((EntityInternal)entity()).setAttribute(SoftwareProcess.INSTALL_DIR, (String)null);
+                entity().setConfig(SoftwareProcess.INSTALL_UNIQUE_LABEL, (String)null);
+                entity().getConfigMap().addToLocalBag(parameters.getAllConfig());
+                entity().setAttribute(BrooklynNode.DOWNLOAD_URL, entity().getConfig(DOWNLOAD_URL));
+
+                // Setting SUGGESTED_VERSION will result in an new empty INSTALL_FOLDER, but clear it
+                // just in case the user specified already installed version.
+                ((BrooklynNodeDriver)((DriverDependentEntity<?>)entity()).getDriver()).clearInstallDir();
+            }
+        }).build());
+        
+        // Start this entity, running the new version.
+        // This will download and install the new dist (if not already done by the dry run node).
+        DynamicTasks.queue(Effectors.invocation(entity(), BrooklynNode.START, ConfigBag.EMPTY));
+
+        return null;
+    }
+
+    private String dryRunUpdate(ConfigBag parameters) {
+        // TODO require entity() node state master or hot standby AND require persistence enabled, or a new 'force_attempt_upgrade' parameter to be applied
+        // TODO could have a 'skip_dry_run_upgrade' parameter
+        // TODO could support 'dry_run_only' parameter, with optional resumption tasks (eg new dynamic effector)
+
+        // 1 add new brooklyn version entity as child (so uses same machine), with same config apart from things in parameters
+        final Entity dryRunChild = entity().addChild(createDryRunSpec()
+            .displayName("Upgraded Version Dry-Run Node")
+            // install dir and label are recomputed because they are not inherited, and download_url will normally be different
+            .configure(parameters.getAllConfig()));
+
+        //force this to start as hot-standby
+        // TODO alternatively could use REST API as in BrooklynClusterUpgradeEffectorBody
+        String launchParameters = dryRunChild.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
+        if (Strings.isBlank(launchParameters)) launchParameters = "";
+        else launchParameters += " ";
+        launchParameters += "--highAvailability "+HighAvailabilityMode.HOT_STANDBY;
+        ((EntityInternal)dryRunChild).setConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS, launchParameters);
+
+        Entities.manage(dryRunChild);
+        final String dryRunNodeUid = dryRunChild.getId();
+        ((EntityInternal)dryRunChild).setDisplayName("Dry-Run Upgraded Brooklyn Node ("+dryRunNodeUid+")");
+
+        DynamicTasks.queue(Effectors.invocation(dryRunChild, BrooklynNode.START, ConfigBag.EMPTY));
+
+        // 2 confirm hot standby status
+        DynamicTasks.queue(EntityTasks.requiringAttributeEventually(dryRunChild, BrooklynNode.MANAGEMENT_NODE_STATE, 
+            Predicates.equalTo(ManagementNodeState.HOT_STANDBY), Duration.FIVE_MINUTES));
+
+        // 3 stop new version
+        DynamicTasks.queue(Tasks.builder().name("shutdown transient node")
+            .add(Effectors.invocation(dryRunChild, BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, ImmutableMap.of(StopSoftwareParameters.STOP_MACHINE_MODE, StopMode.NEVER)))
+            .build());
+
+        DynamicTasks.queue(Tasks.<Void>builder().name("remove transient node").body(
+            new Runnable() {
+                @Override
+                public void run() {
+                    Entities.unmanage(dryRunChild);
+                }
+            }
+        ).build());
+
+        return dryRunChild.getId();
+    }
+
+    protected EntitySpec<? extends BrooklynNode> createDryRunSpec() {
+        return EntitySpec.create(BrooklynNode.class);
+    }
+
+    @Beta
+    static boolean isPersistenceModeEnabled(Entity entity) {
+        // TODO when there are PERSIST* options in BrooklynNode, look at them here!
+        // or, even better, make a REST call to check persistence
+        String params = null;
+        if (entity instanceof BrooklynCluster) {
+            EntitySpec<?> spec = entity.getConfig(BrooklynCluster.MEMBER_SPEC);
+            params = Strings.toString( spec.getConfig().get(BrooklynNode.EXTRA_LAUNCH_PARAMETERS) );
+        }
+        if (params==null) params = entity.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
+        if (params==null) return false;
+        if (params.indexOf("persist")==0) return false;
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java
new file mode 100644
index 0000000..f9bc79f
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java
@@ -0,0 +1,174 @@
+/*
+ * 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.effector;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.SelectMasterEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityModeEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityPriorityEffector;
+import org.apache.brooklyn.entity.core.EntityPredicates;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.repeat.Repeater;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterables;
+
+public class SelectMasterEffectorBody extends EffectorBody<Void> implements SelectMasterEffector {
+    public static final Effector<Void> SELECT_MASTER = Effectors.effector(SelectMasterEffector.SELECT_MASTER).impl(new SelectMasterEffectorBody()).build();
+    
+    private static final Logger LOG = LoggerFactory.getLogger(SelectMasterEffectorBody.class);
+
+    private static final int HA_STANDBY_PRIORITY = 0;
+    private static final int HA_MASTER_PRIORITY = 1;
+
+    private AtomicBoolean selectMasterInProgress = new AtomicBoolean();
+
+    @Override
+    public Void call(ConfigBag parameters) {
+        if (!selectMasterInProgress.compareAndSet(false, true)) {
+            throw new IllegalStateException("A master change is already in progress.");
+        }
+
+        try {
+            selectMaster(parameters);
+        } finally {
+            selectMasterInProgress.set(false);
+        }
+        return null;
+    }
+
+    private void selectMaster(ConfigBag parameters) {
+        String newMasterId = parameters.get(NEW_MASTER_ID);
+        Preconditions.checkNotNull(newMasterId, NEW_MASTER_ID.getName() + " parameter is required");
+
+        final Entity oldMaster = entity().getAttribute(BrooklynCluster.MASTER_NODE);
+        if (oldMaster != null && oldMaster.getId().equals(newMasterId)) {
+            LOG.info(newMasterId + " is already the current master, no change needed.");
+            return;
+        }
+
+        final Entity newMaster = getMember(newMasterId);
+
+        //1. Increase the priority of the node we wish to become master
+        toggleNodePriority(newMaster, HA_MASTER_PRIORITY);
+
+        //2. Demote the existing master so a new election takes place
+        try {
+            // this allows the finally block to run even on failure
+            DynamicTasks.swallowChildrenFailures();
+            
+            if (oldMaster != null) {
+                demoteOldMaster(oldMaster, HighAvailabilityMode.HOT_STANDBY);
+            }
+
+            waitMasterHandover(oldMaster, newMaster);
+        } finally { 
+            //3. Revert the priority of the node once it has become master
+            toggleNodePriority(newMaster, HA_STANDBY_PRIORITY);
+        }
+
+        checkMasterSelected(newMaster);
+    }
+
+    private void waitMasterHandover(final Entity oldMaster, final Entity newMaster) {
+        boolean masterChanged = Repeater.create()
+            .backoff(Repeater.DEFAULT_REAL_QUICK_PERIOD, 1.5, Duration.FIVE_SECONDS)
+            .limitTimeTo(Duration.ONE_MINUTE)
+            .until(new Callable<Boolean>() {
+                @Override
+                public Boolean call() throws Exception {
+                    Entity master = getMasterNode();
+                    return !Objects.equal(master, oldMaster) && master != null;
+                }
+            })
+            .run();
+        if (!masterChanged) {
+            LOG.warn("Timeout waiting for node to become master: " + newMaster + ".");
+        }
+    }
+
+    private void demoteOldMaster(Entity oldMaster, HighAvailabilityMode mode) {
+        ManagementNodeState oldState = DynamicTasks.queue(
+                Effectors.invocation(
+                        oldMaster,
+                        BrooklynNode.SET_HIGH_AVAILABILITY_MODE,
+                        MutableMap.of(SetHighAvailabilityModeEffector.MODE, mode))
+            ).asTask().getUnchecked();
+
+        if (oldState != ManagementNodeState.MASTER) {
+            LOG.warn("The previous HA state on node " + oldMaster.getId() + " was " + oldState +
+                    ", while the expected value is " + ManagementNodeState.MASTER + ".");
+        }
+    }
+
+    private void toggleNodePriority(Entity node, int newPriority) {
+        Integer oldPriority = DynamicTasks.queue(
+                Effectors.invocation(
+                    node,
+                    BrooklynNode.SET_HIGH_AVAILABILITY_PRIORITY,
+                    MutableMap.of(SetHighAvailabilityPriorityEffector.PRIORITY, newPriority))
+            ).asTask().getUnchecked();
+
+        Integer expectedPriority = (newPriority == HA_MASTER_PRIORITY ? HA_STANDBY_PRIORITY : HA_MASTER_PRIORITY);
+        if (oldPriority != expectedPriority) {
+            LOG.warn("The previous HA priority on node " + node.getId() + " was " + oldPriority +
+                    ", while the expected value is " + expectedPriority + " (while setting priority " +
+                    newPriority + ").");
+        }
+    }
+
+    private void checkMasterSelected(Entity newMaster) {
+        Entity actualMaster = getMasterNode();
+        if (actualMaster != newMaster) {
+            throw new IllegalStateException("Expected node " + newMaster + " to be master, but found that " +
+                    "master is " + actualMaster + " instead.");
+        }
+    }
+
+    private Entity getMember(String memberId) {
+        Group cluster = (Group)entity();
+        try {
+            return Iterables.find(cluster.getMembers(), EntityPredicates.idEqualTo(memberId));
+        } catch (NoSuchElementException e) {
+            throw new IllegalStateException(memberId + " is not an ID of brooklyn node in this cluster");
+        }
+    }
+
+    private Entity getMasterNode() {
+        return entity().getAttribute(BrooklynCluster.MASTER_NODE);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java
new file mode 100644
index 0000000..96d3e49
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java
@@ -0,0 +1,63 @@
+/*
+ * 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.effector;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityModeEffector;
+import org.apache.brooklyn.sensor.feed.http.HttpValueFunctions;
+import org.apache.brooklyn.sensor.feed.http.JsonFunctions;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.brooklyn.util.guava.Functionals;
+import org.apache.brooklyn.util.javalang.Enums;
+import org.apache.http.HttpStatus;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+
+public class SetHighAvailabilityModeEffectorBody extends EffectorBody<ManagementNodeState> implements SetHighAvailabilityModeEffector {
+    public static final Effector<ManagementNodeState> SET_HIGH_AVAILABILITY_MODE = Effectors.effector(SetHighAvailabilityModeEffector.SET_HIGH_AVAILABILITY_MODE).impl(new SetHighAvailabilityModeEffectorBody()).build();
+
+    @Override
+    public ManagementNodeState call(ConfigBag parameters) {
+        HighAvailabilityMode mode = parameters.get(MODE);
+        Preconditions.checkNotNull(mode, MODE.getName() + " parameter is required");
+
+        EntityHttpClient httpClient = ((BrooklynNode)entity()).http();
+        HttpToolResponse resp = httpClient.post("/v1/server/ha/state", 
+                ImmutableMap.of("Brooklyn-Allow-Non-Master-Access", "true"),
+                ImmutableMap.of("mode", mode.toString()));
+
+        if (resp.getResponseCode() == HttpStatus.SC_OK) {
+            Function<HttpToolResponse, ManagementNodeState> parseRespone = Functionals.chain(
+                    Functionals.chain(HttpValueFunctions.jsonContents(), JsonFunctions.cast(String.class)),
+                    Enums.fromStringFunction(ManagementNodeState.class));
+            return parseRespone.apply(resp);
+        } else {
+            throw new IllegalStateException("Unexpected response code: " + resp.getResponseCode() + "\n" + resp.getContentAsString());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java
new file mode 100644
index 0000000..4fd1aa4
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java
@@ -0,0 +1,54 @@
+/*
+ * 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.effector;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityPriorityEffector;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.http.HttpStatus;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+public class SetHighAvailabilityPriorityEffectorBody extends EffectorBody<Integer> implements SetHighAvailabilityPriorityEffector {
+    public static final Effector<Integer> SET_HIGH_AVAILABILITY_PRIORITY = Effectors.effector(SetHighAvailabilityPriorityEffector.SET_HIGH_AVAILABILITY_PRIORITY).impl(new SetHighAvailabilityPriorityEffectorBody()).build();
+
+    @Override
+    public Integer call(ConfigBag parameters) {
+        Integer priority = parameters.get(PRIORITY);
+        Preconditions.checkNotNull(priority, PRIORITY.getName() + " parameter is required");
+
+        EntityHttpClient httpClient = ((BrooklynNode)entity()).http();
+        HttpToolResponse resp = httpClient.post("/v1/server/ha/priority",
+            ImmutableMap.of("Brooklyn-Allow-Non-Master-Access", "true"),
+            ImmutableMap.of("priority", priority.toString()));
+
+        if (resp.getResponseCode() == HttpStatus.SC_OK) {
+            return Integer.valueOf(resp.getContentAsString());
+        } else {
+            throw new IllegalStateException("Unexpected response code: " + resp.getResponseCode() + "\n" + resp.getContentAsString());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
new file mode 100644
index 0000000..6dcd9a9
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
@@ -0,0 +1,410 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.mgmt.ExecutionContext;
+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.core.EntityInternal;
+import org.apache.brooklyn.sensor.feed.AbstractFeed;
+import org.apache.brooklyn.sensor.feed.PollHandler;
+import org.apache.brooklyn.sensor.feed.Poller;
+import org.apache.brooklyn.sensor.feed.ssh.SshPollValue;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * A sensor feed that retrieves attributes from Chef server and converts selected attributes to sensors.
+ *
+ * <p>To use this feed, you must provide the entity, the name of the node as it is known to Chef, and a collection of attribute
+ * sensors. The attribute sensors must follow the naming convention of starting with the string <tt>chef.attribute.</tt>
+ * followed by a period-separated path through the Chef attribute hierarchy. For example, an attribute sensor named
+ * <tt>chef.attribute.sql_server.instance_name</tt> would cause the feed to search for a Chef attribute called
+ * <tt>sql_server</tt>, and within that an attribute <tt>instance_name</tt>, and set the sensor to the value of this
+ * attribute.</p>
+ *
+ * <p>This feed uses the <tt>knife</tt> tool to query all the attributes on a named node. It then iterates over the configured
+ * list of attribute sensors, using the sensor name to locate an equivalent Chef attribute. The sensor is then set to the value
+ * of the Chef attribute.</p>
+ *
+ * <p>Example:</p>
+ *
+ * {@code
+ * @Override
+ * protected void connectSensors() {
+ *     nodeAttributesFeed = ChefAttributeFeed.newFeed(this, nodeName, new AttributeSensor[]{
+ *             SqlServerNode.CHEF_ATTRIBUTE_NODE_NAME,
+ *             SqlServerNode.CHEF_ATTRIBUTE_SQL_SERVER_INSTANCE_NAME,
+ *             SqlServerNode.CHEF_ATTRIBUTE_SQL_SERVER_PORT,
+ *             SqlServerNode.CHEF_ATTRIBUTE_SQL_SERVER_SA_PASSWORD
+ *     });
+ * }
+ * }
+ *
+ * @since 0.6.0
+ * @author richardcloudsoft
+ */
+public class ChefAttributeFeed extends AbstractFeed {
+
+    private static final Logger log = LoggerFactory.getLogger(ChefAttributeFeed.class);
+
+    /**
+     * Prefix for attribute sensor names.
+     */
+    public static final String CHEF_ATTRIBUTE_PREFIX = "chef.attribute.";
+
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Set<ChefAttributePollConfig<?>>> POLLS = ConfigKeys.newConfigKey(
+            new TypeToken<Set<ChefAttributePollConfig<?>>>() {},
+            "polls");
+
+    public static final ConfigKey<String> NODE_NAME = ConfigKeys.newStringConfigKey("nodeName");
+    
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @SuppressWarnings("rawtypes")
+    public static class Builder {
+        private EntityLocal entity;
+        private boolean onlyIfServiceUp = false;
+        private String nodeName;
+        private Set<ChefAttributePollConfig> polls = Sets.newLinkedHashSet();
+        private Duration period = Duration.of(30, TimeUnit.SECONDS);
+        private String uniqueTag;
+        private volatile boolean built;
+
+        public Builder entity(EntityLocal val) {
+            this.entity = checkNotNull(val, "entity");
+            return this;
+        }
+        public Builder onlyIfServiceUp() { return onlyIfServiceUp(true); }
+        public Builder onlyIfServiceUp(boolean onlyIfServiceUp) { 
+            this.onlyIfServiceUp = onlyIfServiceUp; 
+            return this; 
+        }
+        public Builder nodeName(String nodeName) {
+            this.nodeName = checkNotNull(nodeName, "nodeName");
+            return this;
+        }
+        public Builder addSensor(ChefAttributePollConfig config) {
+            polls.add(config);
+            return this;
+        }
+        @SuppressWarnings("unchecked")
+        public Builder addSensor(String chefAttributePath, AttributeSensor sensor) {
+            return addSensor(new ChefAttributePollConfig(sensor).chefAttributePath(chefAttributePath));
+        }
+        public Builder addSensors(Map<String, AttributeSensor> sensors) {
+            for (Map.Entry<String, AttributeSensor> entry : sensors.entrySet()) {
+                addSensor(entry.getKey(), entry.getValue());
+            }
+            return this;
+        }
+        public Builder addSensors(AttributeSensor[] sensors) {
+            return addSensors(Arrays.asList(checkNotNull(sensors, "sensors")));
+        }
+        public Builder addSensors(Iterable<AttributeSensor> sensors) {
+            for(AttributeSensor sensor : checkNotNull(sensors, "sensors")) {
+                checkNotNull(sensor, "sensors collection contains a null value");
+                checkArgument(sensor.getName().startsWith(CHEF_ATTRIBUTE_PREFIX), "sensor name must be prefixed "+CHEF_ATTRIBUTE_PREFIX+" for autodetection to work");
+                addSensor(sensor.getName().substring(CHEF_ATTRIBUTE_PREFIX.length()), sensor);
+            }
+            return this;
+        }
+        public Builder period(Duration period) {
+            this.period = period;
+            return this;
+        }
+        public Builder period(long millis) {
+            return period(Duration.of(millis, TimeUnit.MILLISECONDS));
+        }
+        public Builder period(long val, TimeUnit units) {
+            return period(Duration.of(val, units));
+        }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
+        public ChefAttributeFeed build() {
+            built = true;
+            ChefAttributeFeed result = new ChefAttributeFeed(this);
+            result.setEntity(checkNotNull(entity, "entity"));
+            result.start();
+            return result;
+        }
+        @Override
+        protected void finalize() {
+            if (!built) log.warn("SshFeed.Builder created, but build() never called");
+        }
+    }
+
+    private KnifeTaskFactory<String> knifeTaskFactory;
+
+    /**
+     * For rebind; do not call directly; use builder
+     */
+    public ChefAttributeFeed() {
+    }
+    
+    protected ChefAttributeFeed(Builder builder) {
+        setConfig(ONLY_IF_SERVICE_UP, builder.onlyIfServiceUp);
+        setConfig(NODE_NAME, checkNotNull(builder.nodeName, "builder.nodeName"));
+
+        Set<ChefAttributePollConfig<?>> polls = Sets.newLinkedHashSet();
+        for (ChefAttributePollConfig<?> config : builder.polls) {
+            if (!config.isEnabled()) continue;
+            @SuppressWarnings({ "unchecked", "rawtypes" })
+            ChefAttributePollConfig<?> configCopy = new ChefAttributePollConfig(config);
+            if (configCopy.getPeriod() < 0) configCopy.period(builder.period);
+            polls.add(configCopy);
+        }
+        setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls);
+    }
+
+    @Override
+    protected void preStart() {
+        final String nodeName = getConfig(NODE_NAME);
+        final Set<ChefAttributePollConfig<?>> polls = getConfig(POLLS);
+        
+        long minPeriod = Integer.MAX_VALUE;
+        for (ChefAttributePollConfig<?> config : polls) {
+            minPeriod = Math.min(minPeriod, config.getPeriod());
+        }
+
+        knifeTaskFactory = new KnifeNodeAttributeQueryTaskFactory(nodeName);
+        
+        final Callable<SshPollValue> getAttributesFromKnife = new Callable<SshPollValue>() {
+            public SshPollValue call() throws Exception {
+                ProcessTaskWrapper<String> taskWrapper = knifeTaskFactory.newTask();
+                final ExecutionContext executionContext = ((EntityInternal) entity).getManagementSupport().getExecutionContext();
+                log.debug("START: Running knife to query attributes of Chef node {}", nodeName);
+                executionContext.submit(taskWrapper);
+                taskWrapper.block();
+                log.debug("DONE:  Running knife to query attributes of Chef node {}", nodeName);
+                return new SshPollValue(null, taskWrapper.getExitCode(), taskWrapper.getStdout(), taskWrapper.getStderr());
+            }
+        };
+
+        getPoller().scheduleAtFixedRate(
+                new CallInEntityExecutionContext<SshPollValue>(entity, getAttributesFromKnife),
+                new SendChefAttributesToSensors(entity, polls),
+                minPeriod);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected Poller<SshPollValue> getPoller() {
+        return (Poller<SshPollValue>) super.getPoller();
+    }
+
+    /**
+     * An implementation of {@link KnifeTaskFactory} that queries for the attributes of a node.
+     */
+    private static class KnifeNodeAttributeQueryTaskFactory extends KnifeTaskFactory<String> {
+        private final String nodeName;
+
+        public KnifeNodeAttributeQueryTaskFactory(String nodeName) {
+            super("retrieve attributes of node " + nodeName);
+            this.nodeName = nodeName;
+        }
+
+        @Override
+        protected List<String> initialKnifeParameters() {
+            return ImmutableList.of("node", "show", "-l", nodeName, "--format", "json");
+        }
+    }
+
+    /**
+     * A {@link Callable} that wraps another {@link Callable}, where the inner {@link Callable} is executed in the context of a
+     * specific entity.
+     *
+     * @param <T> The type of the {@link Callable}.
+     */
+    private static class CallInEntityExecutionContext<T> implements Callable<T> {
+
+        private final Callable<T> job;
+        private EntityLocal entity;
+
+        private CallInEntityExecutionContext(EntityLocal entity, Callable<T> job) {
+            this.job = job;
+            this.entity = entity;
+        }
+
+        @Override
+        public T call() throws Exception {
+            final ExecutionContext executionContext = ((EntityInternal) entity).getManagementSupport().getExecutionContext();
+            return executionContext.submit(Maps.newHashMap(), job).get();
+        }
+    }
+
+    /**
+     * A poll handler that takes the result of the <tt>knife</tt> invocation and sets the appropriate sensors.
+     */
+    private static class SendChefAttributesToSensors implements PollHandler<SshPollValue> {
+        private static final Iterable<String> PREFIXES = ImmutableList.of("", "automatic", "force_override", "override", "normal", "force_default", "default");
+        private static final Splitter SPLITTER = Splitter.on('.');
+
+        private final EntityLocal entity;
+        private final Map<String, AttributeSensor<?>> chefAttributeSensors;
+
+        public SendChefAttributesToSensors(EntityLocal entity, Set<ChefAttributePollConfig<?>> polls) {
+            this.entity = entity;
+            chefAttributeSensors = Maps.newLinkedHashMap();
+            for (ChefAttributePollConfig<?> config : polls) {
+                chefAttributeSensors.put(config.getChefAttributePath(), config.getSensor());
+            }
+        }
+
+        @Override
+        public boolean checkSuccess(SshPollValue val) {
+            if (val.getExitStatus() != 0) return false;
+            String stderr = val.getStderr();
+            if (stderr == null || stderr.length() != 0) return false;
+            String out = val.getStdout();
+            if (out == null || out.length() == 0) return false;
+            if (!out.contains("{")) return false;
+            return true;
+        }
+
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        @Override
+        public void onSuccess(SshPollValue val) {
+            String stdout = val.getStdout();
+            int jsonStarts = stdout.indexOf('{');
+            if (jsonStarts > 0)
+                stdout = stdout.substring(jsonStarts);
+            JsonElement jsonElement = new Gson().fromJson(stdout, JsonElement.class);
+
+            for (Map.Entry<String, AttributeSensor<?>> attribute : chefAttributeSensors.entrySet()) {
+                String chefAttributeName = attribute.getKey();
+                AttributeSensor<?> sensor = attribute.getValue();
+                log.trace("Finding value for attribute sensor " + sensor.getName());
+
+                Iterable<String> path = SPLITTER.split(chefAttributeName);
+                JsonElement elementForSensor = null;
+                for(String prefix : PREFIXES) {
+                    Iterable<String> prefixedPath = !Strings.isNullOrEmpty(prefix)
+                            ? Iterables.concat(ImmutableList.of(prefix), path)
+                            : path;
+                    try {
+                        elementForSensor = getElementByPath(jsonElement.getAsJsonObject(), prefixedPath);
+                    } catch(IllegalArgumentException e) {
+                        log.error("Entity {}: bad Chef attribute {} for sensor {}: {}", new Object[]{
+                                entity.getDisplayName(),
+                                Joiner.on('.').join(prefixedPath),
+                                sensor.getName(),
+                                e.getMessage()});
+                        throw Throwables.propagate(e);
+                    }
+                    if (elementForSensor != null) {
+                        log.debug("Entity {}: apply Chef attribute {} to sensor {} with value {}", new Object[]{
+                                entity.getDisplayName(),
+                                Joiner.on('.').join(prefixedPath),
+                                sensor.getName(),
+                                elementForSensor.getAsString()});
+                        break;
+                    }
+                }
+                if (elementForSensor != null) {
+                    entity.setAttribute((AttributeSensor)sensor, TypeCoercions.coerce(elementForSensor.getAsString(), sensor.getTypeToken()));
+                } else {
+                    log.debug("Entity {}: no Chef attribute matching {}; setting sensor {} to null", new Object[]{
+                            entity.getDisplayName(),
+                            chefAttributeName,
+                            sensor.getName()});
+                    entity.setAttribute(sensor, null);
+                }
+            }
+        }
+
+        private JsonElement getElementByPath(JsonElement element, Iterable<String> path) {
+            if (Iterables.isEmpty(path)) {
+                return element;
+            } else {
+                String head = Iterables.getFirst(path, null);
+                Preconditions.checkArgument(!Strings.isNullOrEmpty(head), "path must not contain empty or null elements");
+                Iterable<String> tail = Iterables.skip(path, 1);
+                JsonElement child = ((JsonObject) element).get(head);
+                return child != null
+                        ? getElementByPath(child, tail)
+                        : null;
+            }
+        }
+
+        @Override
+        public void onFailure(SshPollValue val) {
+            log.error("Chef attribute query did not respond as expected. exitcode={} stdout={} stderr={}", new Object[]{val.getExitStatus(), val.getStdout(), val.getStderr()});
+            for (AttributeSensor<?> attribute : chefAttributeSensors.values()) {
+                if (!attribute.getName().startsWith(CHEF_ATTRIBUTE_PREFIX))
+                    continue;
+                entity.setAttribute(attribute, null);
+            }
+        }
+
+        @Override
+        public void onException(Exception exception) {
+            log.error("Detected exception while retrieving Chef attributes from entity " + entity.getDisplayName(), exception);
+            for (AttributeSensor<?> attribute : chefAttributeSensors.values()) {
+                if (!attribute.getName().startsWith(CHEF_ATTRIBUTE_PREFIX))
+                    continue;
+                entity.setAttribute(attribute, null);
+            }
+        }
+        
+        @Override
+        public String toString() {
+            return super.toString()+"["+getDescription()+"]";
+        }
+        
+        @Override
+        public String getDescription() {
+            return ""+chefAttributeSensors;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
new file mode 100644
index 0000000..c6e1c6b
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
@@ -0,0 +1,53 @@
+/*
+ * 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 org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.sensor.feed.PollConfig;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+public class ChefAttributePollConfig<T> extends PollConfig<Object, T, ChefAttributePollConfig<T>>{
+
+    private String chefAttributePath;
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public ChefAttributePollConfig(AttributeSensor<T> sensor) {
+        super(sensor);
+        onSuccess((Function)Functions.identity());
+    }
+
+    public ChefAttributePollConfig(ChefAttributePollConfig<T> other) {
+        super(other);
+        this.chefAttributePath = other.chefAttributePath;
+    }
+
+    public String getChefAttributePath() {
+        return chefAttributePath;
+    }
+    
+    public ChefAttributePollConfig<T> chefAttributePath(String val) {
+        this.chefAttributePath = val; return this;
+    }
+    
+    @Override protected String toStringBaseName() { return "chef"; }
+    @Override protected String toStringPollSource() { return chefAttributePath; }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
new file mode 100644
index 0000000..dde58d3
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
@@ -0,0 +1,42 @@
+/*
+ * 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 static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_CURL;
+import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_TAR;
+import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_UNZIP;
+import static org.apache.brooklyn.util.ssh.BashCommands.downloadToStdout;
+import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
+
+import org.apache.brooklyn.util.ssh.BashCommands;
+
+import com.google.common.annotations.Beta;
+
+/** BASH commands useful for setting up Chef */
+@Beta
+public class ChefBashCommands {
+
+    public static final String INSTALL_FROM_OPSCODE =
+            BashCommands.chain(
+                    INSTALL_CURL,
+                    INSTALL_TAR,
+                    INSTALL_UNZIP,
+                    "( "+downloadToStdout("https://www.opscode.com/chef/install.sh") + " | " + sudo("bash")+" )");
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
new file mode 100644
index 0000000..974149e
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.chef;
+
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
+import org.apache.brooklyn.core.config.SetConfigKey;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+import com.google.common.annotations.Beta;
+
+/** {@link ConfigKey}s used to configure the chef driver */
+@Beta
+public interface ChefConfig {
+
+    public static final ConfigKey<String> CHEF_COOKBOOK_PRIMARY_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.cookbook.primary.name",
+        "Namespace to use for passing data to Chef and for finding effectors");
+    
+    @Deprecated /** @deprecatd since 0.7.0 use #CHEF_COOKBOOK_URLS */
+    @SetFromFlag("cookbooks")
+    public static final MapConfigKey<String> CHEF_COOKBOOKS = new MapConfigKey<String>(String.class, "brooklyn.chef.cookbooksUrls");
+
+    @SetFromFlag("cookbook_urls")
+    public static final MapConfigKey<String> CHEF_COOKBOOK_URLS = new MapConfigKey<String>(String.class, "brooklyn.chef.cookbooksUrls");
+
+    @SetFromFlag("converge_twice")
+    public static final ConfigKey<Boolean> CHEF_RUN_CONVERGE_TWICE = ConfigKeys.newBooleanConfigKey("brooklyn.chef.converge.twice",
+            "Whether to run converge commands twice if the first one fails; needed in some contexts, e.g. when switching between chef-server and chef-solo mode", false);
+
+    @Deprecated /** @deprecated since 0.7.0 use #CHEF_LAUNCH_RUN_LIST */
+    public static final SetConfigKey<String> CHEF_RUN_LIST = new SetConfigKey<String>(String.class, "brooklyn.chef.runList");
+    
+    /** typically set from spec, to customize the launch part of the start effector */
+    @SetFromFlag("launch_run_list")
+    public static final SetConfigKey<String> CHEF_LAUNCH_RUN_LIST = new SetConfigKey<String>(String.class, "brooklyn.chef.launch.runList");
+    /** typically set from spec, to customize the launch part of the start effector */
+    @SetFromFlag("launch_attributes")
+    public static final MapConfigKey<Object> CHEF_LAUNCH_ATTRIBUTES = new MapConfigKey<Object>(Object.class, "brooklyn.chef.launch.attributes");
+    
+    public static enum ChefModes {
+        /** Force use of Chef Solo */
+        SOLO, 
+        /** Force use of Knife; knife must be installed, and either 
+         *  {@link ChefConfig#KNIFE_EXECUTABLE} and {@link ChefConfig#KNIFE_CONFIG_FILE} must be set 
+         *  or knife on the path with valid global config set up */
+        KNIFE,
+        // TODO server via API
+        /** Tries {@link #KNIFE} if valid, else {@link #SOLO} */
+        AUTODETECT
+    };
+
+    @SetFromFlag("chef_mode")
+    public static final ConfigKey<ChefModes> CHEF_MODE = ConfigKeys.newConfigKey(ChefModes.class, "brooklyn.chef.mode",
+            "Whether Chef should run in solo mode, knife mode, or auto-detect", ChefModes.AUTODETECT);
+
+    // TODO server-url for server via API mode
+    
+    public static final ConfigKey<String> KNIFE_SETUP_COMMANDS = ConfigKeys.newStringConfigKey("brooklyn.chef.knife.setupCommands",
+            "An optional set of commands to run on localhost before invoking knife; useful if using ruby version manager for example");
+    public static final ConfigKey<String> KNIFE_EXECUTABLE = ConfigKeys.newStringConfigKey("brooklyn.chef.knife.executableFile",
+            "Knife command to run on the Brooklyn machine, including full path; defaults to scanning the path");
+    public static final ConfigKey<String> KNIFE_CONFIG_FILE = ConfigKeys.newStringConfigKey("brooklyn.chef.knife.configFile",
+            "Knife config file (typically knife.rb) to use, including full path; defaults to knife default/global config");
+
+    @SetFromFlag("chef_node_name")
+    public static final ConfigKey<String> CHEF_NODE_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.node.nodeName",
+        "Node name to register with the chef server for this entity, if using Chef server and a specific node name is desired; "
+        + "if supplied ,this must be unique across the nodes Chef Server manages; if not supplied, one will be created if needed");
+
+    // for providing some simple (ssh-based) lifecycle operations and checks
+    @SetFromFlag("pid_file")
+    public static final ConfigKey<String> PID_FILE = ConfigKeys.newStringConfigKey("brooklyn.chef.lifecycle.pidFile",
+        "Path to PID file on remote machine, for use in checking running and stopping; may contain wildcards");
+    @SetFromFlag("service_name")
+    public static final ConfigKey<String> SERVICE_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.lifecycle.serviceName",
+        "Name of OS service this will run as, for use in checking running and stopping");
+    @SetFromFlag("windows_service_name")
+    public static final ConfigKey<String> WINDOWS_SERVICE_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.lifecycle.windowsServiceName",
+        "Name of OS service this will run as on Windows, if different there, for use in checking running and stopping");
+    
+}