Skip to content

Instantly share code, notes, and snippets.

@hugoduncan
Created March 16, 2011 18:17
Show Gist options
  • Save hugoduncan/872992 to your computer and use it in GitHub Desktop.
Save hugoduncan/872992 to your computer and use it in GitHub Desktop.
pallet.action
(ns pallet.action
"Actions implement the conversion of phase functions to script and other
execution code.
An action has a :type. Known types include :script/bash and :fn/clojure.
An action has a :location, :origin for execution on the node running
pallet, and :target for the target node.
An action has an :execution, which is one of :aggregated, :in-sequence or
:collected. Calls to :aggregated actions will be grouped, and run before
:in-sequence actions. Calls to :collected actions will be grouped, and run
after :in-sequence actions."
{:author "Hugo Duncan"}
(:require
[pallet.action-plan :as action-plan]
[pallet.argument :as argument]
[pallet.request-map :as request-map]
[clojure.contrib.condition :as condition]
[clojure.contrib.def :as ccdef]
[clojure.contrib.logging :as logging]
[clojure.contrib.seq :as seq]
[clojure.string :as string]))
;;; action defining functions
(defn schedule-action
"Registers an action in the action plan. The action is generated by the
specified action function and arguments that will be applied to the function
when the action plan is executed.
The action can be scheduled within one of three 'executions'
(conceptually, sub-phases):
:in-sequence - The generated action will be applied to the node
\"in order\", as it is defined lexically in the source crate.
This is the default.
:aggregated - All aggregated actions are applied to the node
in the order they are defined, but before all :in-sequence
actions. Note that all of the arguments to any given
action function are gathered such that there is only ever one
invocation of each fn within each phase.
:collected - All collected actions are applied to the node
in the order they are defined, but after all :in-sequence
action. Note that all of the arguments to any given
action function are gathered such that there is only ever one
invocation of each fn within each phase.
The action-type determines how the action should be handled:
:script/bash - action produces bash script for execution on remote machine
:fn/clojure - action is a function for local execution
:transfer/to-local - action is a function specifying remote source
and local destination.
:transfer/from-local - action is a function specifying local source
and remote destination."
[request action-fn args execution action-type location]
{:pre [request
(keyword? (request-map/phase request))
(keyword? (request-map/target-id request))]}
(update-in
request
(action-plan/target-path request)
action-plan/add-action
(action-plan/action-map action-fn args execution action-type location)))
(defmacro action
"Define an anonymous action"
[execution action-type location [request & args] & body]
(let [meta-map (when (and (map? (first body)) (> (count body) 1))
(first body))
body (if meta-map (rest body) body)]
`(let [f# (vary-meta (fn [~request ~@args] ~@body) merge ~meta-map)]
(vary-meta
(fn [& [request# ~@args :as argv#]]
(schedule-action
request# f# (rest argv#) ~execution ~action-type ~location))
merge
~meta-map
{::action-fn f#}))))
(defn action-fn
"Retrieve the action-fn that is used to execute the specified action."
[action]
(::action-fn (meta action)))
;;; Convenience action definers for common cases
(defmacro bash-action
"Define a remotely executed bash action function."
[[request & args] & body]
`(action :in-sequence :script/bash :target [~request ~@args] ~@body))
(defmacro clj-action
"Define a clojure action to be executed on the origin machine."
[[request & args] & body]
`(action :in-sequence :fn/clojure :origin [~request ~@args] ~@body))
(defmacro aggregated-action
"Define a remotely executed aggregated action function, which will
be executed before :in-sequence actions."
[[request & args] & body]
`(action :aggregated :script/bash :target [~request ~@args] ~@body))
(defmacro collected-action
"Define a remotely executed collected action function, which will
be executed after :in-sequence actions."
[[request & args] & body]
`(action :collected :script/bash :target [~request ~@args] ~@body))
(defmacro as-clj-action
"An adaptor for using a normal function as a local resource function"
([f [request & args]]
`(clj-action
[~request ~@(map (comp symbol name) args)]
(~f ~request ~@(map (comp symbol name) args))))
([f]
`(as-clj-action
~f [~@(first (:arglists (meta (var-get (resolve f)))))])))
(defmacro def-action-def
"Define a macro for definining action defining vars"
[name actionfn1]
`(defmacro ~name
{:arglists '(~'[name [request & args] & body]
~'[name [request & args] meta? & body])}
[name# ~'& args#]
(let [[name# args#] (ccdef/name-with-attributes name# args#)
arglist# (first args#)
body# (rest args#)
meta-map# (when (and (map? (first body#)) (> (count body#) 1))
(first body#))
name# (vary-meta
name#
#(merge
{:arglists (list 'quote (list arglist#))}
meta-map#
%))]
`(def ~name# (~'~actionfn1 [~@arglist#] ~@body#)))))
(def-action-def def-bash-action pallet.action/bash-action)
(def-action-def def-clj-action pallet.action/clj-action)
(def-action-def def-aggregated-action pallet.action/aggregated-action)
(def-action-def def-collected-action pallet.action/collected-action)
(defn enter-scope
"Enter a new action scope."
[request]
(update-in request (action-plan/target-path request) action-plan/push-block))
(defn leave-scope
"Leave the current action scope."
[request]
(update-in request (action-plan/target-path request) action-plan/pop-block))
@tbatchelli
Copy link

Hi Hugo, some stream of thought comments, so take them for what they're worth...

can you have local bash actions? I find that bash-action and local-action are not very good names because you are breaking the namespace by different axis. remote-bash-action? local-clj-action?

I also find 'local' to be quite an ambiguous term, I think we talked about this at some point. Target is not ambiguous and it means the same. Something could be local to the target node, so i get confused with local because from the point of view of the scripts being executed, local means the target node.

pre-phase -> pre-phase-name (it returns a name)
execute-pre-phase -> schedule-pre-phase (it does not execute anything)
plan-action -> schedule-action (puts this action somewhere so it gets ultimately executed. The planning happens afterwards, no?)

'execute' is an overloaded term. execute-pre-phase vs. pallet-action/execute. I think they mean very different things.

phase-list doesn't say much about what it does; it gets a phase list and returns a phase list, but those phase lists are not the same (hopefully). Canonical-phase-list maybe?

built-action-plan vs. action-plan. The naming here is confusing too. I can't really tell the difference.

In general, I would say that any function that is public is meant to be used by users of pallet, and therefore it's name should clearly indicate what the function does, at least at the right level of abstraction (note that I consider the namespace as part of the name, so action/get-fn is equivalent to me to action/action-fn, event though you could argue that get-fn is not very precise). You might disagree with this (most of people I know do), but I prefer longer-but-descriptive names, like 'action-plan-for-phase-and-group' and 'action-plan-for-phase-and-node', or 'phase-and-group-action-plan' and 'phase-and-node-action-plan' (names chosen after the functions' docstrings). If the function is not public, I care less about it. But it is hard for me to tell from the code which one is meant to be used only internally to pallet, and which ones can/should be used in crates.

Finally, note that if names needed to be too long to be descriptive enough this could be an indicator of other problems, like using wrong abstractions, or maybe opportunities to break a fn into smaller and conceptually simpler fns.

I hope this helps

Finally, a lot of action-plan names in the 'action' package, when there is an 'action-plan' package...

@hugoduncan
Copy link
Author

hugoduncan commented Mar 18, 2011 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment