You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ri...@apache.org on 2014/05/28 18:05:21 UTC

[05/21] git commit: Modified SaltStack to use masterless minion configuration initially

Modified SaltStack to use masterless minion configuration initially


Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/b67d8bd0
Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/b67d8bd0
Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/b67d8bd0

Branch: refs/heads/master
Commit: b67d8bd0a09e218042b4f025dbe44965b84d42c8
Parents: c21a7bc
Author: Andrew Kennedy <an...@cloudsoftcorp.com>
Authored: Wed Oct 23 14:26:20 2013 +0100
Committer: Andrew Kennedy <an...@cloudsoftcorp.com>
Committed: Wed May 28 15:12:53 2014 +0100

----------------------------------------------------------------------
 .../brooklyn/entity/salt/SaltBashCommands.java  |  57 ++++++-----
 .../java/brooklyn/entity/salt/SaltConfig.java   |  15 ++-
 .../java/brooklyn/entity/salt/SaltConfigs.java  |  14 +--
 .../entity/salt/SaltLifecycleEffectorTasks.java |  75 ++++++++------
 .../java/brooklyn/entity/salt/SaltTasks.java    | 102 +++++++++++--------
 .../main/resources/brooklyn/entity/salt/master  |  11 +-
 .../resources/brooklyn/entity/salt/masterless   |  37 +++++++
 .../main/resources/brooklyn/entity/salt/minion  |   4 +-
 .../main/resources/brooklyn/entity/salt/top.sls |   1 +
 .../postgresql/PostgreSqlNodeSaltImpl.java      |  31 +++---
 10 files changed, 219 insertions(+), 128 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/java/brooklyn/entity/salt/SaltBashCommands.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/salt/SaltBashCommands.java b/software/base/src/main/java/brooklyn/entity/salt/SaltBashCommands.java
index 1d37afc..4ee4b4a 100644
--- a/software/base/src/main/java/brooklyn/entity/salt/SaltBashCommands.java
+++ b/software/base/src/main/java/brooklyn/entity/salt/SaltBashCommands.java
@@ -1,10 +1,6 @@
 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 static brooklyn.util.ssh.BashCommands.*;
 
 import javax.annotation.Nullable;
 
@@ -27,11 +23,11 @@ import com.google.common.io.Files;
 public class SaltBashCommands {
 
     /**
-     * SaltStack formulas are normally found at https://github.com/saltstack-formulas as repositories.
-     * 
-     * this assumes the download is an archive containing a single directory on the root which will be renamed to "cookbookName";
-     * if that directory already has the correct name cookbookName can be null,
-     * but if e.g. taking from a github tarball it will typically be of the form cookbookName-master/ 
+     * 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)
@@ -39,9 +35,8 @@ public class SaltBashCommands {
         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 downloaded archive must contain exactly one directory; contained"),
+        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 ..",
@@ -49,27 +44,33 @@ public class SaltBashCommands {
         if (!force) return BashCommands.alternatives("ls "+formulaName, installCmd);
         else return BashCommands.alternatives("rm -rf "+formulaName, installCmd);
     }
-    
-    /** as {@link #downloadAndExpandFormula(String, String)} with no formula name */
-    public static final String downloadAndExpandFormula(String source) {
-//        curl -f -L  https://github.com/saltstack-formulas/nginx-formula/archive/master.tar.gz | tar xvz
-        String ext = Files.getFileExtension(source);
+
+    /**
+     * 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(source) + " | tar xv";
-        if ("tgz".equalsIgnoreCase(ext) || source.toLowerCase().endsWith(".tar.gz"))
-            return downloadToStdout(source) + " | tar xvz";
-        
-        String target = FilenameUtils.getName(source);
+            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(source, target), 
+                BashCommands.commandToDownloadUrlAs(sourceUrl, target),
                 "unzip "+target,
                 "rm "+target);
-        
-        throw new UnsupportedOperationException("No way to expand "+source+" (yet)");
+
+        throw new UnsupportedOperationException("No way to expand "+sourceUrl+" (yet)");
     }
-    
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/java/brooklyn/entity/salt/SaltConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/salt/SaltConfig.java b/software/base/src/main/java/brooklyn/entity/salt/SaltConfig.java
index 5c48487..7d2fbb3 100644
--- a/software/base/src/main/java/brooklyn/entity/salt/SaltConfig.java
+++ b/software/base/src/main/java/brooklyn/entity/salt/SaltConfig.java
@@ -18,7 +18,9 @@ import com.google.common.base.Functions;
 public interface SaltConfig {
 
     MapConfigKey<String> SALT_FORMULAS = new MapConfigKey<String>(String.class,
-            "salt.formulaUrls", "Map of Salt formulaURLs (normally GutHub repositories from the salt-formulas user)");
+            "salt.formulaUrls", "Map of Salt formulaURLs (normally GutHub repository archives from the salt-formulas user)");
+    SetConfigKey<String> SALT_RUN_LIST = new SetConfigKey<String>(String.class, "salt.runList", "TODO");
+    MapConfigKey<Object> SALT_LAUNCH_ATTRIBUTES = new MapConfigKey<Object>(Object.class, "salt.launch.attributes", "TODO");
 
     @SetFromFlag("master")
     ConfigKey<SaltStackMaster> MASTER = ConfigKeys.newConfigKey(SaltStackMaster.class,
@@ -27,6 +29,17 @@ public interface SaltConfig {
     AttributeSensor<String> MINION_ID = new BasicAttributeSensor<String>(String.class,
             "salt.minion.id", "The ID for a Salt minion");
 
+    @SetFromFlag("masterConfigUrl")
+    ConfigKey<String> MASTER_CONFIGURATION_URL = ConfigKeys.newStringConfigKey(
+            "salt.master.config.template.url", "The Salt master configuration file template URL",
+            "classpath://brooklyn/entity/salt/master");
+
+    @SetFromFlag("minionConfigUrl")
+    ConfigKey<String> MINION_CONFIGURATION_URL = ConfigKeys.newStringConfigKey(
+            "salt.minion.config.template.url", "The Salt minion configuration file template URL",
+            "classpath://brooklyn/entity/salt/masterless");
+    // TODO allow choice between this and "classpath://brooklyn/entity/salt/minion" template
+
     @SuppressWarnings({ "unchecked", "rawtypes" })
     @SetFromFlag("minionIdFunction")
     ConfigKey<Function<Entity, String>> MINION_ID_FUNCTION = new BasicConfigKey(Function.class,

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/java/brooklyn/entity/salt/SaltConfigs.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/salt/SaltConfigs.java b/software/base/src/main/java/brooklyn/entity/salt/SaltConfigs.java
index ef3aec6..637eb56 100644
--- a/software/base/src/main/java/brooklyn/entity/salt/SaltConfigs.java
+++ b/software/base/src/main/java/brooklyn/entity/salt/SaltConfigs.java
@@ -17,14 +17,16 @@ import com.google.common.base.Preconditions;
 @Beta
 public class SaltConfigs {
 
-    public static void addToRunList(EntitySpec<?> entity, String ...recipes) {
-        for (String recipe: recipes)
-            entity.configure(SaltConfig.SALT_RUN_LIST, SetModifications.addItem(recipe));
+    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 ...recipes) {
-        for (String recipe: recipes)
-            entity.setConfig(SaltConfig.SALT_RUN_LIST, SetModifications.addItem(recipe));
+    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) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java b/software/base/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
index 9ca26ca..908c2e3 100644
--- a/software/base/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
+++ b/software/base/src/main/java/brooklyn/entity/salt/SaltLifecycleEffectorTasks.java
@@ -20,25 +20,26 @@ 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.
+/**
+ * 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;
@@ -51,39 +52,41 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
         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");
+                    "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(AbstractSoftwareProcessSshDriver.BROOKLYN_HOME_DIR, "salt-install");
-        String runDir = Urls.mergePaths(AbstractSoftwareProcessSshDriver.BROOKLYN_HOME_DIR, 
+        String runDir = Urls.mergePaths(AbstractSoftwareProcessSshDriver.BROOKLYN_HOME_DIR,
                 "apps/"+entity().getApplicationId()+"/salt-entities/"+entity().getId());
-        
+
         DynamicTasks.queue(
-                SaltTasks.installSaltMinion(installDir, false), 
-                SaltTasks.installFormulas(installDir, SaltConfigs.getRequiredConfig(entity(), SALT_FORMULAS), false));
-        
-        DynamicTasks.queue(SaltTasks.buildSaltFile(runDir, installDir, "launch", 
-                SaltConfigs.getRequiredConfig(entity(), SALT_RUN_LIST),
-                entity().getConfig(SALT_LAUNCH_ATTRIBUTES)));
-        
-        DynamicTasks.queue(SaltTasks.runSalt(runDir, "launch"));
+                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));
     }
 
     @Override
@@ -96,10 +99,10 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
             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()) {
@@ -107,14 +110,14 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
         }
 
         // and set the PID
-        entity().setAttribute(Attributes.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())) {
@@ -126,7 +129,7 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
 
     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())) {
@@ -146,14 +149,14 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
         }
         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)");
     }
 
@@ -162,16 +165,16 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
         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 
+            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");
         }
@@ -179,4 +182,14 @@ public class SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks im
         return true;
     }
 
+    /**
+     * Get the Salt master entity if it exists.
+     * <p>
+     * Returns {@literal null} by default.
+     */
+    @Override
+    public SaltStackMaster getMaster() {
+        return master;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/java/brooklyn/entity/salt/SaltTasks.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/salt/SaltTasks.java b/software/base/src/main/java/brooklyn/entity/salt/SaltTasks.java
index 19e814e..39ac3c0 100644
--- a/software/base/src/main/java/brooklyn/entity/salt/SaltTasks.java
+++ b/software/base/src/main/java/brooklyn/entity/salt/SaltTasks.java
@@ -5,19 +5,19 @@ import static brooklyn.util.ssh.BashCommands.*;
 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;
-import com.google.common.collect.ImmutableList;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 
 @Beta
 public class SaltTasks {
@@ -31,24 +31,48 @@ public class SaltTasks {
                         INSTALL_CURL,
                         INSTALL_TAR,
                         INSTALL_UNZIP,
-                        "( "+downloadToStdout(boostrapUrl) + " | " + sudo("sudo sh -s -- -M -N "+version)+" )"));
+                        "( "+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(Entity minion, String saltDirectory, boolean force) {
-        String boostrapUrl = minion.getConfig(SaltStackMaster.BOOTSTRAP_URL);
-        String installCmd = cdAndRun(saltDirectory, 
-                BashCommands.chain(
+    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("sudo sh")+" )"));
-        if (!force) installCmd = BashCommands.alternatives("which salt-minion", installCmd);
-        return SshEffectorTasks.ssh(installCmd).summary("install salt minion");
+                                "( "+downloadToStdout(boostrapUrl) + " | " + sudo("sh")+" )"));
+                        if (!force) installCmd = BashCommands.alternatives("which salt-minion", installCmd);
+
+                        // Process the minion configuration template
+                        String 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 saltDirectory, final Map<String,String> formulasAndUrls, final boolean force) {
+    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() {
@@ -56,52 +80,42 @@ public class SaltTasks {
                         if (formulasAndUrls==null)
                             throw new IllegalStateException("No formulas defined to install at "+e);
                         for (String formula: formulasAndUrls.keySet())
-                            DynamicTasks.queue(installFormula(saltDirectory, formula, formulasAndUrls.get(formula), force));
+                            DynamicTasks.queue(installFormula(installDir, formula, formulasAndUrls.get(formula), force));
                     }
                 }).buildFactory();
     }
 
-    public static TaskFactory<?> installFormula(String saltDirectory, String formula, String url, boolean force) {
-        // TODO if it's server, try knife first
-        // TODO support downloads from classpath / local server
-        return SshEffectorTasks.ssh(cdAndRun(saltDirectory, SaltBashCommands.downloadAndExpandFormula(url, formula, force))).
-                summary("install formula "+formula).requiringExitCodeZero();
+    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,
+        return BashCommands.chain(
+                "mkdir -p "+targetDirectory,
                 "cd "+targetDirectory,
                 command);
     }
 
-    public static TaskFactory<?> buildSaltFile(String runDirectory, String saltDirectory, String phase, Iterable<? extends String> runList,
-            Map<String, Object> optionalAttributes) {
-        // TODO if it's server, try knife first
-        // TODO configure add'l properties
-        String phaseRb = 
-                "root = File.absolute_path(File.dirname(__FILE__))\n"+
-                "\n"+
-                "file_cache_path root\n"+
-//                "formula_path root + '/formulas'\n";
-                "formula_path '"+saltDirectory+"'\n";
-
-        Map<String,Object> phaseJsonMap = MutableMap.of();
-        if (optionalAttributes!=null)
-            phaseJsonMap.putAll(optionalAttributes);
-        if (runList!=null)
-            phaseJsonMap.put("run_list", ImmutableList.copyOf(runList));
-        Gson json = new GsonBuilder().create();
-        String phaseJson = json.toJson(phaseJsonMap);
+    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 Tasks.sequential("build salt files for "+phase,
-                    SshEffectorTasks.put(Urls.mergePaths(runDirectory)+"/"+phase+".rb").contents(phaseRb).createDirectory(),
-                    SshEffectorTasks.put(Urls.mergePaths(runDirectory)+"/"+phase+".json").contents(phaseJson));
+        return SshEffectorTasks.put(Urls.mergePaths(runDir, "base", "top.sls"))
+                .contents(top.toString())
+                .summary("build salt top file")
+                .createDirectory();
     }
 
-    public static TaskFactory<?> runSalt(String runDir, String phase) {
-        // TODO salt server
-        return SshEffectorTasks.ssh(cdAndRun(runDir, "sudo salt-solo -c "+phase+".rb -j "+phase+".json -ldebug")).
-                summary("run salt for "+phase).requiringExitCodeZero();
+    public static TaskFactory<?> runSalt(String runDir) {
+        return SshEffectorTasks.ssh(cdAndRun(runDir, BashCommands.sudo("salt-call --local state.highstate")))
+                .summary("run salt install")
+                .requiringExitCodeZero();
     }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/resources/brooklyn/entity/salt/master
----------------------------------------------------------------------
diff --git a/software/base/src/main/resources/brooklyn/entity/salt/master b/software/base/src/main/resources/brooklyn/entity/salt/master
index 1b3a95e..ce07663 100644
--- a/software/base/src/main/resources/brooklyn/entity/salt/master
+++ b/software/base/src/main/resources/brooklyn/entity/salt/master
@@ -38,4 +38,13 @@ log_level_logfile: debug # Debugging
 #open_mode: False
 #auto_accept: False
 #autosign_file: autosign.conf
-#permissive_pki_access: False
\ No newline at end of file
+#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/b67d8bd0/software/base/src/main/resources/brooklyn/entity/salt/masterless
----------------------------------------------------------------------
diff --git a/software/base/src/main/resources/brooklyn/entity/salt/masterless b/software/base/src/main/resources/brooklyn/entity/salt/masterless
new file mode 100644
index 0000000..03200d9
--- /dev/null
+++ b/software/base/src/main/resources/brooklyn/entity/salt/masterless
@@ -0,0 +1,37 @@
+[#ftl]
+##
+# SaltStack Masterless Minion Configuration
+# /etc/salt/minion
+##
+
+#default_include: minion.d/*.conf
+
+# Minion configuration
+id: ${entity.id}
+user: root
+backup_mode: minion
+
+# Directory settings
+root_dir: ${runDir}/
+pidfile: salt-minion.pid
+pki_dir: pki
+cachedir: cache
+sock_dir: sock
+log_file: minion.log
+key_logfile: key.log
+
+file_client: local
+file_roots:
+  base:
+    - ${installDir}/base
+[#list formulas.entrySet() as entry]
+    - ${installDir}/base/${entry.key}
+[/#list]
+
+#verify_env: True
+#cache_jobs: True # Debugging
+
+output: nested
+color: False
+log_level: info
+log_level_logfile: debug # Debugging
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/resources/brooklyn/entity/salt/minion
----------------------------------------------------------------------
diff --git a/software/base/src/main/resources/brooklyn/entity/salt/minion b/software/base/src/main/resources/brooklyn/entity/salt/minion
index 16c01f4..119c885 100644
--- a/software/base/src/main/resources/brooklyn/entity/salt/minion
+++ b/software/base/src/main/resources/brooklyn/entity/salt/minion
@@ -16,8 +16,8 @@ acceptance_wait_time_max: 300
 
 # Minion configuration
 id: ${entity.id}
-#user: root
-#backup_mode: minion
+user: root
+backup_mode: minion
 
 # Directory settings
 root_dir: ${runDir}/

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/base/src/main/resources/brooklyn/entity/salt/top.sls
----------------------------------------------------------------------
diff --git a/software/base/src/main/resources/brooklyn/entity/salt/top.sls b/software/base/src/main/resources/brooklyn/entity/salt/top.sls
new file mode 100644
index 0000000..bc36359
--- /dev/null
+++ b/software/base/src/main/resources/brooklyn/entity/salt/top.sls
@@ -0,0 +1 @@
+[#ftl]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/b67d8bd0/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
----------------------------------------------------------------------
diff --git a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
index 339a7c8..66d5b85 100644
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNodeSaltImpl.java
@@ -36,7 +36,7 @@ public class PostgreSqlNodeSaltImpl extends EffectorStartableImpl implements Pos
             .description("invokes a script")
             .parameter(ExecuteScriptEffectorBody.SCRIPT)
             .impl(new ExecuteScriptEffectorBody()).build();
-    
+
     private SshFeed feed;
 
     @Override
@@ -44,34 +44,35 @@ public class PostgreSqlNodeSaltImpl extends EffectorStartableImpl implements Pos
         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(), "postgresql", "https://github.com/saltstack-formulas/postgres-formula");
+            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.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();
@@ -82,33 +83,33 @@ public class PostgreSqlNodeSaltImpl extends EffectorStartableImpl implements Pos
             if (creationScriptUrl != null)
                 creationScript = new ResourceUtils(entity()).getResourceAsString(creationScriptUrl);
             else creationScript = entity().getConfig(PostgreSqlNode.CREATION_SCRIPT_CONTENTS);
-            entity().invoke(PostgreSqlNodeSaltImpl.EXECUTE_SCRIPT, 
+            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 -")))
+                            parameters.get(SCRIPT),
+                            BashCommands.sudoAsUser("postgres", "psql --file -")))
                     .requiringExitCodeZero()).getStdout();
         }
     }
-    
+
     protected void connectSensors() {
         setAttribute(DB_URL, String.format("postgresql://%s:%s/", getAttribute(HOSTNAME), getAttribute(POSTGRESQL_PORT)));