Created
March 16, 2011 18:17
-
-
Save hugoduncan/872992 to your computer and use it in GitHub Desktop.
pallet.action
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)) |
On Fri, 18 Mar 2011 03:11:33 -0400, tbatchelli ***@***.*** wrote:
Hi Hugo, some stream of thought comments, so take them for what they're
worth...
Thanks for taking the time to do this - a fresh set of eyes definitely
helps :)
It seems that there are many naming issues:
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?
Maybe we can have bash-action, local-bash-action, local-clj-action (modulo
a different local term)
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.
local, target, central,
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.
Agreed re pre-phase-name.
I like the idea of schedule. schedule-action definitely makes sense.
How about
in-pre-phase,
in-post-phase?
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?
I don't think canonical is quite right, as it's result couldn't be passed
to it's input. How about:
full-phase-list,
phase-list-with-implicit-phases.
built-action-plan vs. action-plan. The naming here is confusing too. I
can't really tell the difference.
one actually constructs the action plan from the phase, the other just
returns
what has already been constructed. Confusing I agree. How about:
```
get-action-plan-for-target - retrieve the action plan
assoc-action-plan-for-target - assoc the action plan
```
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).
I agree in principle - I am a big fan of [require … :as …]. I think
naming uses a different part of the brain than coding, so you can't do one
well while doing the other (at least for me).
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).
I also like long names - particularly avoiding arbitrary contractions -
but we should still spend time on trying to find short names that remain
descriptive.
If the function is not public, I care less about it.
But still important.
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, a lot of action-plan names in the 'action' package, when there
is an 'action-plan' package...
I take the above points together - the split of namespaces is wrong at the
moment.
The public interface should reflect expected usage by the consumer. The
action namespace at the moment has two consumers - core and crates.
pallet.action-plan.tree - the current action-plan - the data structure
used for the action plan
pallet.action-plan - the mapping between request map and action-tree
pallet.action - the definition of bash-fn, et al.
I hope this helps
## Definitely does :)
Hugo Duncan
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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...