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

[4/6] incubator-brooklyn git commit: package rename to org.apache.brooklyn: sandbox

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltConfigs.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltConfigs.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltConfigs.java
deleted file mode 100644
index 2de0e5d..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltConfigs.java
+++ /dev/null
@@ -1,89 +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 brooklyn.entity.salt;
-
-import java.util.Map;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.Entity;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.entity.proxying.EntitySpec;
-import brooklyn.event.basic.MapConfigKey.MapModifications;
-import brooklyn.event.basic.SetConfigKey.SetModifications;
-
-import com.google.common.annotations.Beta;
-import com.google.common.base.Preconditions;
-
-/**
- * Conveniences for configuring brooklyn Salt entities 
- *
- * @since 0.6.0
- */
-@Beta
-public class SaltConfigs {
-
-    public static void addToRunList(EntitySpec<?> entity, String...states) {
-        for (String state : states) {
-            entity.configure(SaltConfig.SALT_RUN_LIST, SetModifications.addItem(state));
-        }
-    }
-
-    public static void addToRunList(EntityInternal entity, String...states) {
-        for (String state : states) {
-            entity.setConfig(SaltConfig.SALT_RUN_LIST, SetModifications.addItem(state));
-        }
-    }
-
-    public static void addToFormuals(EntitySpec<?> entity, String formulaName, String formulaUrl) {
-        entity.configure(SaltConfig.SALT_FORMULAS.subKey(formulaName), formulaUrl);
-    }
-
-    public static void addToFormulas(EntityInternal entity, String formulaName, String formulaUrl) {
-        entity.setConfig(SaltConfig.SALT_FORMULAS.subKey(formulaName), formulaUrl);
-    }
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    public static void addLaunchAttributes(EntitySpec<?> entity, Map<? extends Object,? extends Object> attributesMap) {
-        entity.configure(SaltConfig.SALT_LAUNCH_ATTRIBUTES, MapModifications.add((Map)attributesMap));
-    }
-    
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    public static void addLaunchAttributes(EntityInternal entity, Map<? extends Object,? extends Object> attributesMap) {
-        entity.setConfig(SaltConfig.SALT_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(SaltConfig.SALT_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.setConfig(SaltConfig.SALT_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);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
deleted file mode 100644
index 82e1b17..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
+++ /dev/null
@@ -1,220 +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 brooklyn.entity.salt;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.config.BrooklynServerConfig;
-import brooklyn.entity.Entity;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.software.MachineLifecycleEffectorTasks;
-import brooklyn.entity.software.SshEffectorTasks;
-import brooklyn.location.MachineLocation;
-import brooklyn.util.net.Urls;
-import brooklyn.util.ssh.BashCommands;
-import brooklyn.util.task.DynamicTasks;
-import brooklyn.util.task.Tasks;
-import brooklyn.util.time.Duration;
-import brooklyn.util.time.Time;
-
-import com.google.common.annotations.Beta;
-import com.google.common.base.Supplier;
-
-/**
- * Creates effectors to start, restart, and stop processes using SaltStack.
- * <p>
- * Instances of this should use the {@link SaltConfig} 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 SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements SaltConfig {
-
-    private static final Logger log = LoggerFactory.getLogger(SaltLifecycleEffectorTasks.class);
-
-    protected SaltStackMaster master = null;
-    protected String pidFile, serviceName, windowsServiceName;
-
-    public SaltLifecycleEffectorTasks() {
-    }
-
-    public SaltLifecycleEffectorTasks usePidFile(String pidFile) {
-        this.pidFile = pidFile;
-        return this;
-    }
-    public SaltLifecycleEffectorTasks useService(String serviceName) {
-        this.serviceName = serviceName;
-        return this;
-    }
-    public SaltLifecycleEffectorTasks useWindowsService(String serviceName) {
-        this.windowsServiceName = serviceName;
-        return this;
-    }
-    public SaltLifecycleEffectorTasks master(SaltStackMaster master) {
-        this.master = master;
-        return this;
-    }
-
-    @Override
-    public void attachLifecycleEffectors(Entity entity) {
-        if (pidFile==null && serviceName==null && getClass().equals(SaltLifecycleEffectorTasks.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) {
-        startMinionAsync();
-        return "salt start tasks submitted";
-    }
-
-    protected void startMinionAsync() {
-        // TODO make directories more configurable (both for ssh-drivers and for this)
-        String installDir = Urls.mergePaths(BrooklynServerConfig.getMgmtBaseDir(entity().getManagementContext()), "salt-install");
-        String runDir = Urls.mergePaths(BrooklynServerConfig.getMgmtBaseDir(entity().getManagementContext()),
-                "apps/"+entity().getApplicationId()+"/salt-entities/"+entity().getId());
-
-        Boolean masterless = entity().getConfig(SaltConfig.MASTERLESS_MODE);
-        if (masterless) {
-            DynamicTasks.queue(
-                    SaltTasks.installFormulas(installDir, SaltConfigs.getRequiredConfig(entity(), SALT_FORMULAS), false),
-                    SaltTasks.buildSaltFile(runDir,
-                            SaltConfigs.getRequiredConfig(entity(), SALT_RUN_LIST),
-                            entity().getConfig(SALT_LAUNCH_ATTRIBUTES)),
-                    SaltTasks.installSaltMinion(entity(), runDir, installDir, false),
-                    SaltTasks.runSalt(runDir));
-        } else {
-            throw new UnsupportedOperationException("Salt master mode not yet supported for minions");
-        }
-    }
-
-    @Override
-    protected void postStartCustom() {
-        boolean result = false;
-        result |= tryCheckStartPid();
-        result |= tryCheckStartService();
-        result |= tryCheckStartWindowsService();
-        if (!result) {
-            throw new IllegalStateException("The process for "+entity()+" appears not to be running (no way to check!)");
-        }
-    }
-
-    protected boolean tryCheckStartPid() {
-        if (pidFile==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(pidFile).runAsRoot()).get()) {
-            throw new IllegalStateException("The process for "+entity()+" appears not to be running (pid file "+pidFile+")");
-        }
-
-        // and set the PID
-        entity().setAttribute(Attributes.PID,
-                Integer.parseInt(DynamicTasks.queue(SshEffectorTasks.ssh("cat "+pidFile).runAsRoot()).block().getStdout().trim()));
-        return true;
-    }
-
-    protected boolean tryCheckStartService() {
-        if (serviceName==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/"+serviceName+" status").runAsRoot()).get())) {
-            throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+serviceName+")");
-        }
-
-        return true;
-    }
-
-    protected boolean tryCheckStartWindowsService() {
-        if (windowsServiceName==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 \""+serviceName+"\" | find \"RUNNING\"").runAsCommand()).get())) {
-            throw new IllegalStateException("The process for "+entity()+" appears not to be running (windowsService "+windowsServiceName+")");
-        }
-
-        return true;
-    }
-
-    @Override
-    protected String stopProcessesAtMachine() {
-        boolean result = false;
-        result |= tryStopService();
-        result |= tryStopPid();
-        if (!result) {
-            throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (no impl!)");
-        }
-        return "stopped";
-    }
-
-    protected boolean tryStopService() {
-        if (serviceName==null) return false;
-        int result = DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+serviceName+" stop").runAsRoot()).get();
-        if (0==result) return true;
-        if (entity().getAttribute(Attributes.SERVICE_STATE)!=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)==Lifecycle.RUNNING && pidFile==null)
-                log.warn("No PID recorded for "+entity()+" when running, with PID file "+pidFile+"; skipping kill in "+Tasks.current());
-            else
-                if (log.isDebugEnabled())
-                    log.debug("No PID recorded for "+entity()+"; skipping ("+entity().getAttribute(Attributes.SERVICE_STATE)+" / "+pidFile+")");
-            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().setAttribute(Attributes.PID, null);
-        return true;
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @return the Salt master entity if it exists.
-     * @see #master(SaltStackMaster)
-     * @see SaltConfig#MASTERLESS_MODE
-     */
-    @Override
-    public SaltStackMaster getMaster() {
-        return master;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMaster.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMaster.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMaster.java
deleted file mode 100644
index 0db1841..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMaster.java
+++ /dev/null
@@ -1,70 +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 brooklyn.entity.salt;
-
-import java.util.List;
-
-import org.apache.brooklyn.catalog.Catalog;
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.BrooklynConfigKeys;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.SoftwareProcess;
-import brooklyn.entity.proxying.ImplementedBy;
-import brooklyn.event.AttributeSensor;
-import brooklyn.event.basic.BasicAttributeSensor;
-import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
-import brooklyn.util.flags.SetFromFlag;
-
-import com.google.common.reflect.TypeToken;
-
-@ImplementedBy(SaltStackMasterImpl.class)
-@Catalog(name="SaltStack Master", description="The Salt master server")
-public interface SaltStackMaster extends SoftwareProcess {
-
-    @SetFromFlag("version")
-    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(BrooklynConfigKeys.SUGGESTED_VERSION, "stable");
-
-    @SetFromFlag("bootstrapUrl")
-    ConfigKey<String> BOOTSTRAP_URL = ConfigKeys.newStringConfigKey(
-            "salt.bootstrap.url", "The URL that returns the Salt boostrap commands",
-            "http://bootstrap.saltstack.org/");
-
-    @SetFromFlag("masterUser")
-    ConfigKey<String> MASTER_USER = ConfigKeys.newStringConfigKey(
-            "salt.master.user", "The user that runs the Salt master daemon process",
-            "root");
-
-    @SetFromFlag("masterConfigTemplate")
-    ConfigKey<String> MASTER_CONFIG_TEMPLATE_URL = ConfigKeys.newStringConfigKey(
-            "salt.master.config.templateUrl", "The template for the Salt master configuration (URL)",
-            "classpath:///brooklyn/entity/salt/master");
-
-    @SetFromFlag("saltPort")
-    PortAttributeSensorAndConfigKey SALT_PORT = new PortAttributeSensorAndConfigKey(
-            "salt.port", "Port used for communication between Salt master and minion processes", "4506+");
-
-    @SetFromFlag("publishPort")
-    PortAttributeSensorAndConfigKey PUBLISH_PORT = new PortAttributeSensorAndConfigKey(
-            "salt.publish.port", "Port used by the Salt master publisher", "4505+");
-
-    @SuppressWarnings("serial")
-    AttributeSensor<List<String>> MINION_IDS = new BasicAttributeSensor<List<String>>(new TypeToken<List<String>>() {},
-            "salt.minions", "List of Salt minion IDs");
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterDriver.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterDriver.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterDriver.java
deleted file mode 100644
index 55d46da..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterDriver.java
+++ /dev/null
@@ -1,25 +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 brooklyn.entity.salt;
-
-import brooklyn.entity.basic.SoftwareProcessDriver;
-
-public interface SaltStackMasterDriver extends SoftwareProcessDriver {
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterImpl.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterImpl.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterImpl.java
deleted file mode 100644
index 2a6815c..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterImpl.java
+++ /dev/null
@@ -1,56 +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 brooklyn.entity.salt;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.SoftwareProcessImpl;
-import brooklyn.event.feed.ConfigToAttributes;
-
-public class SaltStackMasterImpl extends SoftwareProcessImpl implements SaltStackMaster {
-
-    private static final Logger log = LoggerFactory.getLogger(SaltStackMasterImpl.class);
-
-    public SaltStackMasterImpl() {
-        super();
-    }
-
-    @Override
-    public Class getDriverInterface() {
-        return SaltStackMasterDriver.class;
-    }
-
-    @Override
-    protected void connectSensors() {
-        super.connectSensors();
-
-        // TODO what sensors should we poll?
-        ConfigToAttributes.apply(this);
-
-        connectServiceUpIsRunning();
-    }
-
-    @Override
-    protected void disconnectSensors() {
-        disconnectServiceUpIsRunning();
-
-        super.disconnectSensors();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterSshDriver.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterSshDriver.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterSshDriver.java
deleted file mode 100644
index 8a326e7..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltStackMasterSshDriver.java
+++ /dev/null
@@ -1,95 +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 brooklyn.entity.salt;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.java.JavaSoftwareProcessSshDriver;
-import brooklyn.location.basic.SshMachineLocation;
-import brooklyn.util.ssh.BashCommands;
-import brooklyn.util.task.DynamicTasks;
-
-import com.google.common.collect.ImmutableMap;
-
-public class SaltStackMasterSshDriver extends JavaSoftwareProcessSshDriver implements SaltStackMasterDriver {
-
-    public SaltStackMasterSshDriver(SaltStackMasterImpl entity, SshMachineLocation machine) {
-        super(entity, machine);
-    }
-
-    @Override
-    public SaltStackMasterImpl getEntity() {
-        return (SaltStackMasterImpl) super.getEntity();
-    }
-
-    @Override
-    protected String getLogFileLocation() {
-        return "master.log";
-    }
-
-    private String getPidFile() {
-        return "master.pid";
-    }
-
-    @Override
-    public void install() {
-        String url = Entities.getRequiredUrlConfig(getEntity(), SaltStackMaster.BOOTSTRAP_URL);
-        copyTemplate(url, "/etc/salt/master");
-
-        // Copy the file contents to the remote machine
-//        DynamicTasks.queue(SshEffectorTasks.put("/tmp/cumulus.yaml").contents(contents)).get();
-
-        // Run Salt bootstrap task to install master
-        DynamicTasks.queue(SaltTasks.installSaltMaster(getEntity(), getRunDir(), true));
-
-
-        newScript("createInstallDir")
-                .body.append("mkdir -p "+getInstallDir())
-                .failOnNonZeroResultCode()
-                .execute();
-
-        newScript(INSTALLING).
-                failOnNonZeroResultCode().
-                body.append("").execute();
-    }
-
-    @Override
-    public void customize() {
-    }
-
-    @Override
-    public void launch() {
-        newScript(ImmutableMap.of("usePidFile", false), LAUNCHING)
-                .body.append(BashCommands.sudo("start salt-master"))
-                .execute();
-    }
-
-    @Override
-    public boolean isRunning() {
-        return newScript(ImmutableMap.of("usePidFile", false), CHECK_RUNNING)
-                .body.append(BashCommands.sudo("status salt-master"))
-                .execute() == 0;
-    }
-
-    @Override
-    public void stop() {
-        newScript(ImmutableMap.of("usePidFile", false), STOPPING)
-                .body.append(BashCommands.sudo("stop salt-master"))
-                .execute();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltTasks.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltTasks.java b/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltTasks.java
deleted file mode 100644
index 5b4c4ee..0000000
--- a/sandbox/extra/src/main/java/brooklyn/entity/salt/SaltTasks.java
+++ /dev/null
@@ -1,145 +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 brooklyn.entity.salt;
-
-import static brooklyn.util.ssh.BashCommands.INSTALL_CURL;
-import static brooklyn.util.ssh.BashCommands.INSTALL_TAR;
-import static brooklyn.util.ssh.BashCommands.INSTALL_UNZIP;
-import static brooklyn.util.ssh.BashCommands.downloadToStdout;
-import static brooklyn.util.ssh.BashCommands.sudo;
-
-import java.util.Map;
-
-import brooklyn.entity.Entity;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.effector.EffectorTasks;
-import brooklyn.entity.software.SshEffectorTasks;
-import brooklyn.management.TaskFactory;
-import brooklyn.util.ResourceUtils;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.net.Urls;
-import brooklyn.util.ssh.BashCommands;
-import brooklyn.util.task.DynamicTasks;
-import brooklyn.util.task.Tasks;
-import brooklyn.util.text.TemplateProcessor;
-
-import com.google.common.annotations.Beta;
-
-@Beta
-public class SaltTasks {
-
-    public static TaskFactory<?> installSaltMaster(Entity master, String saltDirectory, boolean force) {
-        // TODO check on entity whether it is salt _server_
-        String boostrapUrl = master.getConfig(SaltStackMaster.BOOTSTRAP_URL);
-        String version = master.getConfig(SaltStackMaster.SUGGESTED_VERSION);
-        String installCmd = cdAndRun(saltDirectory,
-                BashCommands.chain(
-                        INSTALL_CURL,
-                        INSTALL_TAR,
-                        INSTALL_UNZIP,
-                        "( "+downloadToStdout(boostrapUrl) + " | " + sudo("sh -s -- -M -N "+version)+" )"));
-        if (!force) installCmd = BashCommands.alternatives("which salt-master", installCmd);
-        return SshEffectorTasks.ssh(installCmd).summary("install salt master");
-    }
-
-    public static TaskFactory<?> installSaltMinion(final Entity minion, final String runDir, final String installDir, final boolean force) {
-        return Tasks.<Void>builder().name("install minion").body(
-                new Runnable() {
-                    public void run() {
-                        // Setup bootstrap installation command for minion
-                        String boostrapUrl = minion.getConfig(SaltStackMaster.BOOTSTRAP_URL);
-                        String installCmd = cdAndRun(runDir, BashCommands.chain(
-                                INSTALL_CURL,
-                                INSTALL_TAR,
-                                INSTALL_UNZIP,
-                                "( "+downloadToStdout(boostrapUrl) + " | " + sudo("sh")+" )"));
-                        if (!force) installCmd = BashCommands.alternatives("which salt-minion", installCmd);
-
-                        // Process the minion configuration template
-                        Boolean masterless = minion.getConfig(SaltConfig.MASTERLESS_MODE);
-                        String url = masterless ? Entities.getRequiredUrlConfig(minion, SaltConfig.MASTERLESS_CONFIGURATION_URL)
-                                                : Entities.getRequiredUrlConfig(minion, SaltConfig.MINION_CONFIGURATION_URL);
-                        Map<String, Object> config = MutableMap.<String, Object>builder()
-                                .put("entity", minion)
-                                .put("runDir", runDir)
-                                .put("installDir", installDir)
-                                .put("formulas", minion.getConfig(SaltConfig.SALT_FORMULAS))
-                                .build();
-                        String contents = TemplateProcessor.processTemplateContents(new ResourceUtils(minion).getResourceAsString(url), config);
-
-                        // Copy the file contents to the remote machine and install/start salt-minion
-                        DynamicTasks.queue(
-                                SshEffectorTasks.ssh(installCmd),
-                                SshEffectorTasks.put("/tmp/minion")
-                                        .contents(contents)
-                                        .createDirectory(),
-                                SshEffectorTasks.ssh(sudo("mv /tmp/minion /etc/salt/minion")), // TODO clunky
-                                SshEffectorTasks.ssh(sudo("restart salt-minion"))
-                            );
-                    }
-                }).buildFactory();
-    }
-
-    public static TaskFactory<?> installFormulas(final String installDir, final Map<String,String> formulasAndUrls, final boolean force) {
-        return Tasks.<Void>builder().name("install formulas").body(
-                new Runnable() {
-                    public void run() {
-                        Entity e = EffectorTasks.findEntity();
-                        if (formulasAndUrls==null)
-                            throw new IllegalStateException("No formulas defined to install at "+e);
-                        for (String formula: formulasAndUrls.keySet())
-                            DynamicTasks.queue(installFormula(installDir, formula, formulasAndUrls.get(formula), force));
-                    }
-                }).buildFactory();
-    }
-
-    public static TaskFactory<?> installFormula(String installDir, String formula, String url, boolean force) {
-        return SshEffectorTasks.ssh(cdAndRun(installDir, SaltBashCommands.downloadAndExpandFormula(url, formula, force)))
-                .summary("install formula "+formula)
-                .requiringExitCodeZero();
-    }
-
-    protected static String cdAndRun(String targetDirectory, String command) {
-        return BashCommands.chain(
-                "mkdir -p "+targetDirectory,
-                "cd "+targetDirectory,
-                command);
-    }
-
-    public static TaskFactory<?> buildSaltFile(String runDir, Iterable<? extends String> runList, Map<String, Object> attributes) {
-        StringBuilder top =  new StringBuilder()
-                .append("base:\n")
-                .append("    '*':\n");
-        for (String run : runList) {
-            top.append("      - " + run + "\n");
-        }
-
-        return SshEffectorTasks.put(Urls.mergePaths(runDir, "base", "top.sls"))
-                .contents(top.toString())
-                .summary("build salt top file")
-                .createDirectory();
-    }
-
-    public static TaskFactory<?> runSalt(String runDir) {
-        return SshEffectorTasks.ssh(cdAndRun(runDir, BashCommands.sudo("salt-call state.highstate")))
-                .summary("run salt install")
-                .requiringExitCodeZero();
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
new file mode 100644
index 0000000..66c7acb
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
@@ -0,0 +1,179 @@
+/*
+ * 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.database.postgresql;
+
+import org.apache.brooklyn.entity.database.postgresql.PostgreSqlNodeSaltImpl;
+import org.apache.brooklyn.entity.salt.SaltConfig;
+import org.apache.brooklyn.entity.salt.SaltConfigs;
+import org.apache.brooklyn.entity.salt.SaltLifecycleEffectorTasks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Effector;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EffectorStartableImpl;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.database.postgresql.PostgreSqlNode;
+import brooklyn.entity.effector.EffectorBody;
+import brooklyn.entity.effector.Effectors;
+import brooklyn.entity.software.SshEffectorTasks;
+import brooklyn.event.basic.DependentConfiguration;
+import brooklyn.event.feed.ssh.SshFeed;
+import brooklyn.event.feed.ssh.SshPollConfig;
+import brooklyn.location.Location;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.task.DynamicTasks;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+public class PostgreSqlNodeSaltImpl extends EffectorStartableImpl implements PostgreSqlNode, SoftwareProcess {
+
+    private static final Logger LOG = LoggerFactory.getLogger(PostgreSqlNodeSaltImpl.class);
+
+    public static final Effector<String> EXECUTE_SCRIPT = Effectors.effector(String.class, "executeScript")
+            .description("invokes a script")
+            .parameter(ExecuteScriptEffectorBody.SCRIPT)
+            .impl(new ExecuteScriptEffectorBody())
+            .build();
+
+    private SshFeed feed;
+
+    @Override
+    public void init() {
+        super.init();
+        new SaltPostgreSqlLifecycle().attachLifecycleEffectors(this);
+    }
+
+    public static class SaltPostgreSqlLifecycle extends SaltLifecycleEffectorTasks {
+        public SaltPostgreSqlLifecycle() {
+            usePidFile("/var/run/postgresql/*.pid");
+            useService("postgresql");
+        }
+
+        @Override
+        protected void startMinionAsync() {
+            Entities.warnOnIgnoringConfig(entity(), SaltConfig.SALT_FORMULAS);
+            Entities.warnOnIgnoringConfig(entity(), SaltConfig.SALT_RUN_LIST);
+            Entities.warnOnIgnoringConfig(entity(), SaltConfig.SALT_LAUNCH_ATTRIBUTES);
+
+            // TODO Set these as defaults, rather than replacing user's value!?
+            SaltConfigs.addToFormulas(entity(), "postgres", "https://github.com/saltstack-formulas/postgres-formula/archive/master.tar.gz");
+            SaltConfigs.addToRunList(entity(), "postgres");
+            SaltConfigs.addLaunchAttributes(entity(), ImmutableMap.<String,Object>builder()
+                    .put("port", DependentConfiguration.attributeWhenReady(entity(), PostgreSqlNode.POSTGRESQL_PORT))
+                    .put("listen_addresses", "*")
+                    .put("pg_hba.type", "host")
+                    .put("pg_hba.db", "all")
+                    .put("pg_hba.user", "all")
+                    .put("pg_hba.addr", "0.0.0.0/0")
+                    .put("pg_hba.method", "md5")
+                    .build());
+
+            super.startMinionAsync();
+        }
+
+        @Override
+        protected void postStartCustom() {
+            super.postStartCustom();
+
+            // now run the creation script
+            String creationScriptUrl = entity().getConfig(PostgreSqlNode.CREATION_SCRIPT_URL);
+            String creationScript;
+            if (creationScriptUrl != null) {
+                creationScript = new ResourceUtils(entity()).getResourceAsString(creationScriptUrl);
+            } else {
+                creationScript = entity().getConfig(PostgreSqlNode.CREATION_SCRIPT_CONTENTS);
+            }
+            entity().invoke(PostgreSqlNodeSaltImpl.EXECUTE_SCRIPT,
+                    ConfigBag.newInstance().configure(ExecuteScriptEffectorBody.SCRIPT, creationScript).getAllConfig()).getUnchecked();
+
+            // and finally connect sensors
+            ((PostgreSqlNodeSaltImpl) entity()).connectSensors();
+        }
+
+        @Override
+        protected void preStopCustom() {
+            ((PostgreSqlNodeSaltImpl) entity()).disconnectSensors();
+            super.preStopCustom();
+        }
+    }
+
+    public static class ExecuteScriptEffectorBody extends EffectorBody<String> {
+        public static final ConfigKey<String> SCRIPT = ConfigKeys.newStringConfigKey("script", "contents of script to run");
+
+        @Override
+        public String call(ConfigBag parameters) {
+            return DynamicTasks.queue(SshEffectorTasks.ssh(
+                    BashCommands.pipeTextTo(
+                            parameters.get(SCRIPT),
+                            BashCommands.sudoAsUser("postgres", "psql --file -")))
+                    .requiringExitCodeZero()).getStdout();
+        }
+    }
+
+    protected void connectSensors() {
+        setAttribute(DATASTORE_URL, String.format("postgresql://%s:%s/", getAttribute(HOSTNAME), getAttribute(POSTGRESQL_PORT)));
+
+        Location machine = Iterables.get(getLocations(), 0, null);
+
+        if (machine instanceof SshMachineLocation) {
+            feed = SshFeed.builder()
+                    .entity(this)
+                    .machine((SshMachineLocation)machine)
+                    .poll(new SshPollConfig<Boolean>(SERVICE_UP)
+                            .command("ps -ef | grep [p]ostgres")
+                            .setOnSuccess(true)
+                            .setOnFailureOrException(false))
+                    .build();
+        } else {
+            LOG.warn("Location(s) %s not an ssh-machine location, so not polling for status; setting serviceUp immediately", getLocations());
+        }
+    }
+
+    protected void disconnectSensors() {
+        if (feed != null) feed.stop();
+    }
+
+    @Override
+    public Integer getPostgreSqlPort() { return getAttribute(POSTGRESQL_PORT); }
+
+    @Override
+    public String getSharedMemory() { return getConfig(SHARED_MEMORY); }
+
+    @Override
+    public Integer getMaxConnections() { return getConfig(MAX_CONNECTIONS); }
+
+    @Override
+    public String getShortName() {
+        return "PostgreSQL";
+    }
+
+    @Override
+    public String executeScript(String commands) {
+        return Entities.invokeEffector(this, this, EXECUTE_SCRIPT,
+                ConfigBag.newInstance().configure(ExecuteScriptEffectorBody.SCRIPT, commands).getAllConfig()).getUnchecked();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltBashCommands.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltBashCommands.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltBashCommands.java
new file mode 100644
index 0000000..a9a07bf
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltBashCommands.java
@@ -0,0 +1,92 @@
+/*
+ * 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.salt;
+
+import static brooklyn.util.ssh.BashCommands.downloadToStdout;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.io.FilenameUtils;
+
+import brooklyn.entity.chef.ChefBashCommands;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.text.Strings;
+
+import com.google.common.annotations.Beta;
+import com.google.common.io.Files;
+
+/**
+ * BASH commands useful for setting up SaltStack.
+ */
+@Beta
+public class SaltBashCommands {
+
+    /**
+     * SaltStack formulas can be found at {@code https://github.com/saltstack-formulas} as repositories.
+     * <p>
+     * This assumes the download is an archive containing a single directory on the root which will
+     * be renamed to {@code formulaName}. if that directory already has the correct name {@code formulaName}
+     * can be null, but if taking from a GitHub tarball it will typically be of the form {@code formulaName-master/}
+     * hence the renaming.
+     */
+    // TODO support installing from classpath, and using the repository (tie in with those methods)
+    public static final String downloadAndExpandFormula(String source, @Nullable String formulaName, boolean force) {
+        String dl = downloadAndExpandFormula(source);
+        if (formulaName==null) return dl;
+        String tmpName = "tmp-"+Strings.makeValidFilename(formulaName)+"-"+Identifiers.makeRandomId(4);
+        String installCmd = BashCommands.chain("mkdir "+tmpName, "cd "+tmpName, dl,
+                BashCommands.requireTest("`ls | wc -w` -eq 1", "The archive must contain exactly one directory"),
+                "FORMULA_EXPANDED_DIR=`ls`",
+                "mv $FORMULA_EXPANDED_DIR '../"+formulaName+"'",
+                "cd ..",
+                "rm -rf "+tmpName);
+        if (!force) return BashCommands.alternatives("ls "+formulaName, installCmd);
+        else return BashCommands.alternatives("rm -rf "+formulaName, installCmd);
+    }
+
+    /**
+     * Same as {@link #downloadAndExpandFormula(String, String)} with no formula name.
+     * <p>
+     * Equivalent to the following command, but substituting the given {@code sourceUrl}.
+     * <pre>{@code
+     * curl -f -L  https://github.com/saltstack-formulas/nginx-formula/archive/master.tar.gz | tar xvz
+     * }</pre>
+     */
+    public static final String downloadAndExpandFormula(String sourceUrl) {
+        String ext = Files.getFileExtension(sourceUrl);
+        if ("tar".equalsIgnoreCase(ext))
+            return downloadToStdout(sourceUrl) + " | tar xv";
+        if ("tgz".equalsIgnoreCase(ext) || sourceUrl.toLowerCase().endsWith(".tar.gz"))
+            return downloadToStdout(sourceUrl) + " | tar xvz";
+
+        String target = FilenameUtils.getName(sourceUrl);
+        if (target==null) target = ""; else target = target.trim();
+        target += "_"+Strings.makeRandomId(4);
+
+        if ("zip".equalsIgnoreCase(ext) || "tar.gz".equalsIgnoreCase(ext))
+            return BashCommands.chain(
+                BashCommands.commandToDownloadUrlAs(sourceUrl, target),
+                "unzip "+target,
+                "rm "+target);
+
+        throw new UnsupportedOperationException("No way to expand "+sourceUrl+" (yet)");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfig.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfig.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfig.java
new file mode 100644
index 0000000..3b5962f
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfig.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.salt;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.MapConfigKey;
+import brooklyn.event.basic.SetConfigKey;
+import brooklyn.management.TaskAdaptable;
+import brooklyn.management.TaskFactory;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * {@link ConfigKey}s used to configure Salt entities.
+ *
+ * @see SaltConfigs
+ * @see SaltLifecycleEffectorTasks
+ */
+@Beta
+public interface SaltConfig {
+
+    MapConfigKey<String> SALT_FORMULAS = new MapConfigKey<String>(String.class,
+            "salt.formulaUrls", "Map of Salt formula URLs (normally GutHub repository archives from the salt-formulas user)");
+    SetConfigKey<String> SALT_RUN_LIST = new SetConfigKey<String>(String.class,
+            "salt.runList", "Set of Salt states to install from the formula URLs");
+    MapConfigKey<Object> SALT_LAUNCH_ATTRIBUTES = new MapConfigKey<Object>(Object.class, "salt.launch.attributes", "TODO");
+
+    @SetFromFlag("master")
+    ConfigKey<SaltStackMaster> MASTER = ConfigKeys.newConfigKey(SaltStackMaster.class,
+            "salt.master.entity", "The Salt master server");
+
+    AttributeSensor<String> MINION_ID = new BasicAttributeSensor<String>(String.class,
+            "salt.minionId", "The ID for a Salt minion");
+
+    @SetFromFlag("masterless")
+    ConfigKey<Boolean> MASTERLESS_MODE = ConfigKeys.newBooleanConfigKey(
+            "salt.masterless", "Salt masterless, minion only configuration (default uses master and minion)",
+            Boolean.FALSE);
+
+    @SetFromFlag("masterConfigUrl")
+    ConfigKey<String> MASTER_CONFIGURATION_URL = ConfigKeys.newStringConfigKey(
+            "salt.master.templateUrl", "The Salt master configuration file template URL",
+            "classpath://org/apache/brooklyn/entity/salt/master");
+
+    @SetFromFlag("minionConfigUrl")
+    ConfigKey<String> MINION_CONFIGURATION_URL = ConfigKeys.newStringConfigKey(
+            "salt.minion.templateUrl", "The Salt minion configuration file template URL",
+            "classpath://org/apache/brooklyn/entity/salt/minion");
+
+    @SetFromFlag("masterlessConfigUrl")
+    ConfigKey<String> MASTERLESS_CONFIGURATION_URL = ConfigKeys.newStringConfigKey(
+            "salt.masterless.templateUrl", "The Salt minion masterless configuration file template URL",
+            "classpath://org/apache/brooklyn/entity/salt/masterless");
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @SetFromFlag("minionIdFunction")
+    ConfigKey<Function<Entity, String>> MINION_ID_FUNCTION = new BasicConfigKey(Function.class,
+            "salt.minionId.function", "Function to generate the ID of a Salt minion for an entity", Functions.toStringFunction());
+
+    @SuppressWarnings("serial")
+    ConfigKey<TaskFactory<? extends TaskAdaptable<Boolean>>> IS_RUNNING_TASK = ConfigKeys.newConfigKey(
+            new TypeToken<TaskFactory<? extends TaskAdaptable<Boolean>>>() {}, 
+            "salt.driver.isRunningTask");
+
+    @SuppressWarnings("serial")
+    ConfigKey<TaskFactory<?>> STOP_TASK = ConfigKeys.newConfigKey(
+            new TypeToken<TaskFactory<?>>() {}, 
+            "salt.driver.stopTask");
+
+    /**
+     * The {@link SaltStackMaster master} entity.
+     */
+    SaltStackMaster getMaster();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfigs.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfigs.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfigs.java
new file mode 100644
index 0000000..1622d85
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltConfigs.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.salt;
+
+import java.util.Map;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.event.basic.MapConfigKey.MapModifications;
+import brooklyn.event.basic.SetConfigKey.SetModifications;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+
+/**
+ * Conveniences for configuring brooklyn Salt entities 
+ *
+ * @since 0.6.0
+ */
+@Beta
+public class SaltConfigs {
+
+    public static void addToRunList(EntitySpec<?> entity, String...states) {
+        for (String state : states) {
+            entity.configure(SaltConfig.SALT_RUN_LIST, SetModifications.addItem(state));
+        }
+    }
+
+    public static void addToRunList(EntityInternal entity, String...states) {
+        for (String state : states) {
+            entity.setConfig(SaltConfig.SALT_RUN_LIST, SetModifications.addItem(state));
+        }
+    }
+
+    public static void addToFormuals(EntitySpec<?> entity, String formulaName, String formulaUrl) {
+        entity.configure(SaltConfig.SALT_FORMULAS.subKey(formulaName), formulaUrl);
+    }
+
+    public static void addToFormulas(EntityInternal entity, String formulaName, String formulaUrl) {
+        entity.setConfig(SaltConfig.SALT_FORMULAS.subKey(formulaName), formulaUrl);
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static void addLaunchAttributes(EntitySpec<?> entity, Map<? extends Object,? extends Object> attributesMap) {
+        entity.configure(SaltConfig.SALT_LAUNCH_ATTRIBUTES, MapModifications.add((Map)attributesMap));
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static void addLaunchAttributes(EntityInternal entity, Map<? extends Object,? extends Object> attributesMap) {
+        entity.setConfig(SaltConfig.SALT_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(SaltConfig.SALT_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.setConfig(SaltConfig.SALT_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);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
new file mode 100644
index 0000000..7005db9
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
@@ -0,0 +1,220 @@
+/*
+ * 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.salt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.BrooklynServerConfig;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.software.MachineLifecycleEffectorTasks;
+import brooklyn.entity.software.SshEffectorTasks;
+import brooklyn.location.MachineLocation;
+import brooklyn.util.net.Urls;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.task.DynamicTasks;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Supplier;
+
+/**
+ * Creates effectors to start, restart, and stop processes using SaltStack.
+ * <p>
+ * Instances of this should use the {@link SaltConfig} 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 SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements SaltConfig {
+
+    private static final Logger log = LoggerFactory.getLogger(SaltLifecycleEffectorTasks.class);
+
+    protected SaltStackMaster master = null;
+    protected String pidFile, serviceName, windowsServiceName;
+
+    public SaltLifecycleEffectorTasks() {
+    }
+
+    public SaltLifecycleEffectorTasks usePidFile(String pidFile) {
+        this.pidFile = pidFile;
+        return this;
+    }
+    public SaltLifecycleEffectorTasks useService(String serviceName) {
+        this.serviceName = serviceName;
+        return this;
+    }
+    public SaltLifecycleEffectorTasks useWindowsService(String serviceName) {
+        this.windowsServiceName = serviceName;
+        return this;
+    }
+    public SaltLifecycleEffectorTasks master(SaltStackMaster master) {
+        this.master = master;
+        return this;
+    }
+
+    @Override
+    public void attachLifecycleEffectors(Entity entity) {
+        if (pidFile==null && serviceName==null && getClass().equals(SaltLifecycleEffectorTasks.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) {
+        startMinionAsync();
+        return "salt start tasks submitted";
+    }
+
+    protected void startMinionAsync() {
+        // TODO make directories more configurable (both for ssh-drivers and for this)
+        String installDir = Urls.mergePaths(BrooklynServerConfig.getMgmtBaseDir(entity().getManagementContext()), "salt-install");
+        String runDir = Urls.mergePaths(BrooklynServerConfig.getMgmtBaseDir(entity().getManagementContext()),
+                "apps/"+entity().getApplicationId()+"/salt-entities/"+entity().getId());
+
+        Boolean masterless = entity().getConfig(SaltConfig.MASTERLESS_MODE);
+        if (masterless) {
+            DynamicTasks.queue(
+                    SaltTasks.installFormulas(installDir, SaltConfigs.getRequiredConfig(entity(), SALT_FORMULAS), false),
+                    SaltTasks.buildSaltFile(runDir,
+                            SaltConfigs.getRequiredConfig(entity(), SALT_RUN_LIST),
+                            entity().getConfig(SALT_LAUNCH_ATTRIBUTES)),
+                    SaltTasks.installSaltMinion(entity(), runDir, installDir, false),
+                    SaltTasks.runSalt(runDir));
+        } else {
+            throw new UnsupportedOperationException("Salt master mode not yet supported for minions");
+        }
+    }
+
+    @Override
+    protected void postStartCustom() {
+        boolean result = false;
+        result |= tryCheckStartPid();
+        result |= tryCheckStartService();
+        result |= tryCheckStartWindowsService();
+        if (!result) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (no way to check!)");
+        }
+    }
+
+    protected boolean tryCheckStartPid() {
+        if (pidFile==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(pidFile).runAsRoot()).get()) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (pid file "+pidFile+")");
+        }
+
+        // and set the PID
+        entity().setAttribute(Attributes.PID,
+                Integer.parseInt(DynamicTasks.queue(SshEffectorTasks.ssh("cat "+pidFile).runAsRoot()).block().getStdout().trim()));
+        return true;
+    }
+
+    protected boolean tryCheckStartService() {
+        if (serviceName==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/"+serviceName+" status").runAsRoot()).get())) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+serviceName+")");
+        }
+
+        return true;
+    }
+
+    protected boolean tryCheckStartWindowsService() {
+        if (windowsServiceName==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 \""+serviceName+"\" | find \"RUNNING\"").runAsCommand()).get())) {
+            throw new IllegalStateException("The process for "+entity()+" appears not to be running (windowsService "+windowsServiceName+")");
+        }
+
+        return true;
+    }
+
+    @Override
+    protected String stopProcessesAtMachine() {
+        boolean result = false;
+        result |= tryStopService();
+        result |= tryStopPid();
+        if (!result) {
+            throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (no impl!)");
+        }
+        return "stopped";
+    }
+
+    protected boolean tryStopService() {
+        if (serviceName==null) return false;
+        int result = DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+serviceName+" stop").runAsRoot()).get();
+        if (0==result) return true;
+        if (entity().getAttribute(Attributes.SERVICE_STATE)!=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)==Lifecycle.RUNNING && pidFile==null)
+                log.warn("No PID recorded for "+entity()+" when running, with PID file "+pidFile+"; skipping kill in "+Tasks.current());
+            else
+                if (log.isDebugEnabled())
+                    log.debug("No PID recorded for "+entity()+"; skipping ("+entity().getAttribute(Attributes.SERVICE_STATE)+" / "+pidFile+")");
+            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().setAttribute(Attributes.PID, null);
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the Salt master entity if it exists.
+     * @see #master(SaltStackMaster)
+     * @see SaltConfig#MASTERLESS_MODE
+     */
+    @Override
+    public SaltStackMaster getMaster() {
+        return master;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMaster.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMaster.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMaster.java
new file mode 100644
index 0000000..8ce2e3b
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMaster.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.salt;
+
+import java.util.List;
+
+import org.apache.brooklyn.catalog.Catalog;
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.BrooklynConfigKeys;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.reflect.TypeToken;
+
+@ImplementedBy(SaltStackMasterImpl.class)
+@Catalog(name="SaltStack Master", description="The Salt master server")
+public interface SaltStackMaster extends SoftwareProcess {
+
+    @SetFromFlag("version")
+    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(BrooklynConfigKeys.SUGGESTED_VERSION, "stable");
+
+    @SetFromFlag("bootstrapUrl")
+    ConfigKey<String> BOOTSTRAP_URL = ConfigKeys.newStringConfigKey(
+            "salt.bootstrap.url", "The URL that returns the Salt boostrap commands",
+            "http://bootstrap.saltstack.org/");
+
+    @SetFromFlag("masterUser")
+    ConfigKey<String> MASTER_USER = ConfigKeys.newStringConfigKey(
+            "salt.master.user", "The user that runs the Salt master daemon process",
+            "root");
+
+    @SetFromFlag("masterConfigTemplate")
+    ConfigKey<String> MASTER_CONFIG_TEMPLATE_URL = ConfigKeys.newStringConfigKey(
+            "salt.master.config.templateUrl", "The template for the Salt master configuration (URL)",
+            "classpath://org/apache/brooklyn/entity/salt/master");
+
+    @SetFromFlag("saltPort")
+    PortAttributeSensorAndConfigKey SALT_PORT = new PortAttributeSensorAndConfigKey(
+            "salt.port", "Port used for communication between Salt master and minion processes", "4506+");
+
+    @SetFromFlag("publishPort")
+    PortAttributeSensorAndConfigKey PUBLISH_PORT = new PortAttributeSensorAndConfigKey(
+            "salt.publish.port", "Port used by the Salt master publisher", "4505+");
+
+    @SuppressWarnings("serial")
+    AttributeSensor<List<String>> MINION_IDS = new BasicAttributeSensor<List<String>>(new TypeToken<List<String>>() {},
+            "salt.minions", "List of Salt minion IDs");
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterDriver.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterDriver.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterDriver.java
new file mode 100644
index 0000000..703213a
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterDriver.java
@@ -0,0 +1,25 @@
+/*
+ * 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.salt;
+
+import brooklyn.entity.basic.SoftwareProcessDriver;
+
+public interface SaltStackMasterDriver extends SoftwareProcessDriver {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterImpl.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterImpl.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterImpl.java
new file mode 100644
index 0000000..1adfd1a
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterImpl.java
@@ -0,0 +1,56 @@
+/*
+ * 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.salt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.SoftwareProcessImpl;
+import brooklyn.event.feed.ConfigToAttributes;
+
+public class SaltStackMasterImpl extends SoftwareProcessImpl implements SaltStackMaster {
+
+    private static final Logger log = LoggerFactory.getLogger(SaltStackMasterImpl.class);
+
+    public SaltStackMasterImpl() {
+        super();
+    }
+
+    @Override
+    public Class getDriverInterface() {
+        return SaltStackMasterDriver.class;
+    }
+
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+
+        // TODO what sensors should we poll?
+        ConfigToAttributes.apply(this);
+
+        connectServiceUpIsRunning();
+    }
+
+    @Override
+    protected void disconnectSensors() {
+        disconnectServiceUpIsRunning();
+
+        super.disconnectSensors();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterSshDriver.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterSshDriver.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterSshDriver.java
new file mode 100644
index 0000000..4b88acc
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltStackMasterSshDriver.java
@@ -0,0 +1,95 @@
+/*
+ * 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.salt;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.java.JavaSoftwareProcessSshDriver;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.task.DynamicTasks;
+
+import com.google.common.collect.ImmutableMap;
+
+public class SaltStackMasterSshDriver extends JavaSoftwareProcessSshDriver implements SaltStackMasterDriver {
+
+    public SaltStackMasterSshDriver(SaltStackMasterImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    @Override
+    public SaltStackMasterImpl getEntity() {
+        return (SaltStackMasterImpl) super.getEntity();
+    }
+
+    @Override
+    protected String getLogFileLocation() {
+        return "master.log";
+    }
+
+    private String getPidFile() {
+        return "master.pid";
+    }
+
+    @Override
+    public void install() {
+        String url = Entities.getRequiredUrlConfig(getEntity(), SaltStackMaster.BOOTSTRAP_URL);
+        copyTemplate(url, "/etc/salt/master");
+
+        // Copy the file contents to the remote machine
+//        DynamicTasks.queue(SshEffectorTasks.put("/tmp/cumulus.yaml").contents(contents)).get();
+
+        // Run Salt bootstrap task to install master
+        DynamicTasks.queue(SaltTasks.installSaltMaster(getEntity(), getRunDir(), true));
+
+
+        newScript("createInstallDir")
+                .body.append("mkdir -p "+getInstallDir())
+                .failOnNonZeroResultCode()
+                .execute();
+
+        newScript(INSTALLING).
+                failOnNonZeroResultCode().
+                body.append("").execute();
+    }
+
+    @Override
+    public void customize() {
+    }
+
+    @Override
+    public void launch() {
+        newScript(ImmutableMap.of("usePidFile", false), LAUNCHING)
+                .body.append(BashCommands.sudo("start salt-master"))
+                .execute();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return newScript(ImmutableMap.of("usePidFile", false), CHECK_RUNNING)
+                .body.append(BashCommands.sudo("status salt-master"))
+                .execute() == 0;
+    }
+
+    @Override
+    public void stop() {
+        newScript(ImmutableMap.of("usePidFile", false), STOPPING)
+                .body.append(BashCommands.sudo("stop salt-master"))
+                .execute();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltTasks.java
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltTasks.java b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltTasks.java
new file mode 100644
index 0000000..5cec099
--- /dev/null
+++ b/sandbox/extra/src/main/java/org/apache/brooklyn/entity/salt/SaltTasks.java
@@ -0,0 +1,145 @@
+/*
+ * 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.salt;
+
+import static brooklyn.util.ssh.BashCommands.INSTALL_CURL;
+import static brooklyn.util.ssh.BashCommands.INSTALL_TAR;
+import static brooklyn.util.ssh.BashCommands.INSTALL_UNZIP;
+import static brooklyn.util.ssh.BashCommands.downloadToStdout;
+import static brooklyn.util.ssh.BashCommands.sudo;
+
+import java.util.Map;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.effector.EffectorTasks;
+import brooklyn.entity.software.SshEffectorTasks;
+import brooklyn.management.TaskFactory;
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.net.Urls;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.task.DynamicTasks;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.text.TemplateProcessor;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class SaltTasks {
+
+    public static TaskFactory<?> installSaltMaster(Entity master, String saltDirectory, boolean force) {
+        // TODO check on entity whether it is salt _server_
+        String boostrapUrl = master.getConfig(SaltStackMaster.BOOTSTRAP_URL);
+        String version = master.getConfig(SaltStackMaster.SUGGESTED_VERSION);
+        String installCmd = cdAndRun(saltDirectory,
+                BashCommands.chain(
+                        INSTALL_CURL,
+                        INSTALL_TAR,
+                        INSTALL_UNZIP,
+                        "( "+downloadToStdout(boostrapUrl) + " | " + sudo("sh -s -- -M -N "+version)+" )"));
+        if (!force) installCmd = BashCommands.alternatives("which salt-master", installCmd);
+        return SshEffectorTasks.ssh(installCmd).summary("install salt master");
+    }
+
+    public static TaskFactory<?> installSaltMinion(final Entity minion, final String runDir, final String installDir, final boolean force) {
+        return Tasks.<Void>builder().name("install minion").body(
+                new Runnable() {
+                    public void run() {
+                        // Setup bootstrap installation command for minion
+                        String boostrapUrl = minion.getConfig(SaltStackMaster.BOOTSTRAP_URL);
+                        String installCmd = cdAndRun(runDir, BashCommands.chain(
+                                INSTALL_CURL,
+                                INSTALL_TAR,
+                                INSTALL_UNZIP,
+                                "( "+downloadToStdout(boostrapUrl) + " | " + sudo("sh")+" )"));
+                        if (!force) installCmd = BashCommands.alternatives("which salt-minion", installCmd);
+
+                        // Process the minion configuration template
+                        Boolean masterless = minion.getConfig(SaltConfig.MASTERLESS_MODE);
+                        String url = masterless ? Entities.getRequiredUrlConfig(minion, SaltConfig.MASTERLESS_CONFIGURATION_URL)
+                                                : Entities.getRequiredUrlConfig(minion, SaltConfig.MINION_CONFIGURATION_URL);
+                        Map<String, Object> config = MutableMap.<String, Object>builder()
+                                .put("entity", minion)
+                                .put("runDir", runDir)
+                                .put("installDir", installDir)
+                                .put("formulas", minion.getConfig(SaltConfig.SALT_FORMULAS))
+                                .build();
+                        String contents = TemplateProcessor.processTemplateContents(new ResourceUtils(minion).getResourceAsString(url), config);
+
+                        // Copy the file contents to the remote machine and install/start salt-minion
+                        DynamicTasks.queue(
+                                SshEffectorTasks.ssh(installCmd),
+                                SshEffectorTasks.put("/tmp/minion")
+                                        .contents(contents)
+                                        .createDirectory(),
+                                SshEffectorTasks.ssh(sudo("mv /tmp/minion /etc/salt/minion")), // TODO clunky
+                                SshEffectorTasks.ssh(sudo("restart salt-minion"))
+                            );
+                    }
+                }).buildFactory();
+    }
+
+    public static TaskFactory<?> installFormulas(final String installDir, final Map<String,String> formulasAndUrls, final boolean force) {
+        return Tasks.<Void>builder().name("install formulas").body(
+                new Runnable() {
+                    public void run() {
+                        Entity e = EffectorTasks.findEntity();
+                        if (formulasAndUrls==null)
+                            throw new IllegalStateException("No formulas defined to install at "+e);
+                        for (String formula: formulasAndUrls.keySet())
+                            DynamicTasks.queue(installFormula(installDir, formula, formulasAndUrls.get(formula), force));
+                    }
+                }).buildFactory();
+    }
+
+    public static TaskFactory<?> installFormula(String installDir, String formula, String url, boolean force) {
+        return SshEffectorTasks.ssh(cdAndRun(installDir, SaltBashCommands.downloadAndExpandFormula(url, formula, force)))
+                .summary("install formula "+formula)
+                .requiringExitCodeZero();
+    }
+
+    protected static String cdAndRun(String targetDirectory, String command) {
+        return BashCommands.chain(
+                "mkdir -p "+targetDirectory,
+                "cd "+targetDirectory,
+                command);
+    }
+
+    public static TaskFactory<?> buildSaltFile(String runDir, Iterable<? extends String> runList, Map<String, Object> attributes) {
+        StringBuilder top =  new StringBuilder()
+                .append("base:\n")
+                .append("    '*':\n");
+        for (String run : runList) {
+            top.append("      - " + run + "\n");
+        }
+
+        return SshEffectorTasks.put(Urls.mergePaths(runDir, "base", "top.sls"))
+                .contents(top.toString())
+                .summary("build salt top file")
+                .createDirectory();
+    }
+
+    public static TaskFactory<?> runSalt(String runDir) {
+        return SshEffectorTasks.ssh(cdAndRun(runDir, BashCommands.sudo("salt-call state.highstate")))
+                .summary("run salt install")
+                .requiringExitCodeZero();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/resources/brooklyn/entity/salt/master
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/resources/brooklyn/entity/salt/master b/sandbox/extra/src/main/resources/brooklyn/entity/salt/master
deleted file mode 100644
index 72d7eb9..0000000
--- a/sandbox/extra/src/main/resources/brooklyn/entity/salt/master
+++ /dev/null
@@ -1,65 +0,0 @@
-[#ftl]
-#
-# 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.
-#
-# /etc/salt/master
-##
-
-#interface: 0.0.0.0
-#ipv6: False
-#publish_port: ${entity.publishPort,c} # 4505
-
-#user: root
-#max_open_files: 100000
-#worker_threads: 5
-ret_port: ${entity.saltPort,c} # 4506
-
-root_dir: /
-pidfile: ${driver.pidFile}
-pki_dir: ${runDir}/pki
-cachedir: ${runDir}/cache
-log_file: ${driver.logFileLocation}
-key_logfile: ${runDir}/key.log
-
-#verify_env: True
-#keep_jobs: 24
-
-#timeout: 5
-#loop_interval: 60
-
-output: nested
-color: False
-log_level: info
-log_level_logfile: debug # Debugging
-
-#job_cache: True
-#minion_data_cache: True
-
-#open_mode: False
-#auto_accept: False
-#autosign_file: autosign.conf
-#permissive_pki_access: False
-
-fileserver_backend:
-  - git
-
-gitfs_remotes:
-  - git://github.com/saltstack/salt-states.git
-  - git://github.com/saltstack-formulas/postgres-formula.git
-  - ${entity.remoteUrl}
-  # TODO iterate through formula URLs

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/070b5ca7/sandbox/extra/src/main/resources/brooklyn/entity/salt/masterless
----------------------------------------------------------------------
diff --git a/sandbox/extra/src/main/resources/brooklyn/entity/salt/masterless b/sandbox/extra/src/main/resources/brooklyn/entity/salt/masterless
deleted file mode 100644
index ba41c6e..0000000
--- a/sandbox/extra/src/main/resources/brooklyn/entity/salt/masterless
+++ /dev/null
@@ -1,53 +0,0 @@
-[#ftl]
-##
-#
-# 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.
-#
-# SaltStack Masterless Minion Configuration
-#
-# /etc/salt/minion
-##
-
-# Minion configuration
-id: ${entity.id}
-user: root
-backup_mode: minion
-
-# Directory settings
-root_dir: /
-pidfile: ${runDir}/salt-minion.pid
-pki_dir: ${runDir}/pki
-cachedir: ${runDir}/cache
-log_file: ${runDir}/minion.log
-key_logfile: ${runDir}/key.log
-
-file_client: local
-file_roots:
-  base:
-    - ${runDir}/base
-[#list formulas?keys as formulaName]
-    - ${installDir}/${formulaName}
-[/#list]
-
-#verify_env: True
-#cache_jobs: True # Debugging
-
-output: nested
-color: False
-log_level: info
-log_level_logfile: debug # Debugging