Move chef from brooklyn—software-base to own module
diff --git a/karaf/features/src/main/feature/feature.xml b/karaf/features/src/main/feature/feature.xml
index 5bb9319..8106a7b 100644
--- a/karaf/features/src/main/feature/feature.xml
+++ b/karaf/features/src/main/feature/feature.xml
@@ -27,6 +27,7 @@
     </feature>
 
     <feature name="brooklyn-software-cm" version="${project.version}" description="Configuration Management modules">
+        <bundle>mvn:org.apache.brooklyn/brooklyn-software-cm-chef/${project.version}</bundle>
         <bundle>mvn:org.apache.brooklyn/brooklyn-software-cm-salt/${project.version}</bundle>
         <bundle>mvn:org.apache.brooklyn/brooklyn-software-cm-ansible/${project.version}</bundle>
     </feature>
diff --git a/software/cm/chef/pom.xml b/software/cm/chef/pom.xml
new file mode 100644
index 0000000..e673281
--- /dev/null
+++ b/software/cm/chef/pom.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>brooklyn-software-cm-chef</artifactId>
+  <packaging>jar</packaging>
+  <name>Brooklyn CM Chef</name>
+  <description>Brooklyn entities for Configuration Management using Chef</description>
+
+
+    <parent>
+        <groupId>org.apache.brooklyn</groupId>
+        <artifactId>brooklyn-library</artifactId>
+        <version>0.12.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
+        <relativePath>../../../pom.xml</relativePath>
+    </parent>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.brooklyn</groupId>
+      <artifactId>brooklyn-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.brooklyn</groupId>
+      <artifactId>brooklyn-software-base</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+         <groupId>org.apache.brooklyn</groupId>
+         <artifactId>brooklyn-camp</artifactId>
+        <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.brooklyn</groupId>
+      <artifactId>brooklyn-test-support</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.brooklyn</groupId>
+      <artifactId>brooklyn-core</artifactId>
+      <version>${project.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.brooklyn</groupId>
+      <artifactId>brooklyn-software-base</artifactId>
+      <version>${project.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <version>${assertj.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
new file mode 100644
index 0000000..f6d2615
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
@@ -0,0 +1,413 @@
+/*
+ * 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.entity.Entity;
+import org.apache.brooklyn.api.entity.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.core.entity.EntityInternal;
+import org.apache.brooklyn.core.feed.AbstractFeed;
+import org.apache.brooklyn.core.feed.PollHandler;
+import org.apache.brooklyn.core.feed.Poller;
+import org.apache.brooklyn.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 Entity 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(Entity 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((EntityLocal)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>() {
+            @Override
+            public SshPollValue call() throws Exception {
+                ProcessTaskWrapper<String> taskWrapper = knifeTaskFactory.newTask();
+                final ExecutionContext executionContext = ((EntityInternal) entity).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);
+    }
+
+    @Override
+    @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 Entity entity;
+
+        private CallInEntityExecutionContext(Entity entity, Callable<T> job) {
+            this.job = job;
+            this.entity = entity;
+        }
+
+        @Override
+        public T call() throws Exception {
+            final ExecutionContext executionContext = ((EntityInternal) entity).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 Entity entity;
+        private final Map<String, AttributeSensor<?>> chefAttributeSensors;
+
+        public SendChefAttributesToSensors(Entity 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.sensors().set((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.sensors().set(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.sensors().set(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.sensors().set(attribute, null);
+            }
+        }
+        
+        @Override
+        public String toString() {
+            return super.toString()+"["+getDescription()+"]";
+        }
+        
+        @Override
+        public String getDescription() {
+            return ""+chefAttributeSensors;
+        }
+    }
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
new file mode 100644
index 0000000..c090f34
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.chef;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.core.feed.PollConfig;
+
+public class ChefAttributePollConfig<T> extends PollConfig<Object, T, ChefAttributePollConfig<T>>{
+
+    private String chefAttributePath;
+
+    public static <T> ChefAttributePollConfig<T> forSensor(AttributeSensor<T> sensor) {
+        return new ChefAttributePollConfig<T>(sensor);
+    }
+
+    public static ChefAttributePollConfig<Void> forMultiple() {
+        return new ChefAttributePollConfig<Void>(PollConfig.NO_SENSOR);
+    }
+
+    @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; }
+    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
new file mode 100644
index 0000000..dde58d3
--- /dev/null
+++ b/software/cm/chef/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")+" )");
+
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
new file mode 100644
index 0000000..07b4ce5
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
@@ -0,0 +1,94 @@
+/*
+ * 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");
+
+    @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");
+    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java
new file mode 100644
index 0000000..16d5fa0
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java
@@ -0,0 +1,102 @@
+/*
+ * 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.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.MapConfigKey.MapModifications;
+import org.apache.brooklyn.core.config.SetConfigKey.SetModifications;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.util.git.GithubUrls;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+
+/** Conveniences for configuring brooklyn Chef entities 
+ * @since 0.6.0 */
+@Beta
+public class ChefConfigs {
+
+    public static void addToLaunchRunList(EntitySpec<?> entity, String ...recipes) {
+        for (String recipe: recipes)
+            entity.configure(ChefConfig.CHEF_LAUNCH_RUN_LIST, SetModifications.addItem(recipe));
+    }
+
+    public static void addToLaunchRunList(EntityInternal entity, String ...recipes) {
+        for (String recipe: recipes)
+            entity.config().set(ChefConfig.CHEF_LAUNCH_RUN_LIST, SetModifications.addItem(recipe));
+    }
+
+    public static void addToCookbooksFromGithub(EntitySpec<?> entity, String ...cookbookNames) {
+        for (String cookbookName: cookbookNames)
+            addToCookbooksFromGithub(entity, cookbookName, getGithubOpscodeRepo(cookbookName)); 
+    }
+    
+    public static void addToCookbooksFromGithub(EntityInternal entity, String ...cookbookNames) {
+        for (String cookbookName: cookbookNames)
+            addToCookbooksFromGithub(entity, cookbookName, getGithubOpscodeRepo(cookbookName)); 
+    }
+
+    public static String getGithubOpscodeRepo(String cookbookName) {
+        return getGithubOpscodeRepo(cookbookName, "master");
+    }
+    public static String getGithubOpscodeRepo(String cookbookName, String tag) {
+        return GithubUrls.tgz("opscode-cookbooks", cookbookName, tag);
+    }
+    
+    public static void addToCookbooksFromGithub(EntitySpec<?> entity, String cookbookName, String cookbookUrl) {
+        entity.configure(ChefConfig.CHEF_COOKBOOK_URLS.subKey(cookbookName), cookbookUrl);
+    }
+
+    public static void addToCookbooksFromGithub(EntityInternal entity, String cookbookName, String cookbookUrl) {
+        entity.config().set(ChefConfig.CHEF_COOKBOOK_URLS.subKey(cookbookName), cookbookUrl);
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static void addLaunchAttributes(EntitySpec<?> entity, Map<? extends Object,? extends Object> attributesMap) {
+        entity.configure(ChefConfig.CHEF_LAUNCH_ATTRIBUTES, MapModifications.add((Map)attributesMap));
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static void addLaunchAttributes(EntityInternal entity, Map<? extends Object,? extends Object> attributesMap) {
+        entity.config().set(ChefConfig.CHEF_LAUNCH_ATTRIBUTES, MapModifications.add((Map)attributesMap));
+    }
+    
+    /** replaces the attributes underneath the rootAttribute parameter with the given value;
+     * see {@link #addLaunchAttributesMap(EntitySpec, Map)} for richer functionality */
+    public static void setLaunchAttribute(EntitySpec<?> entity, String rootAttribute, Object value) {
+        entity.configure(ChefConfig.CHEF_LAUNCH_ATTRIBUTES.subKey(rootAttribute), value);
+    }
+    
+    /** replaces the attributes underneath the rootAttribute parameter with the given value;
+     * see {@link #addLaunchAttributesMap(EntitySpec, Map)} for richer functionality */
+    public static void setLaunchAttribute(EntityInternal entity, String rootAttribute, Object value) {
+        entity.config().set(ChefConfig.CHEF_LAUNCH_ATTRIBUTES.subKey(rootAttribute), value);
+    }
+
+    public static <T> T getRequiredConfig(Entity entity, ConfigKey<T> key) {
+        return Preconditions.checkNotNull(
+                Preconditions.checkNotNull(entity, "Entity must be supplied").getConfig(key), 
+                "Key "+key+" is required on "+entity);
+    }
+
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java
new file mode 100644
index 0000000..1b1f866
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java
@@ -0,0 +1,26 @@
+/*
+ * 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.entity.ImplementedBy;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+
+@ImplementedBy(ChefEntityImpl.class)
+public interface ChefEntity extends SoftwareProcess, ChefConfig {
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java
new file mode 100644
index 0000000..6bc3cfd
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java
@@ -0,0 +1,39 @@
+/*
+ * 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.entity.stock.EffectorStartableImpl;
+import org.apache.brooklyn.util.text.Strings;
+
+public class ChefEntityImpl extends EffectorStartableImpl implements ChefEntity {
+
+    @Override
+    public void init() {
+        String primaryName = getConfig(CHEF_COOKBOOK_PRIMARY_NAME);
+        if (!Strings.isBlank(primaryName)) setDefaultDisplayName(primaryName+" (chef)");
+        
+        super.init();
+        new ChefLifecycleEffectorTasks().attachLifecycleEffectors(this);
+    }
+
+    @Override
+    public void populateServiceNotUpDiagnostics() {
+        // TODO no-op currently; should check ssh'able etc
+    }    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java
new file mode 100644
index 0000000..8c3dad9
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java
@@ -0,0 +1,364 @@
+/*
+ * 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.Collection;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.location.Machines;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.collections.Jsonya;
+import org.apache.brooklyn.util.collections.Jsonya.Navigator;
+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.TaskTags;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.ssh.BashCommands;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+
+/** 
+ * Creates effectors to start, restart, and stop processes using Chef.
+ * <p>
+ * Instances of this should use the {@link ChefConfig} config attributes to configure startup,
+ * and invoke {@link #usePidFile(String)} or {@link #useService(String)} to determine check-running and stop behaviour.
+ * Alternatively this can be subclassed and {@link #postStartCustom()} and {@link #stopProcessesAtMachine()} overridden.
+ * 
+ * @since 0.6.0
+ **/
+@Beta
+public class ChefLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements ChefConfig {
+
+    private static final Logger log = LoggerFactory.getLogger(ChefLifecycleEffectorTasks.class);
+    
+    protected String _pidFile, _serviceName, _windowsServiceName;
+    
+    public ChefLifecycleEffectorTasks() {
+    }
+    
+    public ChefLifecycleEffectorTasks usePidFile(String pidFile) {
+        this._pidFile = pidFile;
+        return this;
+    }
+    public ChefLifecycleEffectorTasks useService(String serviceName) {
+        this._serviceName = serviceName;
+        return this;
+    }
+    public ChefLifecycleEffectorTasks useWindowsService(String serviceName) {
+        this._windowsServiceName = serviceName;
+        return this;
+    }
+    
+    public String getPidFile() {
+        if (_pidFile!=null) return _pidFile;
+        return _pidFile = entity().getConfig(ChefConfig.PID_FILE);
+    }
+
+    public String getServiceName() {
+        if (_serviceName!=null) return _serviceName;
+        return _serviceName = entity().getConfig(ChefConfig.SERVICE_NAME);
+    }
+
+    protected String getNodeName() {
+        // (node name is needed so we can node delete it)
+        
+        // TODO would be better if CHEF_NODE_NAME were a freemarker template, could access entity.id, or hostname, etc,
+        // in addition to supporting hard-coded node names (which is all we support so far).
+        
+        String nodeName = entity().getConfig(ChefConfig.CHEF_NODE_NAME);
+        if (Strings.isNonBlank(nodeName)) return Strings.makeValidFilename(nodeName);
+        // node name is taken from ID of this entity, if not specified
+        return entity().getId();
+    }
+
+    public String getWindowsServiceName() {
+        if (_windowsServiceName!=null) return _windowsServiceName;
+        return _windowsServiceName = entity().getConfig(ChefConfig.WINDOWS_SERVICE_NAME);
+    }
+
+    @Override
+    public void attachLifecycleEffectors(Entity entity) {
+        if (getPidFile()==null && getServiceName()==null && getClass().equals(ChefLifecycleEffectorTasks.class)) {
+            // warn on incorrect usage
+            log.warn("Uses of "+getClass()+" must define a PID file or a service name (or subclass and override {start,stop} methods as per javadoc) " +
+                    "in order for check-running and stop to work");
+        }
+            
+        super.attachLifecycleEffectors(entity);
+    }
+
+    public static ChefModes detectChefMode(Entity entity) {
+        ChefModes mode = entity.getConfig(ChefConfig.CHEF_MODE);
+        if (mode == ChefModes.AUTODETECT) {
+            // TODO server via API
+            ProcessTaskWrapper<Boolean> installCheck = DynamicTasks.queue(
+                    ChefServerTasks.isKnifeInstalled());
+            mode = installCheck.get() ? ChefModes.KNIFE : ChefModes.SOLO;
+            log.debug("Using Chef in "+mode+" mode due to autodetect exit code "+installCheck.getExitCode());
+        }
+        Preconditions.checkNotNull(mode, "Non-null "+ChefConfig.CHEF_MODE+" required for "+entity);
+        return mode;
+    }
+    
+    @Override
+    protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
+        ChefModes mode = detectChefMode(entity());
+        switch (mode) {
+        case KNIFE:
+            startWithKnifeAsync();
+            break;
+            
+        case SOLO:
+            startWithChefSoloAsync();
+            break;
+            
+        default:
+            throw new IllegalStateException("Unknown Chef mode "+mode+" when starting processes for "+entity());
+        }
+        
+        return "chef start tasks submitted ("+mode+")";
+    }
+
+    protected String getPrimaryCookbook() {
+        return entity().getConfig(CHEF_COOKBOOK_PRIMARY_NAME);
+    }
+    
+    @SuppressWarnings({ "unchecked", "deprecation" })
+    protected void startWithChefSoloAsync() {
+        String baseDir = MachineLifecycleEffectorTasks.resolveOnBoxDir(entity(), Machines.findUniqueMachineLocation(entity().getLocations(), SshMachineLocation.class).get());
+        String installDir = Urls.mergePaths(baseDir, "installs/chef");
+        
+        @SuppressWarnings("rawtypes")
+        Map<String, String> cookbooks = (Map) 
+            ConfigBag.newInstance( entity().getConfig(CHEF_COOKBOOK_URLS) )
+            .putIfAbsent( entity().getConfig(CHEF_COOKBOOK_URLS) )
+            .getAllConfig();
+        if (cookbooks.isEmpty())
+            log.warn("No cookbook_urls set for "+entity()+"; launch will likely fail subsequently");
+        DynamicTasks.queue(
+                ChefSoloTasks.installChef(installDir, false), 
+                ChefSoloTasks.installCookbooks(installDir, cookbooks, false));
+
+        // TODO chef for and run a prestart recipe if necessary
+        // TODO open ports
+
+        String primary = getPrimaryCookbook();
+        
+        // put all config under brooklyn/cookbook/config
+        Navigator<MutableMap<Object, Object>> attrs = Jsonya.newInstancePrimitive().at("brooklyn");
+        if (Strings.isNonBlank(primary)) attrs.at(primary);
+        attrs.at("config");
+        attrs.put( entity().config().getBag().getAllConfig() );
+        // and put launch attrs at root
+        try {
+            attrs.root().put((Map<?,?>)Tasks.resolveDeepValue(entity().getConfig(CHEF_LAUNCH_ATTRIBUTES), Object.class, entity().getExecutionContext()));
+        } catch (Exception e) { Exceptions.propagate(e); }
+        
+        Collection<? extends String> runList = entity().getConfig(CHEF_LAUNCH_RUN_LIST);
+        if (runList==null) runList = entity().getConfig(CHEF_RUN_LIST);
+        if (runList==null) {
+            if (Strings.isNonBlank(primary)) runList = ImmutableList.of(primary+"::"+"start");
+            else throw new IllegalStateException("Require a primary cookbook or a run_list to effect "+"start"+" on "+entity());
+        }
+        
+        String runDir = Urls.mergePaths(baseDir,  
+            "apps/"+entity().getApplicationId()+"/chef/entities/"+entity().getEntityType().getSimpleName()+"_"+entity().getId());
+        
+        DynamicTasks.queue(ChefSoloTasks.buildChefFile(runDir, installDir, "launch", 
+                runList, (Map<String, Object>) attrs.root().get()));
+        
+        DynamicTasks.queue(ChefSoloTasks.runChef(runDir, "launch", entity().getConfig(CHEF_RUN_CONVERGE_TWICE)));
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
+    protected void startWithKnifeAsync() {
+        // TODO prestart, ports (as above); also, note, some aspects of this are untested as we need a chef server
+        
+        String primary = getPrimaryCookbook();
+
+        // put all config under brooklyn/cookbook/config
+        Navigator<MutableMap<Object, Object>> attrs = Jsonya.newInstancePrimitive().at("brooklyn");
+        if (Strings.isNonBlank(primary)) attrs.at(primary);
+        attrs.at("config");
+        attrs.put( entity().config().getBag().getAllConfig() );
+        // and put launch attrs at root
+        try {
+            attrs.root().put((Map<?,?>)Tasks.resolveDeepValue(entity().getConfig(CHEF_LAUNCH_ATTRIBUTES), Object.class, entity().getExecutionContext()));
+        } catch (Exception e) { Exceptions.propagate(e); }
+
+        Collection<? extends String> runList = entity().getConfig(CHEF_LAUNCH_RUN_LIST);
+        if (runList==null) runList = entity().getConfig(CHEF_RUN_LIST);
+        if (runList==null) {
+            if (Strings.isNonBlank(primary)) runList = ImmutableList.of(primary+"::"+"start");
+            else throw new IllegalStateException("Require a primary cookbook or a run_list to effect "+"start"+" on "+entity());
+        }
+
+        DynamicTasks.queue(
+                ChefServerTasks.knifeConvergeTask()
+                    .knifeNodeName(getNodeName())
+                    .knifeRunList(Strings.join(runList, ","))
+                    .knifeAddAttributes((Map) attrs.root().get())
+                    .knifeRunTwice(entity().getConfig(CHEF_RUN_CONVERGE_TWICE)) );
+    }
+
+    @Override
+    protected void postStartCustom() {
+        boolean result = false;
+        result |= tryCheckStartPid();
+        result |= tryCheckStartService();
+        result |= tryCheckStartWindowsService();
+        if (!result) {
+            log.warn("No way to check whether "+entity()+" is running; assuming yes");
+        }
+        entity().sensors().set(SoftwareProcess.SERVICE_UP, true);
+        super.postStartCustom();
+    }
+    
+    protected boolean tryCheckStartPid() {
+        if (getPidFile()==null) return false;
+        
+        // if it's still up after 5s assume we are good (default behaviour)
+        Time.sleep(Duration.FIVE_SECONDS);
+        if (!DynamicTasks.queue(SshEffectorTasks.isPidFromFileRunning(getPidFile()).runAsRoot()).get()) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (pid file "+getPidFile()+")");
+        }
+
+        // and set the PID
+        entity().sensors().set(Attributes.PID, 
+                Integer.parseInt(DynamicTasks.queue(SshEffectorTasks.ssh("cat "+getPidFile()).runAsRoot()).block().getStdout().trim()));
+        return true;
+    }
+
+    protected boolean tryCheckStartService() {
+        if (getServiceName()==null) return false;
+        
+        // if it's still up after 5s assume we are good (default behaviour)
+        Time.sleep(Duration.FIVE_SECONDS);
+        if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+getServiceName()+" status").runAsRoot()).get())) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+getServiceName()+")");
+        }
+
+        return true;
+    }
+
+    protected boolean tryCheckStartWindowsService() {
+        if (getWindowsServiceName()==null) return false;
+        
+        // if it's still up after 5s assume we are good (default behaviour)
+        Time.sleep(Duration.FIVE_SECONDS);
+        if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh("sc query \""+getWindowsServiceName()+"\" | find \"RUNNING\"").runAsCommand()).get())) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (windowsService "+getWindowsServiceName()+")");
+        }
+
+        return true;
+    }
+
+    @Override
+    protected String stopProcessesAtMachine() {
+        boolean result = false;
+        result |= tryStopService();
+        result |= tryStopWindowsService();
+        result |= tryStopPid();
+        if (!result) {
+            throw new IllegalStateException("The process for "+entity()+" could not be stopped (no impl!)");
+        }
+        return "stopped";
+    }
+    
+    @Override
+    protected StopMachineDetails<Integer> stopAnyProvisionedMachines() {
+        if (detectChefMode(entity())==ChefModes.KNIFE) {
+            DynamicTasks.queue(
+                // if this task fails show it as failed but don't block subsequent routines
+                // (ie allow us to actually decommission the machine)
+                // TODO args could be a List<String> config key ?
+                TaskTags.markInessential(
+                new KnifeTaskFactory<String>("delete node and client registration at chef server")
+                    .add("knife node delete "+getNodeName()+" -y")
+                    .add("knife client delete "+getNodeName()+" -y")
+                    .requiringZeroAndReturningStdout()
+                    .newTask() ));
+        }
+
+        return super.stopAnyProvisionedMachines();
+    }
+    
+    protected boolean tryStopService() {
+        if (getServiceName()==null) return false;
+        int result = DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+getServiceName()+" stop").runAsRoot()).get();
+        if (0==result) return true;
+        if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)!=Lifecycle.RUNNING)
+            return true;
+        
+        throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)");
+    }
+
+    protected boolean tryStopWindowsService() {
+        if (getWindowsServiceName()==null) return false;
+                int result = DynamicTasks.queue(SshEffectorTasks.ssh("sc query \""+getWindowsServiceName()+"\"").runAsCommand()).get();
+        if (0==result) return true;
+        if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)!=Lifecycle.RUNNING)
+            return true;
+
+        throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)");
+    }
+
+    protected boolean tryStopPid() {
+        Integer pid = entity().getAttribute(Attributes.PID);
+        if (pid==null) {
+            if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)==Lifecycle.RUNNING && getPidFile()==null)
+                log.warn("No PID recorded for "+entity()+" when running, with PID file "+getPidFile()+"; skipping kill in "+Tasks.current());
+            else 
+                if (log.isDebugEnabled())
+                    log.debug("No PID recorded for "+entity()+"; skipping ("+entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)+" / "+getPidFile()+")");
+            return false;
+        }
+        
+        // allow non-zero exit as process may have already been killed
+        DynamicTasks.queue(SshEffectorTasks.ssh(
+                "kill "+pid, "sleep 5", BashCommands.ok("kill -9 "+pid)).allowingNonZeroExitCode().runAsRoot()).block();
+        
+        if (DynamicTasks.queue(SshEffectorTasks.isPidRunning(pid).runAsRoot()).get()) {
+            throw new IllegalStateException("Process for "+entity()+" in "+pid+" still running after kill");
+        }
+        entity().sensors().set(Attributes.PID, null);
+        return true;
+    }
+
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.java
new file mode 100644
index 0000000..c81ff9d
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.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.chef;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.KeyPair;
+
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.crypto.SecureKeys;
+
+import com.google.common.base.Throwables;
+import com.google.common.io.Files;
+
+public class ChefServerTasks {
+
+    private static File chefKeyDir;
+    
+    private synchronized static File getExtractedKeysDir() {
+        if (chefKeyDir==null) {
+            chefKeyDir = Files.createTempDir();
+            chefKeyDir.deleteOnExit();
+        }
+        return chefKeyDir;
+    }
+    
+    /** extract key to a temp file, but one per machine, scheduled for deletion afterwards;
+     * we extract the key because chef has no way to accept passphrases at present */
+    synchronized static File extractKeyFile(SshMachineLocation machine) {
+        File f = new File(getExtractedKeysDir(), machine.getAddress().getHostName()+".pem");
+        if (f.exists()) return f;
+        KeyPair data = machine.findKeyPair();
+        if (data==null) return null;
+        try {
+            f.deleteOnExit();
+            Files.write(SecureKeys.stringPem(data), f, Charset.defaultCharset());
+            return f;
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+    
+    public static KnifeTaskFactory<Boolean> isKnifeInstalled() {
+        return new KnifeTaskFactory<Boolean>("knife install check")
+                .knifeAddParameters("node list")
+                .notThrowingOnCommonKnifeErrors()
+                .returningIsExitCodeZero();
+    }
+
+    /** plain knife converge task - run list must be set, other arguments are optional */
+    public static KnifeConvergeTaskFactory<String> knifeConvergeTask() {
+        return new KnifeConvergeTaskFactory<String>("knife converge")
+                .requiringZeroAndReturningStdout();
+    }
+    /** knife converge task configured for this run list (and sudo) */
+    public static KnifeConvergeTaskFactory<String> knifeConvergeRunList(String runList) {
+        return knifeConvergeTask()
+                .knifeRunList(runList)
+                .knifeSudo(true);
+    }
+    
+    /** knife converge task configured for this run list on windows (ssh) */
+    public static KnifeConvergeTaskFactory<String> knifeConvergeRunListWindowsSsh(String runList) {
+        return knifeConvergeTask()
+                .knifeRunList(runList)
+                .knifeSudo(false)
+                .knifeAddExtraBootstrapParameters("windows ssh");
+    }
+    
+    /** knife converge task configured for this run list on windows (winrm) */
+    public static KnifeConvergeTaskFactory<String> knifeConvergeRunListWindowsWinrm(String runList) {
+        return knifeConvergeTask()
+                .knifeRunList(runList)
+                .knifeSudo(false)
+                .knifeAddExtraBootstrapParameters("windows winrm")
+                .knifePortUseKnifeDefault();
+    }
+    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java
new file mode 100644
index 0000000..6ee1786
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java
@@ -0,0 +1,85 @@
+/*
+ * 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.entity.EntityLocal;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+
+import com.google.common.annotations.Beta;
+import com.google.common.reflect.TypeToken;
+
+/** Driver class to facilitate use of Chef */
+@Beta
+@Deprecated /** @deprecated since 0.7.0 use ChefEntity or ChefLifecycleEffectorTasks */
+public class ChefSoloDriver extends AbstractSoftwareProcessSshDriver implements ChefConfig {
+
+    @SuppressWarnings("serial")
+    public static final ConfigKey<TaskFactory<? extends TaskAdaptable<Boolean>>> IS_RUNNING_TASK = ConfigKeys.newConfigKey(
+            new TypeToken<TaskFactory<? extends TaskAdaptable<Boolean>>>() {}, 
+            "brooklyn.chef.task.driver.isRunningTask");
+    
+    @SuppressWarnings("serial")
+    public static final ConfigKey<TaskFactory<?>> STOP_TASK = ConfigKeys.newConfigKey(
+            new TypeToken<TaskFactory<?>>() {}, 
+            "brooklyn.chef.task.driver.stopTask");
+    
+    public ChefSoloDriver(EntityLocal entity, SshMachineLocation location) {
+        super(entity, location);
+    }
+
+    @Override
+    public void install() {
+        // TODO flag to force reinstallation
+        DynamicTasks.queue(
+                ChefSoloTasks.installChef(getInstallDir(), false), 
+                ChefSoloTasks.installCookbooks(getInstallDir(), getRequiredConfig(CHEF_COOKBOOK_URLS), false));
+    }
+
+    @Override
+    public void customize() {
+        DynamicTasks.queue(ChefSoloTasks.buildChefFile(getRunDir(), getInstallDir(), "launch", getRequiredConfig(CHEF_RUN_LIST),
+                getEntity().getConfig(CHEF_LAUNCH_ATTRIBUTES)));
+    }
+
+    @Override
+    public void launch() {
+        DynamicTasks.queue(ChefSoloTasks.runChef(getRunDir(), "launch", getEntity().getConfig(CHEF_RUN_CONVERGE_TWICE)));
+    }
+
+    @Override
+    public boolean isRunning() {
+        return DynamicTasks.queue(getRequiredConfig(IS_RUNNING_TASK)).asTask().getUnchecked();
+    }
+
+    @Override
+    public void stop() {
+        DynamicTasks.queue(getRequiredConfig(STOP_TASK));
+    }
+
+    protected <T> T getRequiredConfig(ConfigKey<T> key) {
+        return ChefConfigs.getRequiredConfig(getEntity(), key);
+    }
+    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java
new file mode 100644
index 0000000..505f37e
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java
@@ -0,0 +1,70 @@
+/*
+ * 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.Map;
+
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.util.ssh.BashCommands;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class ChefSoloTasks {
+
+    public static TaskFactory<?> installChef(String chefDirectory, boolean force) {
+        // TODO check on entity whether it is chef _server_
+        String installCmd = cdAndRun(chefDirectory, ChefBashCommands.INSTALL_FROM_OPSCODE);
+        if (!force) installCmd = BashCommands.alternatives("which chef-solo", installCmd);
+        return SshEffectorTasks.ssh(installCmd).summary("install chef");
+    }
+
+    public static TaskFactory<?> installCookbooks(final String chefDirectory, final Map<String,String> cookbooksAndUrls, final boolean force) {
+        return ChefTasks.installCookbooks(chefDirectory, cookbooksAndUrls, force);
+    }
+
+    public static TaskFactory<?> installCookbook(String chefDirectory, String cookbookName, String cookbookArchiveUrl, boolean force) {
+        return ChefTasks.installCookbook(chefDirectory, cookbookName, cookbookArchiveUrl, force);
+    }
+
+    protected static String cdAndRun(String targetDirectory, String command) {
+        return BashCommands.chain("mkdir -p "+targetDirectory,
+                "cd "+targetDirectory,
+                command);
+    }
+
+    public static TaskFactory<?> buildChefFile(String runDirectory, String chefDirectory, String phase, Iterable<? extends String> runList,
+            Map<String, Object> optionalAttributes) {
+        return ChefTasks.buildChefFile(runDirectory, chefDirectory, phase, runList, optionalAttributes);
+    }
+
+    public static TaskFactory<?> runChef(String runDir, String phase) {
+        return runChef(runDir, phase, false);
+    }
+    /** see {@link ChefConfig#CHEF_RUN_CONVERGE_TWICE} for background on why 'twice' is available */
+    public static TaskFactory<?> runChef(String runDir, String phase, Boolean twice) {
+        String cmd = "sudo chef-solo -c "+phase+".rb -j "+phase+".json -ldebug";
+        if (twice!=null && twice) cmd = BashCommands.alternatives(cmd, cmd);
+
+        return SshEffectorTasks.ssh(cdAndRun(runDir, cmd)).
+                summary("run chef for "+phase).requiringExitCodeZero();
+    }
+    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java
new file mode 100644
index 0000000..e2edcb2
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java
@@ -0,0 +1,154 @@
+/*
+ * 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.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.core.effector.EffectorTasks;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.file.ArchiveTasks;
+import org.apache.brooklyn.util.core.file.ArchiveUtils.ArchiveType;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.TaskBuilder;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.ssh.BashCommands;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+@Beta
+public class ChefTasks {
+
+    private static final Logger log = LoggerFactory.getLogger(ChefTasks.class);
+    
+    public static TaskFactory<?> installChef(String chefDirectory, boolean force) {
+        // TODO check on entity whether it is chef _server_
+        String installCmd = cdAndRun(chefDirectory, ChefBashCommands.INSTALL_FROM_OPSCODE);
+        if (!force) installCmd = BashCommands.alternatives("which chef-solo", installCmd);
+        return SshEffectorTasks.ssh(installCmd).summary("install chef");
+    }
+
+    public static TaskFactory<?> installCookbooks(final String chefDirectory, final Map<String,String> cookbooksAndUrls, final boolean force) {
+        return Tasks.<Void>builder().displayName("install "+(cookbooksAndUrls==null ? "0" : cookbooksAndUrls.size())+" cookbook"+Strings.s(cookbooksAndUrls)).body(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        Entity e = EffectorTasks.findEntity();
+                        if (cookbooksAndUrls==null)
+                            throw new IllegalStateException("No cookbooks defined to install at "+e);
+                        for (String cookbook: cookbooksAndUrls.keySet())
+                            DynamicTasks.queue(installCookbook(chefDirectory, cookbook, cookbooksAndUrls.get(cookbook), force));
+                    }
+                }).buildFactory();
+    }
+
+    public static TaskFactory<?> installCookbook(final String chefDirectory, final String cookbookName, final String cookbookArchiveUrl, final boolean force) {
+        return new TaskFactory<TaskAdaptable<?>>() {
+            @Override
+            public TaskAdaptable<?> newTask() {
+                TaskBuilder<Void> tb = Tasks.<Void>builder().displayName("install cookbook "+cookbookName);
+                
+                String cookbookDir = Urls.mergePaths(chefDirectory, cookbookName);
+                String privateTmpDirContainingUnpackedCookbook = 
+                    Urls.mergePaths(chefDirectory, "tmp-"+Strings.makeValidFilename(cookbookName)+"-"+Identifiers.makeRandomId(4));
+
+                // TODO - skip the install earlier if it exists and isn't forced
+//                if (!force) {
+//                    // in builder.body, check 
+//                    // "ls "+cookbookDir
+//                    // and stop if it's zero
+//                    // remove reference to 'force' below
+//                }
+                
+                String destName = null;
+                if (ArchiveType.of(cookbookArchiveUrl)==ArchiveType.UNKNOWN) {
+                    destName = cookbookName + ".tgz";
+                    log.debug("Assuming TGZ type for chef cookbook url "+cookbookArchiveUrl+"; it will be downloaded as "+destName);
+                }
+                tb.add(ArchiveTasks.deploy(null, null, cookbookArchiveUrl, EffectorTasks.findSshMachine(), privateTmpDirContainingUnpackedCookbook,
+                    false, null, destName).newTask());
+                
+                String installCmd = BashCommands.chain(
+                    "cd "+privateTmpDirContainingUnpackedCookbook,  
+                    "COOKBOOK_EXPANDED_DIR=`ls`",
+                    BashCommands.requireTest("`ls | wc -w` -eq 1", 
+                            "The deployed archive "+cookbookArchiveUrl+" must contain exactly one directory"),
+                    "mv $COOKBOOK_EXPANDED_DIR '../"+cookbookName+"'",
+                    "cd ..",
+                    "rm -rf '"+privateTmpDirContainingUnpackedCookbook+"'");
+                
+                installCmd = force ? BashCommands.alternatives("rm -rf "+cookbookDir, installCmd) : BashCommands.alternatives("ls "+cookbookDir+" > /dev/null 2> /dev/null", installCmd);
+                tb.add(SshEffectorTasks.ssh(installCmd).summary("renaming cookbook dir").requiringExitCodeZero().newTask());
+                
+                return tb.build();
+            }
+        };
+    }
+
+    protected static String cdAndRun(String targetDirectory, String command) {
+        return BashCommands.chain("mkdir -p '"+targetDirectory+"'",
+                "cd '"+targetDirectory+"'",
+                command);
+    }
+
+    public static TaskFactory<?> buildChefFile(String runDirectory, String chefDirectory, String phase, Iterable<? extends String> runList,
+            Map<String, Object> optionalAttributes) {
+        // TODO if it's server, try knife first
+        // TODO configure add'l properties
+        String phaseRb = 
+            "root = "
+            + "'"+runDirectory+"'" 
+            // recommended alternate to runDir is the following, but it is not available in some rubies
+            //+ File.absolute_path(File.dirname(__FILE__))"+
+            + "\n"+
+            "file_cache_path root\n"+
+//            "cookbook_path root + '/cookbooks'\n";
+            "cookbook_path '"+chefDirectory+"'\n";
+
+        Map<String,Object> phaseJsonMap = MutableMap.of();
+        if (optionalAttributes!=null)
+            phaseJsonMap.putAll(optionalAttributes);
+        if (runList!=null)
+            phaseJsonMap.put("run_list", ImmutableList.copyOf(runList));
+        Gson json = new GsonBuilder().create();
+        String phaseJson = json.toJson(phaseJsonMap);
+
+        return Tasks.sequential("build chef files for "+phase,
+                    SshEffectorTasks.put(Urls.mergePaths(runDirectory)+"/"+phase+".rb").contents(phaseRb).createDirectory(),
+                    SshEffectorTasks.put(Urls.mergePaths(runDirectory)+"/"+phase+".json").contents(phaseJson));
+    }
+
+    public static TaskFactory<?> runChef(String runDir, String phase) {
+        // TODO chef server
+        return SshEffectorTasks.ssh(cdAndRun(runDir, "sudo chef-solo -c "+phase+".rb -j "+phase+".json -ldebug")).
+                summary("run chef for "+phase).requiringExitCodeZero();
+    }
+    
+}
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/KnifeConvergeTaskFactory.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/KnifeConvergeTaskFactory.java
new file mode 100644
index 0000000..68a1d50
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/KnifeConvergeTaskFactory.java
@@ -0,0 +1,249 @@
+/*
+ * 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.checkNotNull;
+import static org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes.wrapBash;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.base.Strings;
+import com.google.common.net.HostAndPort;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.core.effector.EffectorTasks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.collections.Jsonya;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.ssh.BashCommands;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.gson.GsonBuilder;
+
+public class KnifeConvergeTaskFactory<RET> extends KnifeTaskFactory<RET> {
+
+    private static final Logger log = LoggerFactory.getLogger(KnifeConvergeTaskFactory.class);
+    
+    protected Function<? super Entity,String> runList;
+    protected Map<Object, Object> knifeAttributes = new MutableMap<Object, Object>();
+    protected List<String> extraBootstrapParameters = MutableList.of();
+    protected Boolean sudo;
+    protected Boolean runTwice;
+    protected String nodeName;
+    protected Integer port;
+    /** null means nothing specified, use user supplied or machine default;
+     * false means use machine default (disallow user supplied);
+     * true means use knife default (omit the argument and disallow user supplied)
+     */
+    protected Boolean portOmittedToUseKnifeDefault;
+
+    public KnifeConvergeTaskFactory(String taskName) {
+        super(taskName);
+    }
+    
+    @Override
+    protected KnifeConvergeTaskFactory<RET> self() {
+        return this;
+    }
+    
+    /** construct the knife command, based on the settings on other methods
+     * (called when instantiating the script, after all parameters sent)
+     */
+    @Override
+    protected List<String> initialKnifeParameters() {
+        // runs inside the task so can detect entity/machine at runtime
+        MutableList<String> result = new MutableList<String>();
+        SshMachineLocation machine = EffectorTasks.findSshMachine();
+        
+        result.add("bootstrap");
+        result.addAll(extraBootstrapParameters);
+
+        HostAndPort hostAndPort = machine.getSshHostAndPort();
+        result.add(wrapBash(hostAndPort.getHostText()));
+        Integer whichPort = knifeWhichPort(hostAndPort);
+        if (whichPort!=null)
+            result.add("-p "+whichPort);
+
+        result.add("-x "+wrapBash(checkNotNull(machine.getUser(), "user")));
+        
+        File keyfile = ChefServerTasks.extractKeyFile(machine);
+        if (keyfile!=null) result.add("-i "+keyfile.getPath());
+        else result.add("-P "+checkNotNull(machine.findPassword(), "No password or private key data for "+machine));
+        
+        result.add("--no-host-key-verify");
+        
+        if (sudo != Boolean.FALSE) result.add("--sudo");
+
+        if (!Strings.isNullOrEmpty(nodeName)) {
+            result.add("--node-name");
+            result.add(nodeName);
+        }
+
+        result.add("-r "+wrapBash(runList.apply(entity())));
+        
+        if (!knifeAttributes.isEmpty())
+            result.add("-j "+wrapBash(new GsonBuilder().create()
+                    .toJson(knifeAttributes)));
+
+        return result;
+    }
+    
+    /** whether knife should attempt to run twice;
+     * see {@link ChefConfig#CHEF_RUN_CONVERGE_TWICE} */
+    public KnifeConvergeTaskFactory<RET> knifeRunTwice(boolean runTwice) {
+        this.runTwice = runTwice;
+        return self();
+    }
+    
+    /** whether to pass --sudo to knife; default true */
+    public KnifeConvergeTaskFactory<RET> knifeSudo(boolean sudo) {
+        this.sudo = sudo;
+        return self();
+    }
+
+    /** what node name to pass to knife; default = null, meaning chef-client will pick the node name */
+    public KnifeConvergeTaskFactory<RET> knifeNodeName(String nodeName) {
+        this.nodeName = nodeName;
+        return self();
+    }
+
+    /** tell knife to use an explicit port */
+    public KnifeConvergeTaskFactory<RET> knifePort(int port) {
+        if (portOmittedToUseKnifeDefault!=null) {
+            log.warn("Port "+port+" specified to "+this+" for when already explicitly told to use a default (overriding previous); see subsequent warning for more details");
+        }
+        this.port = port;
+        return self();
+    }
+
+    /** omit the port parameter altogether (let knife use its default) */
+    public KnifeConvergeTaskFactory<RET> knifePortUseKnifeDefault() {
+        if (port!=null) {
+            log.warn("knifePortUseKnifeDefault specified to "+this+" when already told to use "+port+" explicitly (overriding previous); see subsequent warning for more details");
+            port = -1;
+        }
+        portOmittedToUseKnifeDefault = true;
+        return self();
+    }
+    
+    /** use the default port known to brooklyn for the target machine for ssh */
+    public KnifeConvergeTaskFactory<RET> knifePortUseMachineSshPort() {
+        if (port!=null) {
+            log.warn("knifePortUseMachineSshPort specified to "+this+" when already told to use "+port+" explicitly (overriding previous); see subsequent warning for more details");
+            port = -1;
+        }
+        portOmittedToUseKnifeDefault = false;
+        return self();
+    }
+    
+    protected Integer knifeWhichPort(HostAndPort hostAndPort) {
+        if (port==null) {
+            if (Boolean.TRUE.equals(portOmittedToUseKnifeDefault))
+                // user has explicitly said to use knife default, omitting port here
+                return null;
+            // default is to use the machine port
+            return hostAndPort.getPort();
+        }
+        if (port==-1) {
+            // port was supplied by user, then portDefault (true or false)
+            port = null;
+            Integer whichPort = knifeWhichPort(hostAndPort);
+            log.warn("knife port conflicting instructions for "+this+" at entity "+entity()+" on "+hostAndPort+"; using default ("+whichPort+")");
+            return whichPort;
+        }
+        if (portOmittedToUseKnifeDefault!=null) {
+            // portDefault was specified (true or false), then overridden with a port
+            log.warn("knife port conflicting instructions for "+this+" at entity "+entity()+" on "+hostAndPort+"; using supplied port "+port);
+        }
+        // port was supplied by user, use that
+        return port;
+    }
+    
+    /** parameters to pass to knife after the bootstrap command */
+    public KnifeConvergeTaskFactory<RET> knifeAddExtraBootstrapParameters(String extraBootstrapParameter1, String ...extraBootstrapParameters) {
+        this.extraBootstrapParameters.add(extraBootstrapParameter1);
+        for (String p: extraBootstrapParameters)
+            this.extraBootstrapParameters.add(p);
+        return self();
+    }
+    
+    /** function supplying the run list to be passed to knife, evaluated at the last moment */
+    public KnifeConvergeTaskFactory<RET> knifeRunList(Function<? super Entity, String> runList) {
+        this.runList = runList;
+        return self();
+    }
+    public KnifeConvergeTaskFactory<RET> knifeRunList(String runList) {
+        this.runList = Functions.constant(runList);
+        return self();
+    }
+    
+    /** includes the given attributes in the attributes to be passed to chef; 
+     * when combining with other attributes, this uses {@link Jsonya} semantics to add 
+     * (a deep add, combining lists and maps) */
+    public KnifeConvergeTaskFactory<RET> knifeAddAttributes(Map<? extends Object, ? extends Object> attributes) {
+        if (attributes!=null && !attributes.isEmpty()) {
+            Jsonya.of(knifeAttributes).add(attributes);
+        }
+        return self();
+    }
+    
+    @Override
+    protected String buildKnifeCommand(int knifeCommandIndex) {
+        String result = super.buildKnifeCommand(knifeCommandIndex);
+        if (Boolean.TRUE.equals(runTwice))
+            result = BashCommands.alternatives(result, result);
+        return result;
+    }
+
+    @Override
+    public <T2> KnifeConvergeTaskFactory<T2> returning(ScriptReturnType type) {
+        return (KnifeConvergeTaskFactory<T2>) super.<T2>returning(type);
+    }
+
+    @Override
+    public <RET2> KnifeConvergeTaskFactory<RET2> returning(Function<ProcessTaskWrapper<?>, RET2> resultTransformation) {
+        return (KnifeConvergeTaskFactory<RET2>) super.returning(resultTransformation);
+    }
+    
+    @Override
+    public KnifeConvergeTaskFactory<Boolean> returningIsExitCodeZero() {
+        return (KnifeConvergeTaskFactory<Boolean>) super.returningIsExitCodeZero();
+    }
+    
+    @Override
+    public KnifeConvergeTaskFactory<String> requiringZeroAndReturningStdout() {
+        return (KnifeConvergeTaskFactory<String>) super.requiringZeroAndReturningStdout();
+    }
+
+    @Override
+    public KnifeConvergeTaskFactory<RET> knifeAddParameters(String word1, String ...words) {
+        super.knifeAddParameters(word1, words);
+        return self();
+    }
+
+    // TODO other methods from KnifeTaskFactory will return KTF class not KCTF;
+    // should make it generic so it returns the right type...
+}
\ No newline at end of file
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/KnifeTaskFactory.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/KnifeTaskFactory.java
new file mode 100644
index 0000000..2623a19
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/KnifeTaskFactory.java
@@ -0,0 +1,241 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.core.internal.ssh.process.ProcessTool;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.core.task.system.internal.SystemProcessTaskFactory;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes;
+
+import com.google.common.base.Function;
+
+/** A factory which acts like {@link ProcessTaskFactory} with special options for knife.
+ * Typical usage is to {@link #addKnifeParameters(String)}s for the knife command to be run.
+ * You can also {@link #add(String...)} commands as needed; these will run *before* knife,
+ * unless you addKnifeCommandHere().
+ * <p>
+ * This impl will use sensible defaults, including {@link ConfigKey}s on the context entity,
+ * for general knife config but not specific commands etc. It supports:
+ * <li> {@link ChefConfig#KNIFE_EXECUTABLE}
+ * <li> {@link ChefConfig#KNIFE_CONFIG_FILE}
+ * <p>
+ * (Other fields will typically be used by methods calling to this factory.) 
+ *  */
+// see e.g. http://docs.opscode.com/knife_bootstrap.html
+public class KnifeTaskFactory<RET> extends SystemProcessTaskFactory<KnifeTaskFactory<RET>, RET>{
+    
+    private static String KNIFE_PLACEHOLDER = "<knife command goes here 1234>";
+    public final String taskName;
+    protected String knifeExecutable;
+    protected List<String> knifeParameters = new ArrayList<String>();
+    protected String knifeConfigFile;
+    protected String knifeSetupCommands;
+    protected Boolean throwOnCommonKnifeErrors;
+    
+    public KnifeTaskFactory(String taskName) {
+        this.taskName = taskName;
+        summary(taskName);
+        // knife setup usually requires a login shell
+        config.put(ProcessTool.PROP_LOGIN_SHELL, true);
+    }
+    
+    @Override
+    public List<Function<ProcessTaskWrapper<?>, Void>> getCompletionListeners() {
+        MutableList<Function<ProcessTaskWrapper<?>, Void>> result = MutableList.copyOf(super.getCompletionListeners());
+        if (throwOnCommonKnifeErrors != Boolean.FALSE)
+            insertKnifeCompletionListenerIntoCompletionListenersList(result);
+        return result.asUnmodifiable();
+    }
+    
+    public KnifeTaskFactory<RET> notThrowingOnCommonKnifeErrors() {
+        throwOnCommonKnifeErrors = false;
+        return self();
+    }
+
+    protected void insertKnifeCompletionListenerIntoCompletionListenersList(List<Function<ProcessTaskWrapper<?>, Void>> listeners) {
+        // give a nice warning if chef/knife not set up correctly
+        Function<ProcessTaskWrapper<?>, Void> propagateIfKnifeConfigFileMissing = new Function<ProcessTaskWrapper<?>, Void>() {
+            @Override
+            public Void apply(@Nullable ProcessTaskWrapper<?> input) {
+                if (input.getExitCode()!=0 && input.getStderr().indexOf("WARNING: No knife configuration file found")>=0) {
+                    String myConfig = knifeConfigFileOption();
+                    if (Strings.isEmpty(myConfig))
+                        throw new IllegalStateException("Config file for Chef knife must be specified in "+ChefConfig.KNIFE_CONFIG_FILE+" (or valid knife default set up)");
+                    else
+                        throw new IllegalStateException("Error reading config file for Chef knife ("+myConfig+") -- does it exist?");
+                }
+                return null;
+            }
+        };
+        listeners.add(propagateIfKnifeConfigFileMissing);
+    }
+
+    
+    @Override
+    public ProcessTaskWrapper<RET> newTask() {
+        return new SystemProcessTaskWrapper("Knife");
+    }
+
+    /** Inserts the knife command at the current place in the list.
+     * Can be run multiple times. The knife command added at the end of the list
+     * if this is not invoked (and it is the only command if nothing is {@link #add(String...)}ed.
+     */
+    public KnifeTaskFactory<RET> addKnifeCommandToScript() {
+        add(KNIFE_PLACEHOLDER);
+        return self();
+    }
+    
+    @Override
+    public List<String> getCommands() {
+        MutableList<String> result = new MutableList<String>();
+        String setupCommands = knifeSetupCommands();
+        if (setupCommands != null && Strings.isNonBlank(setupCommands))
+            result.add(setupCommands);
+        int numKnifes = 0;
+        for (String c: super.getCommands()) {
+            if (c==KNIFE_PLACEHOLDER)
+                result.add(buildKnifeCommand(numKnifes++));
+            else
+                result.add(c);
+        }
+        if (numKnifes==0)
+            result.add(buildKnifeCommand(numKnifes++));
+        return result.asUnmodifiable();
+    }
+    
+    /** creates the command for running knife.
+     * in some cases knife may be added multiple times,
+     * and in that case the parameter here tells which time it is being added, 
+     * on a single run. */
+    protected String buildKnifeCommand(int knifeCommandIndex) {
+        MutableList<String> words = new MutableList<String>();
+        words.add(knifeExecutable());
+        words.addAll(initialKnifeParameters());
+        words.addAll(knifeParameters());
+        String x = knifeConfigFileOption();
+        if (Strings.isNonBlank(x)) words.add(knifeConfigFileOption());
+        return Strings.join(words, " ");
+    }
+    
+    /** allows a way for subclasses to build up parameters at the start */
+    protected List<String> initialKnifeParameters() {
+        return new MutableList<String>();
+    }
+    
+    @Nullable /** callers should allow this to be null so task can be used outside of an entity */
+    protected Entity entity() {
+        return BrooklynTaskTags.getTargetOrContextEntity(Tasks.current());
+    }
+    protected <T> T entityConfig(ConfigKey<T> key) {
+        Entity entity = entity();
+        if (entity!=null)
+            return entity.getConfig(key);
+        return null;
+    }
+    
+    public KnifeTaskFactory<RET> knifeExecutable(String knifeExecutable) {
+        this.knifeExecutable = knifeExecutable;
+        return this;
+    }
+    
+    protected String knifeExecutable() {
+        if (knifeExecutable!=null) return knifeExecutable;
+        
+        String knifeExecFromConfig = entityConfig(ChefConfig.KNIFE_EXECUTABLE);
+        if (knifeExecFromConfig!=null) return BashStringEscapes.wrapBash(knifeExecFromConfig);
+        
+        // assume on the path, if executable not set
+        return "knife";
+    }
+    
+    protected List<String> knifeParameters() {
+        return knifeParameters;
+    }
+    
+    public KnifeTaskFactory<RET> knifeAddParameters(String word1, String ...words) {
+        knifeParameters.add(word1);
+        for (String w: words)
+            knifeParameters.add(w);
+        return self();
+    }
+
+    public KnifeTaskFactory<RET> knifeConfigFile(String knifeConfigFile) {
+        this.knifeConfigFile = knifeConfigFile;
+        return self();
+    }
+    
+    @Nullable
+    protected String knifeConfigFileOption() {
+        if (knifeConfigFile!=null) return "-c "+knifeConfigFile;
+
+        String knifeConfigFileFromConfig = entityConfig(ChefConfig.KNIFE_CONFIG_FILE);
+        if (knifeConfigFileFromConfig!=null) return "-c "+BashStringEscapes.wrapBash(knifeConfigFileFromConfig);
+
+        // if not supplied will use global config
+        return null;
+    }
+
+    public KnifeTaskFactory<RET> knifeSetupCommands(String knifeSetupCommands) {
+        this.knifeSetupCommands = knifeSetupCommands;
+        return self();
+    }
+    
+    @Nullable
+    protected String knifeSetupCommands() {
+        if (knifeSetupCommands!=null) return knifeSetupCommands;
+        
+        String knifeSetupCommandsFromConfig = entityConfig(ChefConfig.KNIFE_SETUP_COMMANDS);
+        if (knifeSetupCommandsFromConfig!=null) return knifeSetupCommandsFromConfig;
+        
+        // if not supplied will use global config
+        return null;
+    }
+    
+    @Override
+    public <T2> KnifeTaskFactory<T2> returning(ScriptReturnType type) {
+        return (KnifeTaskFactory<T2>) super.<T2>returning(type);
+    }
+
+    @Override
+    public <RET2> KnifeTaskFactory<RET2> returning(Function<ProcessTaskWrapper<?>, RET2> resultTransformation) {
+        return (KnifeTaskFactory<RET2>) super.returning(resultTransformation);
+    }
+    
+    @Override
+    public KnifeTaskFactory<Boolean> returningIsExitCodeZero() {
+        return (KnifeTaskFactory<Boolean>) super.returningIsExitCodeZero();
+    }
+    
+    @Override
+    public KnifeTaskFactory<String> requiringZeroAndReturningStdout() {
+        return (KnifeTaskFactory<String>) super.requiringZeroAndReturningStdout();
+    }
+}
\ No newline at end of file
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/resolve/ChefEntitySpecResolver.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/resolve/ChefEntitySpecResolver.java
new file mode 100644
index 0000000..91e00ce
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/resolve/ChefEntitySpecResolver.java
@@ -0,0 +1,43 @@
+/*
+ * 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.resolve;
+
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
+import org.apache.brooklyn.core.resolve.entity.AbstractEntitySpecResolver;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefEntity;
+
+public class ChefEntitySpecResolver extends AbstractEntitySpecResolver {
+    private static final String RESOLVER_NAME = "chef";
+
+    public ChefEntitySpecResolver() {
+        super(RESOLVER_NAME);
+    }
+
+    @Override
+    public EntitySpec<?> resolve(String type, BrooklynClassLoadingContext loader, Set<String> encounteredTypes) {
+        return EntitySpec.create(ChefEntity.class)
+                .configure(ChefConfig.CHEF_COOKBOOK_PRIMARY_NAME, getLocalType(type));
+    }
+
+}
+
diff --git a/software/cm/chef/src/main/resources/META-INF/services/org.apache.brooklyn.core.resolve.entity.EntitySpecResolver b/software/cm/chef/src/main/resources/META-INF/services/org.apache.brooklyn.core.resolve.entity.EntitySpecResolver
new file mode 100644
index 0000000..cdd7af9
--- /dev/null
+++ b/software/cm/chef/src/main/resources/META-INF/services/org.apache.brooklyn.core.resolve.entity.EntitySpecResolver
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+org.apache.brooklyn.entity.resolve.ChefEntitySpecResolver
+
diff --git a/software/cm/chef/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/software/cm/chef/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 0000000..4412687
--- /dev/null
+++ b/software/cm/chef/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright 2015 The Apache Software Foundation.
+
+Licensed 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.
+-->
+
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.2.0"
+           xsi:schemaLocation="
+             http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd
+             http://aries.apache.org/schemas/blueprint-cm/blueprint-cm-1.1.0.xsd
+             ">
+
+    <bean id="chefEntitySpecResolver" scope="prototype"
+             class="org.apache.brooklyn.entity.resolve.ChefEntitySpecResolver"/>
+    <service id="chefEntitySpecResolverService" ref="chefEntitySpecResolver"
+             interface="org.apache.brooklyn.core.resolve.entity.EntitySpecResolver" />
+
+</blueprint>
+
diff --git a/software/cm/chef/src/main/resources/catalog.bom b/software/cm/chef/src/main/resources/catalog.bom
new file mode 100644
index 0000000..4322e23
--- /dev/null
+++ b/software/cm/chef/src/main/resources/catalog.bom
@@ -0,0 +1,26 @@
+# 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.
+
+brooklyn.catalog:
+    version: "0.12.0-SNAPSHOT" # BROOKLYN_VERSION
+    itemType: entity
+    items:
+    - id: org.apache.brooklyn.entity.chef.ChefEntity
+      item:
+        type: org.apache.brooklyn.entity.chef.ChefEntity
+        name: Chef Entity
+        description: Software managed by Chef
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java
new file mode 100644
index 0000000..dfdb85f
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefConfigsTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefConfigs;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class ChefConfigsTest extends BrooklynAppUnitTestSupport {
+
+    @Test
+    public void testAddToRunList() {
+        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"));
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefLiveTestSupport.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefLiveTestSupport.java
new file mode 100644
index 0000000..ff389b0
--- /dev/null
+++ b/software/cm/chef/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.InputStream;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.io.FileUtil;
+import org.apache.brooklyn.util.stream.Streams;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+
+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 
+     **/
+    public static MachineProvisioningLocation<? extends SshMachineLocation> createLocation(ManagementContext mgmt) {
+        LocationSpec<?> bestLocation = mgmt.getLocationRegistry().getLocationSpec("named:ChefTests").orNull();
+        if (bestLocation==null) {
+            log.info("using AWS for chef tests because named:ChefTests does not exist");
+            bestLocation = mgmt.getLocationRegistry().getLocationSpec("jclouds:aws-ec2:us-east-1").orNull();
+        }
+        if (bestLocation==null) {
+            throw new IllegalStateException("Need a location called named:ChefTests or AWS configured for these tests");
+        }
+        @SuppressWarnings("unchecked")
+        MachineProvisioningLocation<? extends SshMachineLocation> result = (MachineProvisioningLocation<? extends SshMachineLocation>) 
+        mgmt.getLocationManager().createLocation(bestLocation);
+        return result;
+    }
+    
+    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);
+        for (String f: new String[] { "knife.rb", "brooklyn-tests.pem", "brooklyn-validator.pem" }) {
+            InputStream in = r.getResourceFromUrl("classpath:///org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/"+f);
+            try {
+                FileUtil.copyTo(in, new File(tempDir, f));
+            } finally {
+                Streams.closeQuietly(in);
+            }
+        }
+        File knifeConfig = new File(tempDir, "knife.rb");
+        defaultConfigFile = knifeConfig.getPath();
+        return defaultConfigFile;
+    }
+
+    public static void installBrooklynChefHostedConfig(Entity entity) {
+        ((EntityInternal)entity).config().set(ChefConfig.KNIFE_CONFIG_FILE, ChefLiveTestSupport.installBrooklynChefHostedConfig());
+    }
+
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java
new file mode 100644
index 0000000..a7b5803
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/ChefServerTasksIntegrationTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.core.entity.Entities;
+import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefServerTasks;
+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.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.
+ **/
+// TODO Does it really need to be a live test? When converting from ApplicationBuilder, preserved
+// existing behaviour of using the live BrooklynProperties.
+public class ChefServerTasksIntegrationTest extends BrooklynAppLiveTestSupport {
+
+    private static final Logger log = LoggerFactory.getLogger(ChefServerTasksIntegrationTest.class);
+    
+    /** @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());
+    }
+
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/AbstractChefToyMySqlEntityLiveTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/AbstractChefToyMySqlEntityLiveTest.java
new file mode 100644
index 0000000..55fa4ff
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/AbstractChefToyMySqlEntityLiveTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.mysql;
+
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.entity.chef.ChefLiveTestSupport;
+import org.apache.brooklyn.entity.software.base.test.mysql.AbstractToyMySqlEntityTest;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+
+public abstract class AbstractChefToyMySqlEntityLiveTest extends AbstractToyMySqlEntityTest {
+
+    @Override
+    // mark as live here
+    @Test(groups = "Live")
+    public void testMySqlOnProvisioningLocation() throws Exception {
+        super.testMySqlOnProvisioningLocation();
+    }
+    
+    @Override
+    protected MachineProvisioningLocation<? extends SshMachineLocation> createLocation() {
+        return ChefLiveTestSupport.createLocation(mgmt);
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/ChefSoloDriverMySqlEntityLiveTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/ChefSoloDriverMySqlEntityLiveTest.java
new file mode 100644
index 0000000..a0bfba2
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/ChefSoloDriverMySqlEntityLiveTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.testng.annotations.Test;
+
+public class ChefSoloDriverMySqlEntityLiveTest extends AbstractChefToyMySqlEntityLiveTest {
+
+    // test here just so Eclipse IDE picks it up
+    @Override @Test(groups="Live")
+    public void testMySqlOnProvisioningLocation() throws Exception {
+        super.testMySqlOnProvisioningLocation();
+    }
+
+    @Override
+    protected Integer getPid(Entity mysql) {
+        ProcessTaskWrapper<Integer> t = Entities.submit(mysql, SshEffectorTasks.ssh("sudo cat "+ChefSoloDriverToyMySqlEntity.PID_FILE));
+        return Integer.parseInt(t.block().getStdout().trim());
+    }
+    
+    @Override
+    protected Entity createMysql() {
+        return app.createAndManageChild(EntitySpec.create(Entity.class, ChefSoloDriverToyMySqlEntity.class).
+                additionalInterfaces(SoftwareProcess.class));
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/ChefSoloDriverToyMySqlEntity.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/ChefSoloDriverToyMySqlEntity.java
new file mode 100644
index 0000000..70b20ad
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/ChefSoloDriverToyMySqlEntity.java
@@ -0,0 +1,89 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefConfigs;
+import org.apache.brooklyn.entity.chef.ChefSoloDriver;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
+import org.apache.brooklyn.feed.ssh.SshFeed;
+import org.apache.brooklyn.feed.ssh.SshPollConfig;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.time.Duration;
+
+@Deprecated /** @deprecated since 0.7.0 use see examples {Dynamic,Typed}ToyMySqlEntityChef */
+public class ChefSoloDriverToyMySqlEntity extends SoftwareProcessImpl implements ChefConfig {
+
+    public static final String PID_FILE = "/var/run/mysqld/mysqld.pid";
+    public static final ConfigKey<TaskFactory<? extends TaskAdaptable<Boolean>>> IS_RUNNING_TASK =
+            ConfigKeys.newConfigKeyWithDefault(ChefSoloDriver.IS_RUNNING_TASK, 
+            SshEffectorTasks.isPidFromFileRunning(PID_FILE).runAsRoot());
+
+    public static final ConfigKey<TaskFactory<?>> STOP_TASK =
+            ConfigKeys.newConfigKeyWithDefault(ChefSoloDriver.STOP_TASK, 
+            SshEffectorTasks.ssh("/etc/init.d/mysql stop").allowingNonZeroExitCode().runAsRoot());
+
+    private SshFeed upFeed;
+    
+    @Override
+    public Class<?> getDriverInterface() {
+        return ChefSoloDriver.class;
+    }
+
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        
+        // TODO have a TaskFactoryFeed which reuses the IS_RUNNING_TASK
+        upFeed = SshFeed.builder().entity(this).period(Duration.FIVE_SECONDS.toMilliseconds())
+            .poll(new SshPollConfig<Boolean>(SERVICE_UP)
+                    .command("ps -p `sudo cat /var/run/mysqld/mysqld.pid`")
+                    .setOnSuccess(true).setOnFailureOrException(false))
+            .build();
+    }
+    
+    @Override
+    protected void disconnectSensors() {
+        // TODO nicer way to disconnect
+        if (upFeed != null) upFeed.stop();
+        super.disconnectSensors();
+    }
+    
+    @Override
+    public void init() {
+        super.init();
+        ChefConfigs.addToLaunchRunList(this, "mysql::server");
+        ChefConfigs.addToCookbooksFromGithub(this, "mysql", "build-essential", "openssl");
+        ChefConfigs.setLaunchAttribute(this, "mysql",  
+                MutableMap.of()
+                    .add("server_root_password", "MyPassword")
+                    .add("server_debian_password", "MyPassword")
+                    .add("server_repl_password", "MyPassword")
+                );
+        
+        // TODO other attributes, eg:
+        // node['mysql']['port']
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefAutodetectToyMySqlEntityLiveTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefAutodetectToyMySqlEntityLiveTest.java
new file mode 100644
index 0000000..0102f6e
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefAutodetectToyMySqlEntityLiveTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+public class DynamicChefAutodetectToyMySqlEntityLiveTest extends AbstractChefToyMySqlEntityLiveTest {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicChefAutodetectToyMySqlEntityLiveTest.class);
+    
+    // test here just so Eclipse IDE picks it up
+    @Override @Test(groups="Live")
+    public void testMySqlOnProvisioningLocation() throws Exception {
+        super.testMySqlOnProvisioningLocation();
+    }
+    
+    @Override
+    protected Entity createMysql() {
+        Entity mysql = app.createAndManageChild(DynamicToyMySqlEntityChef.spec());
+        log.debug("created "+mysql);
+        return mysql;
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefServerToyMySqlEntityLiveTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefServerToyMySqlEntityLiveTest.java
new file mode 100644
index 0000000..566a96e
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefServerToyMySqlEntityLiveTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.entity.chef.ChefLiveTestSupport;
+import org.apache.brooklyn.entity.chef.ChefServerTasksIntegrationTest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+/** Expects knife on the path, but will use Brooklyn registered account,
+ * and that account has the mysql recipe installed.
+ * <p>
+ * See {@link ChefServerTasksIntegrationTest} for more info. */
+public class DynamicChefServerToyMySqlEntityLiveTest extends AbstractChefToyMySqlEntityLiveTest {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicChefServerToyMySqlEntityLiveTest.class);
+    
+    // test here just so Eclipse IDE picks it up
+    @Override @Test(groups="Live")
+    public void testMySqlOnProvisioningLocation() throws Exception {
+        super.testMySqlOnProvisioningLocation();
+    }
+    
+    @Override
+    protected Entity createMysql() {
+        ChefLiveTestSupport.installBrooklynChefHostedConfig(app);
+        Entity mysql = app.createAndManageChild(DynamicToyMySqlEntityChef.specKnife());
+        log.debug("created "+mysql);
+        return mysql;
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefSoloToyMySqlEntityLiveTest.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefSoloToyMySqlEntityLiveTest.java
new file mode 100644
index 0000000..ba6c6d9
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicChefSoloToyMySqlEntityLiveTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+public class DynamicChefSoloToyMySqlEntityLiveTest extends AbstractChefToyMySqlEntityLiveTest {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicChefSoloToyMySqlEntityLiveTest.class);
+    
+    // test here just so Eclipse IDE picks it up
+    @Override @Test(groups="Live")
+    public void testMySqlOnProvisioningLocation() throws Exception {
+        super.testMySqlOnProvisioningLocation();
+    }
+    
+    @Override
+    protected Entity createMysql() {
+        Entity mysql = app.createAndManageChild(DynamicToyMySqlEntityChef.specSolo());
+        log.debug("created "+mysql);
+        return mysql;
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicToyMySqlEntityChef.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicToyMySqlEntityChef.java
new file mode 100644
index 0000000..2edd8a1
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/DynamicToyMySqlEntityChef.java
@@ -0,0 +1,81 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefConfigs;
+import org.apache.brooklyn.entity.chef.ChefEntity;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Builds up a MySql entity via chef using specs only */
+public class DynamicToyMySqlEntityChef implements ChefConfig {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicToyMySqlEntityChef.class);
+
+    protected static EntitySpec<? extends Entity> specBase() {
+        EntitySpec<ChefEntity> spec = EntitySpec.create(ChefEntity.class);
+        
+        ChefConfigs.addToLaunchRunList(spec, "mysql::server");
+        spec.configure(PID_FILE, "/var/run/mysqld/mysql*.pid");
+        // init.d service name is sometimes mysql, sometimes mysqld, depending ubuntu/centos
+        // we use pid file above instead, but this (with the right name) could be used:
+//        spec.configure(SERVICE_NAME, "mysql");
+        
+        // chef mysql fails on first run but works on second if switching between server and solo modes
+        spec.configure(ChefConfig.CHEF_RUN_CONVERGE_TWICE, true);
+
+        // only used for solo, but safely ignored for knife
+        ChefConfigs.addToCookbooksFromGithub(spec, "mysql", "build-essential", "openssl");
+        // we always need dependent cookbooks set, and mysql requires password set
+        // (TODO for knife we might wish to prefer things from the server)
+        ChefConfigs.addLaunchAttributes(spec, MutableMap.of("mysql",  
+                MutableMap.of()
+                .add("server_root_password", "MyPassword")
+                .add("server_debian_password", "MyPassword")
+                .add("server_repl_password", "MyPassword")
+            ));
+        
+        return spec;
+    }
+
+    public static EntitySpec<? extends Entity> spec() {
+        EntitySpec<? extends Entity> spec = specBase();
+        log.debug("Created entity spec for MySql: "+spec);
+        return spec;
+    }
+
+    public static EntitySpec<? extends Entity> specSolo() {
+        EntitySpec<? extends Entity> spec = specBase();
+        spec.configure(ChefConfig.CHEF_MODE, ChefConfig.ChefModes.SOLO);
+        log.debug("Created entity spec for MySql: "+spec);
+        return spec;
+    }
+
+    public static EntitySpec<? extends Entity> specKnife() {
+        EntitySpec<? extends Entity> spec = specBase();
+        spec.configure(ChefConfig.CHEF_MODE, ChefConfig.ChefModes.KNIFE);
+        log.debug("Created entity spec for MySql: "+spec);
+        return spec;
+    }
+    
+}
diff --git a/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/TypedToyMySqlEntityChef.java b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/TypedToyMySqlEntityChef.java
new file mode 100644
index 0000000..c02dbc6
--- /dev/null
+++ b/software/cm/chef/src/test/java/org/apache/brooklyn/entity/chef/mysql/TypedToyMySqlEntityChef.java
@@ -0,0 +1,55 @@
+/*
+ * 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.mysql;
+
+import org.apache.brooklyn.entity.chef.ChefConfig;
+import org.apache.brooklyn.entity.chef.ChefEntityImpl;
+import org.apache.brooklyn.util.git.GithubUrls;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+/** Illustrates how to define an entity using Java as a Java class, extending ChefEntityImpl */
+public class TypedToyMySqlEntityChef extends ChefEntityImpl {
+
+    @Override
+    public void init() {
+        super.init();
+
+        String password = "p4ssw0rd";
+        
+        config().set(CHEF_COOKBOOK_PRIMARY_NAME, "mysql");
+        config().set(CHEF_COOKBOOK_URLS, ImmutableMap.of(
+            "mysql", GithubUrls.tgz("opscode-cookbooks", "mysql", "v4.0.12"),
+            "openssl", GithubUrls.tgz("opscode-cookbooks", "openssl", "v1.1.0"),
+            "mysql", GithubUrls.tgz("opscode-cookbooks", "build-essential", "v1.4.4")));
+        
+        config().set(CHEF_LAUNCH_RUN_LIST, ImmutableSet.of("mysql::server"));
+        config().set(CHEF_LAUNCH_ATTRIBUTES, ImmutableMap.<String,Object>of(
+            "mysql", ImmutableMap.of(
+                "server_root_password", password,
+                "server_repl_password", password,
+                "server_debian_password", password)));
+        
+        config().set(ChefConfig.PID_FILE, "/var/run/mysqld/mysqld.pid");
+        
+        config().set(CHEF_MODE, ChefModes.SOLO);
+    }
+
+}
diff --git a/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/brooklyn-tests.pem b/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/brooklyn-tests.pem
new file mode 100644
index 0000000..4ca4d00
--- /dev/null
+++ b/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/brooklyn-tests.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArJZcg6Y9BuPYuLWgcloS/cH6HzqQqQegW+5d7cgRZC1CCo4e
+GxVglEZXuko4N0PVK2E2khGiMEymVXWqmrHAnTSqV1COmRfxxOxGvmQRleDWMhED
+j2KoOwggwsaoHBEdrZ6NX+yv3iFiOS5fz3Klkt3kPXcXZJGiFsOtyZaSxK/m26no
+upS6ouChmk9TD8YcCQzdUGNAWwyzhiDg5DwqnnNQQUKvQyJuV/nyxwRMcpoNazca
+ghr2KalSF92sonnPzMz2gWhKxWVVbnTSafWQcWY+ov/TiyBHsLXO3/OrHaj7h+SF
+UUfWryHyqZI/eQeoWD/cPGtr8qVELIqhCq8OkQIDAQABAoIBAAS2Eg5x8kaG37qj
+Ep8sgEo3Clnh4mMK10DLL/s2s+rVJXFeUcoRelJx3SEzt3civeXyQGgaXSAOZ5f7
+n02bvpNMBb5eb5YURkBG5uN5ndVGjvJM7pjya385CJPoklw5x2Ke6mMM9fwNUz4W
+Wv0xtv1cW0emQZg4NYGDk/Hlz1nZG+y5j0esRlIiknYqdfuXtaQbVI/jGSyNXFVI
+uVkKCwq+k6HnIkjFEZdvQPRPaGMT4cMwczVr8hCIOfCJEVremEJ0GFnazr9iNEVu
+18YJzOEaqPLgVvjt59o+D+CoZS7U74WX6scCsFCkHLdnxlfNC/qkrn2SEaVyrfeP
+cqPLZJUCgYEA1qmmQzuNzWMhsOVty3EI4M2mePF0iM2P/wHc1ry9lddqdqTSGV7x
+NwzGNZ/OqN9UIR/3chyFXQuegz5VZxsrdIyxcgCmM2cDDxny3pr0Lct39gsWuc9g
+BhebUDP7PoF2g2f4AAllVR6atClwXoKfARoXlmbjfOJSB/q1zXWYNW8CgYEAzdJ/
+2o1B4iwDipKxJE2luuaMN4zqRbKdokBJYw0b1TOOcsyNwB5h0TGcJNFMuIqBeWCK
+J3wr7FTOfy3Qsbf3Vc5Le6NdaHomy7+HEwVMzyEPVNKumHqEL9U3Yheu5TVLLKMU
+rYBXXpoN/XXm9XxvRGhGu7Wu99ye37hIa4Qy+/8CgYB/JWlHdWTefJMeFibcU41w
+qh6qkEn4Jdner5nAz3Zz5G447BNN18CEXNqiNI/R0sYgALEuM9qCbDltf2RSd/Nb
+S2JnJh9LXv6e2T3TwHBVF6lsYNELKdu0gBMMhF2SflhWKSTp0KbmrgPwJoNaV4Aa
+xPunqDWiaOMxurwogDixnQKBgGgGQs5P5IOOIUARQeyKPgAHc8jDtMgDLX6KpUyl
+nHKV+yH0VpRKBsA3JabKDc+bWTLiYxDvxjdM6Y0Ht9mKlDxO5oGXoKckTaVeqMMp
+Q5fQKrKBRPMVwOd4COTP+XopBFSMG/BQ1Feg3v9CbreV65qUZWOLwOHPJJEHz7pf
+d3E7AoGBAMPkKjDuhU8YTpB6H4KlCM+9p/6yE7umoWMPJm70301XxBoQA8spkS3o
+K2s2xL7+MuppvLv0xqyZ1vtvqmUMrw/eisQWekGobEiutk+DuxegRlpUkH0s6CH4
+lypeh/FpGdLayAn84ALIEYhCsr0SnJtEL1MEO9qPKJY24Hl6ATCC
+-----END RSA PRIVATE KEY-----
diff --git a/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/brooklyn-validator.pem b/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/brooklyn-validator.pem
new file mode 100644
index 0000000..7fb694c
--- /dev/null
+++ b/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/brooklyn-validator.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA1L/awgkt+AxAHc76IeJAsZYj0ZoW8pP0Ff2SL8ZmkoGOT3C6
+d6DhQ8H194YTYNZ+w+D4K6DkNf8MqoFnQnLyQHlALd+giCE9mS1SUS0YBBdXbGh/
+320cosPqvMrT16nxbTLz7rgGNYSvgS5tnFYIADv4AzvXZMOZ20VXkDNCiqG0Pqy5
+qAMvmNGJNpDfERebdQUoZpqPJysJYXmy2cKYm5cfsDCxmL9iGyPjHazU6FizC50M
+SJ+ZGtqVEB/0d4p4u/pHAZQJCFdxJszHqBbYfQDFJso9lAdkM/11Tq5NtBH3cTPi
+SfcFIWLSS4qu1RcRmURRtsZTr/4FOKcYWpmLcwIDAQABAoIBACLzzi3YsjuxT4tW
+KNQORtCmOQZDiYea9Rvzx+OfddSWAlpcy9XBkaC/Kfjbhy1+r97ghAUu7q9MUrlk
+fyF0nwYmpXuj5MzYoTUcNAzwGqT5bLx7wp5jX7QB+fvAWuNwoSBuoZLTmvmJ7geM
+KCfqHnBjadCmMTH1zL4wez1Onp0FvRDXGPGjERQGptpejTL8PBaarzF2Ufewb/0z
+2/Icl1RAGHNkJHJjedL2mgtKHzW/JTBLcMjN1+PL4pLMriXCTP3VZZlezx8RXrnW
+DTFWe6G8gcPhYh0ni3wtoI/ENvXORg+wFXa2B/FKRObTu+vyYyvDgUSB2TI5LebX
+XsuWEEECgYEA/f9gkab9+m9JDz7AYDF4f+YzECdZwA0ntAnA5Qb/zWnjbcbtOMMF
+lNEm+m9J9GoorSpO9e/4Z61dtEsuX9Vr2tyKSnamNQT2vI+WIUIbGMWFq/VNi390
+o0jQWhEead/xu5BFbLEBD651+NBQGBCQi8slzOAL6jqP8qEGeaYaauECgYEA1m06
+wdHM3U5wwCGsyzTloGZ0LtTFO6oKYPpU/MItEESqVY1grFh5Sqa6tyJEq0jZSdKM
+eXzlqSyJQbaHd85PJnDkVnJVpdeqRj1eMHtv9+s3A8kOzSww+FL7KJHUrnNBZ7MK
+J7wdPtzYBFL124NAhbTaR6+Kv1ZSUpkGFRCV9NMCgYBtMy5BcJog4Vd3xnLO6HX2
+BvJNL53Wg9FeBhN4Y9n2Zl/xAmVa0f3ETWeEo/QXsMxsJpRsCA+0A0UWDnyRlyAX
+qFmMShaLFOc/ijvxcIpVzBX8KCp+nv12dgedsV5yBmXXTd+LK05Zf5gYsPa+YeDD
+OUO3IVv+B897cN9nzZHuAQKBgGIC44ycXUv7AsaPne/0adF7gze0wcKX4s6ZHie6
+ieaZvFIGoV2lwytAMrBq1YCFd+yqdNNDJ6bAWKzUxe8ZOkyT5YsuD8ASaB5bBqaa
+hX+I4Ei2qjFWNbwMEgllPxXOUOMZj1bCQYvuXj77vK1tvRxgojWKI5150381uvX9
+8s1JAoGBAO73ig60VNqPTHg1KDZ7SdAW0KbNiAV+xojvW9pkH5M1vCllz+lpeW1l
+jZFbnL013r5UvzRgwqIuw4t+gZikfIZgPwP8Z2BGtIzNiIHy/RLzOawsPTTXczZA
+hHI2e54G9DgCi8h4LXhFavVykTkNTP62S6za4VTEjxERdWxIBnfL
+-----END RSA PRIVATE KEY-----
\ No newline at end of file
diff --git a/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/knife.rb b/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/knife.rb
new file mode 100644
index 0000000..d52e492
--- /dev/null
+++ b/software/cm/chef/src/test/resources/org/apache/brooklyn/entity/chef/hosted-chef-brooklyn-credentials/knife.rb
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+current_dir = File.dirname(__FILE__)
+log_level                :info
+log_location             STDOUT
+node_name                "brooklyn-tests"
+client_key               "#{current_dir}/brooklyn-tests.pem"
+validation_client_name   "brooklyn-validator"
+validation_key           "#{current_dir}/brooklyn-validator.pem"
+chef_server_url          "https://api.opscode.com/organizations/brooklyn"
diff --git a/software/cm/pom.xml b/software/cm/pom.xml
index 7e3fd3b..273dd41 100644
--- a/software/cm/pom.xml
+++ b/software/cm/pom.xml
@@ -71,10 +71,9 @@
     </mailingLists>
 
     <modules>
-
+        <module>chef</module>
         <module>salt</module>
         <module>ansible</module>
-
     </modules>
 
 </project>
diff --git a/software/database/pom.xml b/software/database/pom.xml
index 7407397..614bee5 100644
--- a/software/database/pom.xml
+++ b/software/database/pom.xml
@@ -70,6 +70,11 @@
         </dependency>
         <dependency>
             <groupId>org.apache.brooklyn</groupId>
+            <artifactId>brooklyn-software-cm-chef</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.brooklyn</groupId>
             <artifactId>brooklyn-api</artifactId>
             <version>${project.version}</version>
         </dependency>
@@ -121,6 +126,13 @@
             <classifier>tests</classifier>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.brooklyn</groupId>
+            <artifactId>brooklyn-software-cm-chef</artifactId>
+            <version>${project.version}</version>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
         <!-- bring in jclouds for testing -->
         <dependency>
             <groupId>org.apache.brooklyn</groupId>