This closes #123
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltSshDriver.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityDriver.java
similarity index 64%
rename from software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltSshDriver.java
rename to software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityDriver.java
index cd9cb38..01ae9cf 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltSshDriver.java
+++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityDriver.java
@@ -7,7 +7,7 @@
  * "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
+ *      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
@@ -16,13 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.brooklyn.entity.cm.salt.impl;
 
+package org.apache.brooklyn.entity.cm.ansible;
 
-public class SaltSshDriver {
+import org.apache.brooklyn.entity.software.base.SoftwareProcessDriver;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
 
-    private SaltSshDriver() {
-        // Utility class
-    }
-
+public interface AnsibleEntityDriver extends SoftwareProcessDriver {
+    String getStatusCmd();
+    ProcessTaskWrapper<Integer> ansibleCommand(String module, String args);
 }
diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java
index a762721..21359d0 100644
--- a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java
+++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java
@@ -18,28 +18,59 @@
  */
 package org.apache.brooklyn.entity.cm.ansible;
 
-import org.apache.brooklyn.entity.stock.EffectorStartableImpl;
-import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.core.location.Locations;
+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.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
-import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import static com.google.common.base.Preconditions.checkNotNull;
+public class AnsibleEntityImpl extends SoftwareProcessImpl implements AnsibleEntity {
 
-public class AnsibleEntityImpl extends EffectorStartableImpl implements AnsibleEntity {
+    private static final Logger LOG = LoggerFactory.getLogger(AnsibleEntityImpl.class);
 
-    private AnsibleLifecycleEffectorTasks lifecycleTasks;
+    private SshFeed feed;
 
     @Override
-    public void init() {
-        checkNotNull(getConfig(SERVICE_NAME), "service name is missing. it has to be provided by the user");
-        String playbookName = getConfig(ANSIBLE_PLAYBOOK);
-        if (!Strings.isBlank(playbookName)) setDefaultDisplayName(playbookName + " (ansible)");
+    public Class getDriverInterface() {
+        return AnsibleEntityDriver.class;
+    }
 
-        super.init();
+    @Override
+    public AnsibleEntityDriver getDriver() {
+        return (AnsibleEntityDriver) super.getDriver();
+    }
 
-        lifecycleTasks = new AnsibleLifecycleEffectorTasks();
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
 
-        lifecycleTasks.attachLifecycleEffectors(this);
+        Maybe<SshMachineLocation> machine = Locations.findUniqueSshMachineLocation(getLocations());
+
+        if (machine.isPresent()) {
+            String cmd = getDriver().getStatusCmd();
+            feed = SshFeed.builder()
+                    .entity(this)
+                    .period(config().get(SERVICE_PROCESS_IS_RUNNING_POLL_PERIOD))
+                    .machine(machine.get())
+                    .poll(new SshPollConfig<Boolean>(SERVICE_UP)
+                            .command(cmd)
+                            .setOnSuccess(true)
+                            .setOnFailureOrException(false))
+                    .build();
+        } else {
+            LOG.warn("Location(s) {} not an ssh-machine location, so not polling for status; setting serviceUp immediately", getLocations());
+            sensors().set(SERVICE_UP, true);
+        }
+    }
+
+    @Override
+    protected void disconnectSensors() {
+        if (feed != null) feed.stop();
+        super.disconnectSensors();
     }
 
     @Override
@@ -49,14 +80,15 @@
 
     @Override
     public String ansibleCommand(String module, String args) {
-        final ProcessTaskWrapper<Integer> command = DynamicTasks.queue(
-            AnsiblePlaybookTasks.moduleCommand(module, config().get(ANSIBLE_VARS), lifecycleTasks.getRunDir(), args));
+        final ProcessTaskWrapper<Integer> command = getDriver().ansibleCommand(module, args);
+
         command.asTask().blockUntilEnded();
+
         if (0 == command.getExitCode()) {
             return command.getStdout();
         } else {
             throw new RuntimeException("Command (" + args + ") in module " + module
-                + " failed with stderr:\n" + command.getStderr() + "\n");
+                    + " failed with stderr:\n" + command.getStderr() + "\n");
         }
     }
 }
diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntitySshDriver.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntitySshDriver.java
new file mode 100644
index 0000000..8846670
--- /dev/null
+++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntitySshDriver.java
@@ -0,0 +1,123 @@
+/*
+ * 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.cm.ansible;
+
+import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
+
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.text.Strings;
+
+public class AnsibleEntitySshDriver extends AbstractSoftwareProcessSshDriver implements AnsibleEntityDriver {
+    public AnsibleEntitySshDriver(AnsibleEntityImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    @Override
+    public boolean isRunning() {
+        return newScript(MutableMap.of("usePidFile", false), CHECK_RUNNING)
+                .body.append(getStatusCmd())
+                .execute() == 0;
+    }
+
+    @Override
+    public void stop() {
+        final String serviceName = getEntity().config().get(AnsibleConfig.SERVICE_NAME);
+
+        newScript(MutableMap.of("usePidFile", false), STOPPING)
+                .updateTaskAndFailOnNonZeroResultCode()
+                .body.append(sudo(String.format(getEntity().config().get(AnsibleConfig.ANSIBLE_SERVICE_STOP), serviceName)))
+                .execute();
+    }
+
+    @Override
+    public void install() {
+        Object extraVars = getEntity().config().get(AnsibleConfig.ANSIBLE_VARS);
+        String playbookName = getEntity().config().get(AnsibleConfig.ANSIBLE_PLAYBOOK);
+        String playbookUrl = getEntity().config().get(AnsibleConfig.ANSIBLE_PLAYBOOK_URL);
+        String playbookYaml = getEntity().config().get(AnsibleConfig.ANSIBLE_PLAYBOOK_YAML);
+
+        if (playbookUrl != null && playbookYaml != null) {
+            throw new IllegalArgumentException( "You can not specify both "+  AnsibleConfig.ANSIBLE_PLAYBOOK_URL.getName() +
+                    " and " + AnsibleConfig.ANSIBLE_PLAYBOOK_YAML.getName() + " as arguments.");
+        }
+
+        if (playbookUrl == null && playbookYaml == null) {
+            throw new IllegalArgumentException("You have to specify either " + AnsibleConfig.ANSIBLE_PLAYBOOK_URL.getName() +
+                    " or " + AnsibleConfig.ANSIBLE_PLAYBOOK_YAML.getName() + " as arguments.");
+        }
+
+        DynamicTasks.queue(AnsiblePlaybookTasks.installAnsible(getInstallDir(), false));
+        DynamicTasks.queue(AnsiblePlaybookTasks.setUpHostsFile(false));
+
+        if (extraVars != null) {
+            DynamicTasks.queue(AnsiblePlaybookTasks.configureExtraVars(getRunDir(), extraVars, false));
+        }
+
+        if (Strings.isNonBlank(playbookUrl)) {
+            DynamicTasks.queue(AnsiblePlaybookTasks.installPlaybook(getRunDir(), playbookName, playbookUrl));
+        }
+
+        if (Strings.isNonBlank(playbookYaml)) {
+            DynamicTasks.queue(AnsiblePlaybookTasks.buildPlaybookFile(getRunDir(), playbookName));
+        }
+        DynamicTasks.queue(AnsiblePlaybookTasks.runAnsible(getRunDir(), extraVars, playbookName));
+    }
+
+    @Override
+    public void customize() {
+        newScript(CUSTOMIZING).execute();
+    }
+
+    @Override
+    public void launch() {
+        final String serviceName = getEntity().config().get(AnsibleConfig.SERVICE_NAME);
+
+        newScript(MutableMap.of("usePidFile", false), LAUNCHING)
+                .updateTaskAndFailOnNonZeroResultCode()
+                .body.append(sudo(String.format(getEntity().config().get(AnsibleConfig.ANSIBLE_SERVICE_START), serviceName)))
+                .execute();
+    }
+
+    @Override
+    public String getStatusCmd() {
+        String serviceNameCheck = getEntity().config().get(AnsibleConfig.SERVICE_NAME).replaceFirst("^(.)(.*)", "[$1]$2");
+        String statusCmd = String.format("ps -ef | grep %s", serviceNameCheck);
+
+        Integer serviceCheckPort = getEntity().config().get(AnsibleConfig.ANSIBLE_SERVICE_CHECK_PORT);
+
+        if (serviceCheckPort != null) {
+            statusCmd = sudo(String.format("ansible localhost -c local -m wait_for -a \"host=" +
+                    getEntity().config().get(AnsibleConfig.ANSIBLE_SERVICE_CHECK_HOST) +
+                    "\" port=%d\"", serviceCheckPort));
+        }
+
+        return statusCmd;
+    }
+
+    @Override
+    public ProcessTaskWrapper<Integer> ansibleCommand(String module, String args) {
+        return DynamicTasks.queue(
+                AnsiblePlaybookTasks.moduleCommand(module, getEntity().config().get(AnsibleConfig.ANSIBLE_VARS), getRunDir(), args));
+    }
+}
diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java
deleted file mode 100644
index b2a104a..0000000
--- a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * 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.cm.ansible;
-
-import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
-
-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.entity.trait.Startable;
-import org.apache.brooklyn.core.location.Locations;
-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.feed.ssh.SshFeed;
-import org.apache.brooklyn.feed.ssh.SshPollConfig;
-import org.apache.brooklyn.location.ssh.SshMachineLocation;
-import org.apache.brooklyn.util.core.task.DynamicTasks;
-import org.apache.brooklyn.util.guava.Maybe;
-import org.apache.brooklyn.util.net.Urls;
-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.base.Supplier;
-
-public class AnsibleLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements AnsibleConfig {
-
-    private static final Logger LOG = LoggerFactory.getLogger(AnsibleLifecycleEffectorTasks.class);
-
-    protected String serviceName;
-    protected SshFeed serviceSshFeed;
-
-    protected Object extraVars;
-    protected String baseDir;
-    protected String runDir;
-
-    public AnsibleLifecycleEffectorTasks() {
-    }
-
-    public String getServiceName() {
-        if (serviceName!=null) return serviceName;
-        return serviceName = entity().config().get(AnsibleConfig.SERVICE_NAME);
-    }
-
-    public Object getExtraVars() {
-        if (extraVars != null) return extraVars;
-        return extraVars = entity().config().get(ANSIBLE_VARS);
-    }
-
-    public String getBaseDir() {
-        if (null != baseDir) return baseDir;
-        return baseDir = MachineLifecycleEffectorTasks.resolveOnBoxDir(entity(),
-                Machines.findUniqueMachineLocation(entity().getLocations(), SshMachineLocation.class).get());
-    }
-
-    public String getRunDir() {
-        if (null != runDir) return runDir;
-        return runDir = Urls.mergePaths(getBaseDir(), "apps/"+entity().getApplicationId()+"/ansible/playbooks/"
-            +entity().getEntityType().getSimpleName()+"_"+entity().getId());
-    }
-
-    @Override
-    public void attachLifecycleEffectors(Entity entity) {
-        if (getServiceName()==null && getClass().equals(AnsibleLifecycleEffectorTasks.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);
-    }
-
-    @Override
-    protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
-        startWithAnsibleAsync();
-
-        return "ansible start tasks submitted";
-    }
-
-    protected String getPlaybookName() {
-        return entity().config().get(ANSIBLE_PLAYBOOK);
-    }
-
-    protected void startWithAnsibleAsync() {
-        
-        String installDir = Urls.mergePaths(getBaseDir(), "installs/ansible");
-
-        String playbookUrl = entity().config().get(ANSIBLE_PLAYBOOK_URL);
-        String playbookYaml = entity().config().get(ANSIBLE_PLAYBOOK_YAML);
-
-        if (playbookUrl != null && playbookYaml != null) {
-            throw new IllegalArgumentException( "You can not specify both "+  AnsibleConfig.ANSIBLE_PLAYBOOK_URL.getName() + 
-                " and " + AnsibleConfig.ANSIBLE_PLAYBOOK_YAML.getName() + " as arguments.");
-        }
-
-        if (playbookUrl == null && playbookYaml == null) { 
-                throw new IllegalArgumentException("You have to specify either " + AnsibleConfig.ANSIBLE_PLAYBOOK_URL.getName() + 
-                " or " + AnsibleConfig.ANSIBLE_PLAYBOOK_YAML.getName() + " as arguments.");
-        }
-
-        DynamicTasks.queue(AnsiblePlaybookTasks.installAnsible(installDir, false));
-        DynamicTasks.queue(AnsiblePlaybookTasks.setUpHostsFile(false));
-
-        if (getExtraVars() != null) {
-            DynamicTasks.queue(AnsiblePlaybookTasks.configureExtraVars(getRunDir(), extraVars, false));
-        }
-
-        if (Strings.isNonBlank(playbookUrl)) {
-            DynamicTasks.queue(AnsiblePlaybookTasks.installPlaybook(getRunDir(), getPlaybookName(), playbookUrl));
-        }
-
-        if (Strings.isNonBlank(playbookYaml)) {
-            DynamicTasks.queue(AnsiblePlaybookTasks.buildPlaybookFile(getRunDir(), getPlaybookName()));
-        }
-        DynamicTasks.queue(AnsiblePlaybookTasks.runAnsible(getRunDir(), getExtraVars(), getPlaybookName()));
-    }
-
-
-    @Override
-    protected void postStartCustom() {
-        boolean result = false;
-        result |= tryCheckStartService();
-
-        if (!result) {
-            LOG.warn("No way to check whether "+entity()+" is running; assuming yes");
-        }
-        entity().sensors().set(SoftwareProcess.SERVICE_UP, true);
-        
-        Maybe<SshMachineLocation> machine = Locations.findUniqueSshMachineLocation(entity().getLocations());
-
-        if (machine.isPresent()) {
-            // For example “ps -f| grep httpd” matches for any process including the text “httpd”,
-            // which includes the grep command itself, whereas “ps | grep [h]ttpd” matches only processes
-            // including the text “httpd” (doesn’t include the grep) and additionally 
-            // provides a correct return code
-            //
-            // The command constructed bellow will look like  - ps -ef |grep [h]ttpd
-            String serviceNameCheck = getServiceName().replaceFirst("^(.)(.*)", "[$1]$2");
-            String checkCmd = String.format("ps -ef | grep %s", serviceNameCheck);
-
-            Integer serviceCheckPort = entity().config().get(ANSIBLE_SERVICE_CHECK_PORT);
-
-            if (serviceCheckPort != null) {
-                checkCmd = sudo(String.format("ansible localhost -c local -m wait_for -a \"host=" + 
-                                        entity().config().get(ANSIBLE_SERVICE_CHECK_HOST) + 
-                                        "\" port=%d\"", serviceCheckPort));
-            }
-            serviceSshFeed = SshFeed.builder()
-                    .entity(entity())
-                    .period(Duration.ONE_MINUTE)
-                    .machine(machine.get())
-                    .poll(new SshPollConfig<Boolean>(Startable.SERVICE_UP)
-                            .command(checkCmd)
-                            .setOnSuccess(true)
-                            .setOnFailureOrException(false))
-                    .build();
-                    
-             entity().feeds().add(serviceSshFeed);
-        } else {
-            LOG.warn("Location(s) {} not an ssh-machine location, so not polling for status; "
-                    + "setting serviceUp immediately", entity().getLocations());
-        }
-        super.postStartCustom();
-    }
-
-    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);
-        int result = DynamicTasks.queue(SshEffectorTasks.ssh(sudo(getServiveStartCommand()))).get();
-        if (0 != result) {
-            throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+getServiceName()+")");
-        }
-
-        return true;
-    }
-
-    @Override
-    protected String stopProcessesAtMachine() {
-        boolean result = false;
-        result |= tryStopService();
-        if (!result) {
-            throw new IllegalStateException("The process for "+entity()+" could not be stopped (no impl!)");
-        }
-        return "stopped";
-    }
-
-    @Override
-    protected StopMachineDetails<Integer> stopAnyProvisionedMachines() {
-        return super.stopAnyProvisionedMachines();
-    }
-
-    protected boolean tryStopService() {
-        if (getServiceName()==null) return false;
-        int result = DynamicTasks.queue(SshEffectorTasks.ssh(sudo(getServiveStopCommand()))).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)");
-    }
-
-    private String getServiveStartCommand() {
-        return String.format(entity().config().get(AnsibleConfig.ANSIBLE_SERVICE_START), getServiceName());
-    }
-
-    private String getServiveStopCommand() {
-        return String.format(entity().config().get(AnsibleConfig.ANSIBLE_SERVICE_STOP), getServiceName());
-    }
-}
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntity.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntity.java
index 4428658..e308a20 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntity.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntity.java
@@ -23,7 +23,6 @@
 import org.apache.brooklyn.api.catalog.Catalog;
 import org.apache.brooklyn.api.entity.ImplementedBy;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
-import org.apache.brooklyn.entity.cm.salt.impl.SaltEntityImpl;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.annotation.Effector;
 import org.apache.brooklyn.core.annotation.EffectorParam;
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityDriver.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityDriver.java
index 19a88ed..a435991 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityDriver.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityDriver.java
@@ -19,7 +19,8 @@
 package org.apache.brooklyn.entity.cm.salt;
 
 import org.apache.brooklyn.entity.software.base.SoftwareProcessDriver;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
 
 public interface SaltEntityDriver extends SoftwareProcessDriver {
-
+    ProcessTaskWrapper<Integer> saltCall(String spec);
 }
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltEntityImpl.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityImpl.java
similarity index 79%
rename from software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltEntityImpl.java
rename to software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityImpl.java
index bea2581..e93ebe7 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltEntityImpl.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntityImpl.java
@@ -7,7 +7,7 @@
  * "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
+ *      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
@@ -16,25 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.brooklyn.entity.cm.salt.impl;
+package org.apache.brooklyn.entity.cm.salt;
 
-import com.google.common.annotations.Beta;
-import org.apache.brooklyn.entity.cm.salt.SaltConfig;
-import org.apache.brooklyn.entity.cm.salt.SaltEntity;
-import org.apache.brooklyn.entity.stock.EffectorStartableImpl;
-import org.apache.brooklyn.util.core.task.DynamicTasks;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Set;
+import com.google.common.annotations.Beta;
 
 @Beta
-public class SaltEntityImpl extends EffectorStartableImpl implements SaltEntity {
+public class SaltEntityImpl extends SoftwareProcessImpl implements SaltEntity {
     private static final Logger LOG = LoggerFactory.getLogger(SaltEntityImpl.class);
 
-    public SaltEntityImpl() {
-        super();
+    @Override
+    public Class getDriverInterface() {
+        return SaltEntityDriver.class;
+    }
+
+    @Override
+    public SaltEntityDriver getDriver() {
+        return (SaltEntityDriver) super.getDriver();
     }
 
     @Override
@@ -60,7 +64,6 @@
 
         SaltConfig.SaltMode mode = getConfig(SaltConfig.SALT_MODE);
         LOG.debug("Initialize SaltStack {} mode", mode.name());
-        new SaltLifecycleEffectorTasks().attachLifecycleEffectors(this);
     }
 
     @Override
@@ -70,8 +73,10 @@
 
     @Override
     public String saltCall(String spec) {
-        final ProcessTaskWrapper<Integer> command = DynamicTasks.queue(SaltSshTasks.saltCall(spec));
+        final ProcessTaskWrapper<Integer> command = getDriver().saltCall(spec);
+
         command.asTask().blockUntilEnded();
+
         if (0 == command.getExitCode()) {
             return command.getStdout();
         } else {
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntitySshDriver.java
similarity index 68%
rename from software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java
rename to software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntitySshDriver.java
index 2051ed6..dac9756 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltLifecycleEffectorTasks.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltEntitySshDriver.java
@@ -7,7 +7,7 @@
  * "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
+ *      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
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.brooklyn.entity.cm.salt.impl;
+package org.apache.brooklyn.entity.cm.salt;
 
 import static java.util.regex.Pattern.DOTALL;
 import static java.util.regex.Pattern.MULTILINE;
@@ -28,8 +28,6 @@
 import java.util.regex.Pattern;
 
 import org.apache.brooklyn.api.effector.Effector;
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.location.MachineLocation;
 import org.apache.brooklyn.api.mgmt.TaskAdaptable;
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
 import org.apache.brooklyn.core.effector.Effectors;
@@ -37,10 +35,9 @@
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
 import org.apache.brooklyn.core.entity.trait.Startable;
-import org.apache.brooklyn.entity.cm.salt.SaltConfig;
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
-import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
-import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.task.DynamicTasks;
@@ -51,43 +48,35 @@
 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.ImmutableMap;
 
-@Beta
-public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements SaltConfig {
-    private static final Logger LOG = LoggerFactory.getLogger(SaltLifecycleEffectorTasks.class);
+// TODO: does this belong to the _.impl package?
+public class SaltEntitySshDriver extends AbstractSoftwareProcessSshDriver implements SaltEntityDriver {
+    private static final Logger LOG = LoggerFactory.getLogger(SaltEntitySshDriver.class);
+    private static final Pattern FAILURES = Pattern.compile(".*^Failed:\\s+(\\d+)$.*", MULTILINE | DOTALL);
+    private static final String ZERO = "0";
+
+    public SaltEntitySshDriver(SaltEntityImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
 
     @Override
-    protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
-        SaltMode mode = detectSaltMode(entity());
-        final MachineLocation machine = machineS.get();
-        LOG.info("Starting salt in '{}' mode at '{}'", mode, machine.getDisplayName());
-        if (mode == SaltMode.MASTERLESS) {
-            startWithSshAsync();
-        } else {
+    public void install() {
+        SaltConfig.SaltMode mode = getEntity().config().get(SaltConfig.SALT_MODE);
+        Preconditions.checkNotNull(mode, "Required config " + SaltConfig.SALT_MODE + " not provided for entity: " + entity);
+        LOG.info("Starting salt in '{}' mode at '{}'", mode, getMachine().getDisplayName());
+
+        if (mode != SaltConfig.SaltMode.MASTERLESS) {
             // TODO: implement MASTER and MINION
             throw new IllegalStateException("Unknown salt mode: " + mode.name());
         }
-        return "salt tasks submitted (" + mode + ")";
-    }
 
-
-    protected static SaltMode detectSaltMode(Entity entity) {
-        SaltMode mode = entity.getConfig(SaltConfig.SALT_MODE);
-        Preconditions.checkNotNull(mode, "Required config " + SaltConfig.SALT_MODE + " not provided for entity: " + entity);
-        return mode;
-    }
-
-    protected void startWithSshAsync() {
-
-        final Set<? extends String> startStates = entity().getConfig(SaltConfig.START_STATES);
-        final Set<? extends String> formulas = entity().getConfig(SaltConfig.SALT_FORMULAS);
-        final Set<? extends String> pillars = entity().getConfig(SaltConfig.SALT_PILLARS);
-        final Set<? extends String> pillarUrls = entity().getConfig(SaltConfig.SALT_PILLAR_URLS);
-        final String entityId = entity().getConfig(BrooklynCampConstants.PLAN_ID);
+        final Set<? extends String> startStates = getEntity().config().get(SaltConfig.START_STATES);
+        final Set<? extends String> formulas = getEntity().config().get(SaltConfig.SALT_FORMULAS);
+        final Set<? extends String> pillars = getEntity().config().get(SaltConfig.SALT_PILLARS);
+        final Set<? extends String> pillarUrls = getEntity().config().get(SaltConfig.SALT_PILLAR_URLS);
+        final String entityId = getEntity().config().get(BrooklynCampConstants.PLAN_ID);
 
         final ProcessTaskWrapper<Integer> installedAlready = queueAndBlock(SaltSshTasks.isSaltInstalled(false));
 
@@ -96,10 +85,10 @@
                 @Override
                 public void run() {
                     DynamicTasks.queue(
-                        SaltSshTasks.installSalt(false),
-                        SaltSshTasks.installSaltUtilities(false),
-                        SaltSshTasks.configureForMasterlessOperation(false),
-                        SaltSshTasks.installTopFile(startStates, false));
+                            SaltSshTasks.installSalt(false),
+                            SaltSshTasks.installSaltUtilities(false),
+                            SaltSshTasks.configureForMasterlessOperation(false),
+                            SaltSshTasks.installTopFile(startStates, false));
 
                     if (Strings.isNonBlank(entityId)) {
                         DynamicTasks.queue(SaltSshTasks.setMinionId(entityId));
@@ -109,18 +98,17 @@
                 }
             });
         }
-
-        startSalt();
-
-        connectSensors();
     }
 
+    @Override
+    public void customize() {
+        newScript(CUSTOMIZING).execute();
+    }
 
-    private static final Pattern FAILURES = Pattern.compile(".*^Failed:\\s+(\\d+)$.*", MULTILINE | DOTALL);
-    private static final String ZERO = "0";
-
-    private void startSalt() {
+    @Override
+    public void launch() {
         String name = "apply top states";
+
         final ProcessTaskWrapper<Integer> topStates = queueAndBlock(SaltSshTasks.applyTopStates(false).summary(name));
 
         // Salt apply returns exit code 0 even upon failure so check the stdout.
@@ -128,10 +116,70 @@
         if (!failCount.matches() || !ZERO.equals(failCount.group(1))) {
             LOG.warn("Encountered error in applying Salt top states: {}", topStates.getStdout());
             throw new RuntimeException(
-                "Encountered error in applying Salt top states, see '" + name + "' in activities for details");
+                    "Encountered error in applying Salt top states, see '" + name + "' in activities for details");
         }
     }
 
+    @Override
+    public void runPostLaunchCommand() {
+        super.runPostLaunchCommand();
+
+        final ProcessTaskWrapper<String> retrieveHighstate = SaltSshTasks.retrieveHighstate();
+        final ProcessTaskWrapper<String> highstate = DynamicTasks.queue(retrieveHighstate).block();
+        String stateDescription = highstate.get();
+
+        SaltHighstate.applyHighstate(stateDescription, getEntity());
+
+        getEntity().sensors().set(SoftwareProcess.SERVICE_UP, true);
+    }
+
+    @Override
+    public boolean isRunning() {
+        return true;
+    }
+
+    @Override
+    public void stop() {
+        final Set<? extends String> stopStates = getEntity().config().get(SaltConfig.STOP_STATES);
+        LOG.debug("Executing Salt stopProcessesAtMachine with states {}", stopStates);
+        if (stopStates.isEmpty()) {
+            stopBasedOnStartStates();
+        } else {
+            applyStates(stopStates);
+        }
+    }
+
+    @Override
+    public void restart() {
+        ServiceStateLogic.setExpectedState(getEntity(), Lifecycle.STOPPING);
+
+        try {
+            final Set<? extends String> restartStates = getEntity().config().get(SaltConfig.RESTART_STATES);
+            LOG.debug("Executing Salt restart with states {}", restartStates);
+            if (restartStates.isEmpty()) {
+                restartBasedOnStartStates();
+            } else {
+                applyStates(restartStates);
+            }
+            ServiceStateLogic.setExpectedState(getEntity(), Lifecycle.RUNNING);
+        } catch (Exception e) {
+            getEntity().sensors().set(ServiceStateLogic.SERVICE_NOT_UP_DIAGNOSTICS,
+                    ImmutableMap.<String, Object>of("restart", e.getMessage()));
+            ServiceStateLogic.setExpectedState(getEntity(), Lifecycle.ON_FIRE);
+        }
+    }
+
+    @Override
+    public ProcessTaskWrapper<Integer> saltCall(String spec) {
+        return DynamicTasks.queue(SaltSshTasks.saltCall(spec));
+    }
+
+    private ProcessTaskWrapper<Integer> queueAndBlock(ProcessTaskFactory<Integer> taskFactory) {
+        final ProcessTaskWrapper<Integer> queued = DynamicTasks.queue(taskFactory);
+        queued.asTask().blockUntilEnded();
+        return queued;
+    }
+
     private void installFormulas(Set<? extends String> formulas) {
         if (formulas.size() > 0) {
             DynamicTasks.queue(SaltSshTasks.enableFileRoots(false));
@@ -148,7 +196,7 @@
         if (pillarUrls.size() > 0) {
             final TaskBuilder<Object> pillarTasks = TaskBuilder.builder().displayName("install pillars");
             pillarTasks.add(SaltSshTasks.invokeSaltUtility("init_pillar_config", null, false)
-                .summary("init pillar config").newTask());
+                    .summary("init pillar config").newTask());
             for (String pillar : pillars) {
                 pillarTasks.add(SaltSshTasks.addPillarToTop(pillar, false).newTask());
             }
@@ -159,32 +207,12 @@
         }
     }
 
-    private void connectSensors() {
-        final ProcessTaskWrapper<String> retrieveHighstate = SaltSshTasks.retrieveHighstate();
-        final ProcessTaskWrapper<String> highstate = DynamicTasks.queue(retrieveHighstate).block();
-        String stateDescription = highstate.get();
-
-        SaltHighstate.applyHighstate(stateDescription, entity());
-    }
-
-    @Override
-    protected void postStartCustom() {
-        // TODO: check for package installed?
-        entity().sensors().set(SoftwareProcess.SERVICE_UP, true);
-        super.postStartCustom();
-    }
-
-
-    @Override
-    protected String stopProcessesAtMachine() {
-        final Set<? extends String> stopStates = entity().getConfig(SaltConfig.STOP_STATES);
-        LOG.debug("Executing Salt stopProcessesAtMachine with states {}", stopStates);
-        if (stopStates.isEmpty()) {
-            stopBasedOnStartStates();
-        } else {
-            applyStates(stopStates);
+    private MutableSet<String> addSuffix(Set<? extends String> names, String suffix) {
+        final MutableSet<String> suffixed = MutableSet.of();
+        for (String name : names) {
+            suffixed.add(name + suffix);
         }
-        return null;
+        return suffixed;
     }
 
     private void applyStates(Set<? extends String> states) {
@@ -194,10 +222,10 @@
     }
 
     private void stopBasedOnStartStates() {
-        final Set<? extends String> startStates = entity().getConfig(SaltConfig.START_STATES);
+        final Set<? extends String> startStates = getEntity().config().get(SaltConfig.START_STATES);
         final MutableSet<String> stopStates = addSuffix(startStates, ".stop");
         final ProcessTaskWrapper<Integer> checkStops =
-            queueAndBlock(SaltSshTasks.verifyStates(stopStates, false).summary("check stop states"));
+                queueAndBlock(SaltSshTasks.verifyStates(stopStates, false).summary("check stop states"));
         if (0 != checkStops.getExitCode()) {
             throw new RuntimeException("No stop_states configured and not all start_states have matching stop states");
         } else {
@@ -205,31 +233,11 @@
         }
     }
 
-    @Override
-    public void restart(ConfigBag parameters) {
-        ServiceStateLogic.setExpectedState(entity(), Lifecycle.STOPPING);
-
-        try {
-            final Set<? extends String> restartStates = entity().getConfig(SaltConfig.RESTART_STATES);
-            LOG.debug("Executing Salt restart with states {}", restartStates);
-            if (restartStates.isEmpty()) {
-                restartBasedOnStartStates();
-            } else {
-                applyStates(restartStates);
-            }
-            ServiceStateLogic.setExpectedState(entity(), Lifecycle.RUNNING);
-        } catch (Exception e) {
-            entity().sensors().set(ServiceStateLogic.SERVICE_NOT_UP_DIAGNOSTICS,
-                ImmutableMap.<String, Object>of("restart", e.getMessage()));
-            ServiceStateLogic.setExpectedState(entity(), Lifecycle.ON_FIRE);
-        }
-    }
-
     private void restartBasedOnStartStates() {
-        final Set<? extends String> startStates = entity().getConfig(SaltConfig.START_STATES);
+        final Set<? extends String> startStates = getEntity().config().get(SaltConfig.START_STATES);
         final MutableSet<String> restartStates = addSuffix(startStates, ".restart");
         final ProcessTaskWrapper<Integer> queued =
-            queueAndBlock(SaltSshTasks.findStates(restartStates, false).summary("check restart states"));
+                queueAndBlock(SaltSshTasks.findStates(restartStates, false).summary("check restart states"));
         final String stdout = queued.getStdout();
         String[] foundStates = Strings.isNonBlank(stdout) ? stdout.split("\\n") : null;
 
@@ -248,28 +256,14 @@
             // (and in that effector we'd fail if stop was not well-defined)
             LOG.debug("No stop states available, invoking stop and start effectors");
             invokeEffector(Startable.STOP, ConfigBag.newInstance()
-                .configure(StopSoftwareParameters.STOP_PROCESS_MODE, ALWAYS)
-                .configure(StopSoftwareParameters.STOP_MACHINE_MODE, NEVER));
+                    .configure(SoftwareProcess.StopSoftwareParameters.STOP_PROCESS_MODE, ALWAYS)
+                    .configure(SoftwareProcess.StopSoftwareParameters.STOP_MACHINE_MODE, NEVER));
             invokeEffector(Startable.START, ConfigBag.EMPTY);
         }
     }
 
-    private ProcessTaskWrapper<Integer> queueAndBlock(ProcessTaskFactory<Integer> taskFactory) {
-        final ProcessTaskWrapper<Integer> queued = DynamicTasks.queue(taskFactory);
-        queued.asTask().blockUntilEnded();
-        return queued;
-    }
-
     private void invokeEffector(Effector<Void> effector, ConfigBag config) {
-        final TaskAdaptable<Void> stop = Entities.submit(entity(), Effectors.invocation(entity(), effector, config));
+        final TaskAdaptable<Void> stop = Entities.submit(getEntity(), Effectors.invocation(getEntity(), effector, config));
         stop.asTask().blockUntilEnded();
     }
-
-    private MutableSet<String> addSuffix(Set<? extends String> names, String suffix) {
-        final MutableSet<String> suffixed = MutableSet.of();
-        for (String name : names) {
-            suffixed.add(name + suffix);
-        }
-        return suffixed;
-    }
 }
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltHighstate.java
similarity index 97%
rename from software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java
rename to software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltHighstate.java
index fdae93b..16a4224 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltHighstate.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltHighstate.java
@@ -7,7 +7,7 @@
  * "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
+ *      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
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.brooklyn.entity.cm.salt.impl;
+package org.apache.brooklyn.entity.cm.salt;
 
 import com.google.common.reflect.TypeToken;
 
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltSshTasks.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltSshTasks.java
similarity index 98%
rename from software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltSshTasks.java
rename to software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltSshTasks.java
index a8e1af6..03daf8e 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltSshTasks.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltSshTasks.java
@@ -7,7 +7,7 @@
  * "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
+ *      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
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.brooklyn.entity.cm.salt.impl;
+package org.apache.brooklyn.entity.cm.salt;
 
 import org.apache.brooklyn.api.mgmt.TaskAdaptable;
 import org.apache.brooklyn.api.mgmt.TaskFactory;
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltUtils.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltUtils.java
similarity index 93%
rename from software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltUtils.java
rename to software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltUtils.java
index b11c06e..fc18a86 100644
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltUtils.java
+++ b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/SaltUtils.java
@@ -7,7 +7,7 @@
  * "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
+ *      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
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.brooklyn.entity.cm.salt.impl;
+package org.apache.brooklyn.entity.cm.salt;
 
 import org.apache.brooklyn.api.location.LocationDefinition;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
diff --git a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltEntitySshDriver.java b/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltEntitySshDriver.java
deleted file mode 100644
index 7531c27..0000000
--- a/software/cm/salt/src/main/java/org/apache/brooklyn/entity/cm/salt/impl/SaltEntitySshDriver.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.cm.salt.impl;
-
-import org.apache.brooklyn.entity.cm.salt.SaltEntityDriver;
-import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
-import org.apache.brooklyn.location.ssh.SshMachineLocation;
-
-// TODO: does this belong to the _.impl package?
-public class SaltEntitySshDriver extends AbstractSoftwareProcessSshDriver implements SaltEntityDriver {
-
-    public SaltEntitySshDriver(SaltEntityImpl entity, SshMachineLocation machine) {
-        super(entity, machine);
-    }
-
-    @Override
-    public SaltEntityImpl getEntity() {
-        return (SaltEntityImpl) super.getEntity();
-    }
-
-    @Override
-    public void install() {
-    }
-
-    @Override
-    public void customize() {
-    }
-
-    @Override
-    public void launch() {
-    }
-
-    @Override
-    public boolean isRunning() {
-        return true;
-    }
-
-    @Override
-    public void stop() {
-    }
-
-}
diff --git a/software/cm/salt/src/test/java/org/apache/brooklyn/entity/cm/salt/HighstateTest.java b/software/cm/salt/src/test/java/org/apache/brooklyn/entity/cm/salt/HighstateTest.java
index 50205e0..e5d130a 100644
--- a/software/cm/salt/src/test/java/org/apache/brooklyn/entity/cm/salt/HighstateTest.java
+++ b/software/cm/salt/src/test/java/org/apache/brooklyn/entity/cm/salt/HighstateTest.java
@@ -20,7 +20,6 @@
 
 import com.google.common.collect.ImmutableSet;
 import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.entity.cm.salt.impl.SaltHighstate;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.test.entity.TestApplication;