├── downloads └── control-0.4.1-standalone.jar ├── update_doc.sh ├── .gitignore ├── project.clj ├── samples └── control.clj ├── LICENSE ├── test └── control │ └── test │ ├── commands.clj │ └── core.clj ├── src ├── control │ ├── commands.clj │ ├── main.clj │ └── core.clj └── leiningen │ └── control.clj ├── README.md └── bin └── control /downloads/control-0.4.1-standalone.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/killme2008/clojure-control/HEAD/downloads/control-0.4.1-standalone.jar -------------------------------------------------------------------------------- /update_doc.sh: -------------------------------------------------------------------------------- 1 | lein deps 2 | lein autodoc 3 | cd autodoc 4 | git add -A 5 | git commit -m"Documentation update" 6 | git push origin gh-pages -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | .lein-deps-sum 6 | .lein-failures 7 | *.swp 8 | autodoc/** 9 | .DS_Store 10 | build/ 11 | doc/ 12 | docs/ 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject control/control "0.4.2-SNAPSHOT" 2 | :lein-release {:deploy-via :clojars} 3 | :dependencies [[org.clojure/clojure "1.4.0"] 4 | [org.clojure/tools.cli "0.2.1"]] 5 | :author "dennis zhuang(killme2008@gmail.com)" 6 | :profiles {:dev {:dependencies [[codox "0.5.0"]]}} 7 | :url "https://github.com/killme2008/clojure-control" 8 | :main control.main 9 | :min-lein-version "2.0.0" 10 | :shell-wrapper {:bin "bin/clojure-control", :main control.main} 11 | :plugins [[lein-exec "0.1"] 12 | [lein-marginalia "0.7.0"] 13 | [lein-autodoc "0.9.0"]] 14 | :description "A clojure DSL for system admin and deployment with many remote machines") 15 | -------------------------------------------------------------------------------- /samples/control.clj: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; 3 | ;; A quick example for clojure-control 4 | ;; 5 | ;; 6 | ;;define clusters 7 | (defcluster :mycluster 8 | :clients [ 9 | { :host "a.domain.com" :user "alogin"} 10 | { :host "b.domain.com" :user "blogin"} 11 | ]) 12 | 13 | (define :all 14 | :ssh-options "-p 44" 15 | :scp-options "-v" 16 | :rsync-options "-i" 17 | :parallel true 18 | :user "deploy" 19 | :addresses ["a.domain.com" "b.domain.com"]) 20 | 21 | ;;define tasks 22 | (deftask :date "Get date" 23 | [] 24 | (ssh "date")) 25 | 26 | (deftask :build "Run build command on server" 27 | [] 28 | (ssh (cd "/home/alogin/src" 29 | (path "/home/alogin/tools/bin/" 30 | (env "JAVA_OPTS" "-XMaxPermSize=128m" 31 | (run "./build.sh")))))) 32 | 33 | (deftask :deploy "scp files to remote machines" 34 | [file1 file2] 35 | (scp [file1 file2] "/home/alogin/") 36 | (ssh (str "tar zxvf " file1)) 37 | (ssh (str "tar zxvf " file2))) 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Dennis Zhuang (MIT License) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/control/test/commands.clj: -------------------------------------------------------------------------------- 1 | (ns control.test.commands 2 | (:use [control.core]) 3 | (:use [control.commands]) 4 | (:use [clojure.test])) 5 | 6 | (deftest test-cd 7 | (is (= "cd /home/sun ; " (cd "/home/sun")))) 8 | 9 | (deftest test-path 10 | (is (= "export PATH=/home/sun/bin:$PATH ; " (path "/home/sun/bin")))) 11 | 12 | (deftest test-env-run 13 | (is (= "JAVA_OPTS=-XMaxPermSize=128m java -version ; " 14 | (env "JAVA_OPTS" "-XMaxPermSize=128m" 15 | (run "java -version"))))) 16 | 17 | (deftest test-cd-run 18 | (is (= "cd /home/sun ; ls ; " 19 | (cd "/home/sun" 20 | (run "ls"))))) 21 | 22 | (deftest test-cd-prefix-run 23 | (is (= "cd /home/sun ; source ~/.bash_profile && ls ; " 24 | (cd "/home/sun" 25 | (prefix "source ~/.bash_profile" 26 | (run "ls")))))) 27 | 28 | (deftest test-sudo 29 | (is (= "sudo service tomcat start ; " 30 | (sudo "service tomcat start")))) 31 | 32 | (deftest test-append 33 | (is (= "echo 'hello world' >> test.txt ; " 34 | (append "test.txt" "hello world"))) 35 | (is (= "echo 'hello world' | sudo tee -a test.txt ; " 36 | (append "test.txt" "hello world" :sudo true)))) 37 | 38 | 39 | (deftest test-sed 40 | (is (= "sed -i.back -r -e \"3 s/hello/world/g\" test ; " 41 | (sed "test" "hello" "world" :limit 3 :backup ".back"))) 42 | (is (= "sudo sed -i.bak -r -e \" s/hello/world/g\" test ; " 43 | (sed "test" "hello" "world" :sudo true)))) 44 | 45 | (deftest test-comm 46 | (is (= "sed -i.bak -r -e \" s/hello\\sworld/#&/g\" test ; " 47 | (comm "test" "hello\\sworld"))) 48 | (is (= "sed -i.bak -r -e \" s/hello\\sworld/;&/g\" test ; " 49 | (comm "test" "hello\\sworld" :char ";")))) 50 | 51 | (deftest test-uncomm 52 | (is (= "sed -i.bak -r -e \" s/\\s*#+\\s*(hello\\sworld)/\\1/g\" test ; " 53 | (uncomm "test" "hello\\sworld"))) 54 | (is (= "sed -i.bak -r -e \" s/\\s*;+\\s*(hello\\sworld)/\\1/g\" test ; " 55 | (uncomm "test" "hello\\sworld" :char ";")))) 56 | 57 | -------------------------------------------------------------------------------- /src/control/commands.clj: -------------------------------------------------------------------------------- 1 | (ns #^{:doc "A set of DSL for ssh, inspired by Fabric, 2 | please see https://github.com/killme2008/clojure-control/wiki/commands" 3 | :author "Sun Ning Dennis Zhuang"} 4 | control.commands) 5 | 6 | (def SEP " ; ") 7 | (defmacro path 8 | "modify shell path" 9 | [new-path & cmd] 10 | `(str "export PATH=" ~new-path ":$PATH" SEP ~@cmd)) 11 | 12 | (defmacro cd 13 | "change current directory" 14 | [path & cmd] 15 | `(str "cd " ~path SEP ~@cmd)) 16 | 17 | (defmacro prefix 18 | "execute a prefix command, for instance, activate shell profile" 19 | [pcmd & cmd] 20 | `(str ~pcmd " && " ~@cmd)) 21 | 22 | (defmacro env 23 | "declare a env variable for next command" 24 | [key val & cmd] 25 | `(str ~key "=" ~val " " ~@cmd)) 26 | 27 | (defn run 28 | "simply run several commands" 29 | [ & cmds] 30 | (let [rt (apply str cmds)] 31 | (if (.endsWith rt SEP) 32 | rt 33 | (str rt SEP)))) 34 | 35 | (defmacro sudo 36 | "run a command with sudo" 37 | [cmd] 38 | `(if (.endsWith ~cmd SEP) 39 | (str "sudo " ~cmd) 40 | (str "sudo " ~cmd SEP))) 41 | 42 | (defn append 43 | "Append a line to a file" 44 | [file line & opts] 45 | (let [m (apply hash-map opts) 46 | escaple (:escaple m) 47 | sudo (:sudo m)] 48 | (if sudo 49 | (str "echo '" line "' | sudo tee -a " file SEP) 50 | (str "echo '" line "' >> " file SEP)))) 51 | 52 | (defn sed- 53 | [file before after flags backup limit] 54 | (str "sed -i" backup " -r -e \"" limit " s/" before "/" after "/" flags "\" " file SEP)) 55 | 56 | (defn sed 57 | "Use sed to replace strings matched pattern with options.Valid options include: 58 | :sudo => true or false to use sudo,default is false. 59 | :flags => sed options,default is nil. 60 | :limit => sed limit,default is not limit. 61 | :backup => backup file posfix,default is \".bak\" 62 | Equivalent to sed -i -r -e \"// s///g \"." 63 | 64 | [file before after & opts] 65 | (let [opts (apply hash-map opts) 66 | use-sudo (:sudo opts) 67 | flags (str (:flags opts) "g") 68 | backup (or (:backup opts) ".bak") 69 | limit (:limit opts)] 70 | (if use-sudo 71 | (sudo (sed- file before after flags backup limit)) 72 | (sed- file before after flags backup limit)))) 73 | 74 | (defn comm 75 | "Comments a line in a file with special character,default :char is \"#\" 76 | It use sed function to replace the line matched pattern, :sudo is also valid" 77 | [file pat & opts] 78 | (let [m (apply hash-map opts) 79 | char (or (:char m) "#")] 80 | (apply sed file pat (str char "&") opts))) 81 | 82 | (defn uncomm 83 | "uncomment a line in a file" 84 | [file pat & opts] 85 | (let [m (apply hash-map opts) 86 | char (or (:char m) "#")] 87 | (apply sed file (str "\\s*" char "+\\s*(" pat ")") "\\1" opts))) 88 | 89 | (defn cat 90 | "cat a file" 91 | [file] 92 | (str "cat " file)) 93 | 94 | (defn chmod 95 | "chmod [mod] [file]" 96 | [mod file] 97 | (str "chmod " mod " " file SEP)) 98 | 99 | -------------------------------------------------------------------------------- /src/leiningen/control.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.control 2 | #^{ :doc "Clojure control leiningen plugin" 3 | :author "Sun Ning Dennis Zhuang "} 4 | (:use [control.core :only [do-begin clusters ssh-client]] 5 | [clojure.string :only [join]] 6 | [clojure.tools.cli :only [cli]] 7 | [leiningen.help :only [help-for]] 8 | [clojure.java.io :only [file reader writer]])) 9 | 10 | (defn- get-config [project key] 11 | (get-in project [:control key])) 12 | 13 | (defn- create-control-ns [] 14 | (create-ns (gensym "user-control"))) 15 | 16 | (defn- load-control-file [project] 17 | (try 18 | (binding [*ns* (create-control-ns)] 19 | (refer-clojure) 20 | (use '[control core commands]) 21 | (load-file 22 | (or 23 | (get-config project :control-file) 24 | "./control.clj"))) 25 | (catch java.io.FileNotFoundException e (println "control file not found.")))) 26 | 27 | (defn- run-control [project args] 28 | (do 29 | (load-control-file project) 30 | (do-begin args))) 31 | 32 | (defn- handle-conn 33 | [^java.net.Socket socket] 34 | (with-open [s socket 35 | rdr (reader socket) 36 | wtr (writer socket)] 37 | (try 38 | (let [line (.readLine rdr) 39 | args (seq (.split line " ")) 40 | rt (with-out-str 41 | (do-begin (vec args)))] 42 | (spit wtr rt)) 43 | (catch Throwable e 44 | (spit wtr (.getMessage e)))))) 45 | 46 | (defn server 47 | "Start a control server for handling requests: 48 | -p [--port] port , listen on which port" 49 | [project & args] 50 | (load-control-file project) 51 | (let [[options _ banner] 52 | (cli args 53 | ["-p" "--port" "Which port to listen on." :default 8123 54 | :parse-fn #(Integer/parseInt %)]) 55 | {:keys [port]} options 56 | ^java.net.ServerSocket ss (java.net.ServerSocket. port) 57 | server (agent ss)] 58 | (set-error-handler! server 59 | (fn [_ e] 60 | (.printStackTrace e))) 61 | (send-off server 62 | (fn [^java.net.ServerSocket ss] 63 | (let [s (.accept ss)] 64 | (handle-conn s) 65 | (recur ss)))) 66 | (await server))) 67 | 68 | (defn init 69 | "Initialize clojure-control, create a sample control file in project home" 70 | [project & args] 71 | (let [control-file (file "." "control.clj")] 72 | (if-not (.exists control-file) 73 | (spit control-file 74 | (str 75 | "(defcluster :default-cluster\n" 76 | " :clients [\n" 77 | " {:host \"localhost\" :user \"root\"}\n" 78 | " ])\n" 79 | "\n" 80 | "(deftask :date \"echo date on cluster\"" 81 | " []\n" 82 | " (ssh \"date\"))\n"))))) 83 | 84 | (defn run 85 | "Run user-defined clojure-control tasks against certain cluster, 86 | -r [--[no-]remote],running commands on remote control server,default is false. 87 | -p [--port] port, control server port, 88 | -h [--host] host, control server host." 89 | [project & args] 90 | (let [[options extra-args] 91 | (cli args 92 | ["-p" "--port" "Which port to connect." :default 8123 :parse-fn #(Integer/parseInt %)] 93 | ["-r" "--[no-]remote" :default false] 94 | [ "-h" "--host" "Which host to connect." :default "localhost"]) 95 | {:keys [port host remote]} options] 96 | (if-not remote 97 | (run-control project args) 98 | (let [^java.net.Socket sock (java.net.Socket. host port)] 99 | (.setTcpNoDelay sock true) 100 | (let [rdr (reader sock) 101 | wtr (writer sock)] 102 | (.write ^java.io.Writer wtr (str (join " " extra-args) "\n")) 103 | (.flush wtr) 104 | (println (slurp rdr)) 105 | (.close wtr) 106 | (.close rdr) 107 | (.close sock)))))) 108 | 109 | (defn show 110 | "Show cluster info" 111 | [project & args] 112 | (do 113 | (load-control-file project) 114 | (if-let [cluster-name (first args)] 115 | (let [ cluster ((keyword cluster-name) @clusters) 116 | user (:user cluster)] 117 | (doseq 118 | [c (:clients cluster)] 119 | (println (ssh-client (:host c) (or (:user c) user)))) 120 | (doseq 121 | [a (:addresses cluster)] 122 | (println (ssh-client a user))) 123 | (doseq 124 | [c (:includes cluster)] 125 | (println (str "Cluster " c))))))) 126 | 127 | (defn control 128 | "Leiningen plugin for Clojure-Control" 129 | {:help-arglists '([subtask [cluster task [args...]]]) 130 | :subtasks [#'init #'run #'show #'server]} 131 | ([project] 132 | (println (help-for "control"))) 133 | ([project subtask & args] 134 | (case subtask 135 | "init" (apply init project args) 136 | "run" (apply run project args) 137 | "show" (apply show project args) 138 | "server" (apply server project args) 139 | (println (help-for "control"))))) 140 | 141 | 142 | -------------------------------------------------------------------------------- /test/control/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns control.test.core 2 | (:use [control.core]) 3 | (:use [clojure.test]) 4 | (:use [clojure.string :only [blank?]])) 5 | 6 | 7 | (defn control-fixture [f] 8 | (try 9 | (f) 10 | (finally 11 | (reset! tasks (hash-map)) 12 | (reset! run-tasks #{}) 13 | (reset! clusters (hash-map))))) 14 | 15 | (use-fixtures :each control-fixture) 16 | 17 | (defn- arg-count [f] (-> f 18 | meta 19 | :arglists 20 | first 21 | count)) 22 | (deftest test-gen-log 23 | (binding [*enable-color* false] 24 | (is (= "localhost:ssh: test" (gen-log "localhost" "ssh" '("test")))) 25 | (is (= "a.domain.com:scp: hello world" (gen-log "a.domain.com" "scp" '("hello world")))))) 26 | 27 | (deftest test-ssh-client 28 | (is (= "apple@localhost" (ssh-client "localhost" "apple"))) 29 | (is (= "dennis@a.domain.com" (ssh-client "a.domain.com" "dennis")))) 30 | 31 | (deftest test-local 32 | (is (= "1\n" (:stdout (local "echo 1")))) 33 | ) 34 | 35 | (deftest test-task 36 | (is (= 0 (count @tasks))) 37 | (deftask :test "test task" 38 | [] 39 | (+ 1 2)) 40 | (is (= 1 (count @tasks))) 41 | (is (= 0 (count @clusters))) 42 | (is (function? (:test @tasks))) 43 | (is (= 3 (arg-count (:test @tasks)))) 44 | (is (= 3 ((:test @tasks) 3 4 5)))) 45 | 46 | 47 | (deftest test-cluster 48 | (is (= 0 (count @clusters))) 49 | (defcluster :mycluster 50 | :clients [{:host "a.domain.com" :user "apple"}] 51 | :addresses ["a.com" "b.com"] 52 | :user "dennis" 53 | ) 54 | (is (= 0 (count @tasks))) 55 | (is (= 1 (count @clusters))) 56 | (let [m (:mycluster @clusters)] 57 | (is (= :mycluster (:name m))) 58 | (is (= [{:host "a.domain.com" :user "apple"}] (:clients m))) 59 | (is (= "dennis" (:user m))) 60 | (is (= ["a.com" "b.com"] (:addresses m))))) 61 | 62 | 63 | (defmacro with-private-fns [[ns fns] & tests] 64 | "Refers private fns from ns and runs tests in context." 65 | `(let ~(reduce #(conj %1 %2 `(ns-resolve '~ns '~%2)) [] fns) 66 | ~@tests)) 67 | 68 | (with-private-fns [control.core [perform spawn await-process user-at-host? find-client-options make-cmd-array *global-options* create-clients]] 69 | (deftest test-make-cmd-array 70 | (is (= ["ssh" "-v" "-p 44" "user@host"] (make-cmd-array "ssh" ["-v" "-p 44"] ["user@host"]))) 71 | (is (= ["rsync" "-arz" "--delete" "src" "user@host::share"] (make-cmd-array "rsync" ["-arz" "--delete"] ["src" "user@host::share"])))) 72 | (deftest test-create-clients 73 | (is (= [{:user "deploy" :host "host"}] (create-clients "deploy@host")))) 74 | (deftest test-perform 75 | (deftask test "test-task" 76 | [a b] 77 | (+ a b)) 78 | (let [t (:test @tasks)] 79 | (is (= 7 (perform 1 2 5 t :test '(3 4)))))) 80 | (deftest test-user-at-host? 81 | (let [f (user-at-host? "host" "user")] 82 | (is (f {:user "user" :host "host"})) 83 | (is (not (f {:user "hello" :host "host"})))) 84 | ) 85 | (deftest test-set-options! 86 | (is (nil? (:ssh-options @@*global-options*))) 87 | (set-options! :ssh-options "-o ConnectTimeout=3000") 88 | (println @@*global-options*) 89 | (is (= 1 (count @@*global-options*))) 90 | (is (= "-o ConnectTimeout=3000" (:ssh-options @@*global-options*))) 91 | (clear-options!)) 92 | (deftest test-find-client-options 93 | (let [cluster1 {:ssh-options "-abc" :clients [ {:user "login" :host "a.domain.com" :ssh-options "-def"} ]} 94 | cluster2 {:addresses ["a.domain.com"]}] 95 | (is (= "-abc" (find-client-options "b.domain.com" "login" cluster1 :ssh-options))) 96 | (is (= "-abc" (find-client-options "a.domain.com" "alogin" cluster1 :ssh-options))) 97 | (is (= "-def" (find-client-options "a.domain.com" "login" cluster1 :ssh-options))) 98 | (is (nil? (find-client-options "a.domain.com" "login" cluster2 :ssh-options))) 99 | (set-options! :ssh-options "-o ConnectTimeout=3000" :user "deploy") 100 | (is (= "-o ConnectTimeout=3000" (find-client-options "a.domain.com" nil cluster2 :ssh-options))) 101 | (is (= "deploy@a.domain.com" (ssh-client "a.domain.com" nil))) 102 | (clear-options!)))) 103 | 104 | (defn not-nil? 105 | [x] 106 | (not (nil? x))) 107 | (defn myexec 108 | [h u c] 109 | (filter not-nil? c)) 110 | 111 | (with-private-fns [control.core [perform]] 112 | (deftest test-once-task 113 | (let [c (atom 0)] 114 | (deftask hello {:once true} [] (swap! c inc)) 115 | (deftask call-hello [] 116 | (call :hello) 117 | (call :hello) 118 | (call :hello)) 119 | (perform 1 2 3 (get @tasks :call-hello) :call-hello []) 120 | (is (= 1 @c)) 121 | (is (->> :hello (get @tasks) meta :once))))) 122 | 123 | (deftest test-scp 124 | (binding [exec myexec 125 | *tmp-dir* "/tmp/test/" 126 | ] 127 | (let [files ["a.text" "b.txt"]] 128 | (is (= '("scp" "-v" "a.text" "b.txt" "user@host:/tmp") 129 | (scp "host" "user" {:scp-options "-v"} files "/tmp"))) 130 | (is (= '("scp" "a.text" "b.txt" "user@host:/tmp") 131 | (scp "host" "user" nil files "/tmp"))) 132 | (is (= '("ssh" "user@host" "mv /tmp/test/* /tmp ; rm -rf /tmp/test/") 133 | (scp "host" "user" nil files "/tmp" :mode 755))) 134 | (is (= '("ssh" "user@host" "sudo mv /tmp/test/* /tmp ; rm -rf /tmp/test/") 135 | (scp "host" "user" nil files "/tmp" :sudo true :mode 755)))))) 136 | 137 | 138 | (deftest test-ssh 139 | (binding [exec myexec] 140 | (is (= '("ssh" "-v" "user@host" "date")) 141 | (ssh "host" "user" {:ssh-options "-v"} "date")) 142 | (is (= '("ssh" "user@host" "date")) 143 | (ssh "host" "user" nil "date")))) 144 | 145 | (deftest test-rsync 146 | (binding [exec myexec] 147 | (is (= '("rsync" "-v" "src" "user@host:dst")) 148 | (rsync "host" "user" {:rsync-options "-v"} "src" "dst")) 149 | (is (= '("rsync" "src" "user@host:dst")) 150 | (rsync "host" "user" nil "src" "dst")))) 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Define clusters and tasks for system administration or code deployment, then execute them on one or many remote machines. 4 | 5 | Clojure-control depends only on OpenSSH and clojure on the local control machine.Remote machines simply need a standard sshd daemon. 6 | 7 | The idea came from [node-control](https://github.com/tsmith/node-control). 8 | 9 | ##News 10 | 11 | * Control 0.4.1 released.[ReleaseNotes](https://groups.google.com/forum/?fromgroups#!topic/clojure/MLR_5VfenSs) 12 | 13 | ## Installation 14 | 15 | Clojure-Control bootstraps itself using the `control` shell script; there is no separate install script. It installs its dependencies upon the first run on unix, so the first run will take longer. 16 | 17 | * [Download the script.](https://raw.github.com/killme2008/clojure-control/master/bin/control) 18 | * Place it on your $PATH. (I like to use ~/bin) 19 | * Set it to be executable. (`chmod 755 ~/bin/control`) 20 | 21 | The link above will get you the stable release. 22 | 23 | On Windows most users can get the batch file. If you have wget.exe or curl.exe already installed and in PATH, you can just run `control self-install`, otherwise get the standalone jar from the downloads page. If you have Cygwin you should be able to use the shell script above rather than the batch file. 24 | 25 | ## Basic Usage 26 | 27 | The [tutorial](https://github.com/killme2008/clojure-control/wiki/Getting-started) has a detailed walk-through of the steps involved in creating a control project, but here are the commonly-used tasks: 28 | 29 | control init #create a sample control file in current folder 30 | control run CLUSTER TASK #run user-defined clojure-control tasks against certain cluster 31 | control show CLUSTER #show certain cluster info. 32 | 33 | Use `control help` to see a complete list. 34 | 35 | ## Getting started 36 | 37 | Creating a control file by: 38 | 39 | control init 40 | 41 | It will create a file named `control.clj` under current folder.Defines your clusters and tasks in this file,for example: 42 | 43 | ```clj 44 | (defcluster :default-cluster 45 | :clients [ 46 | {:host "localhost" :user "root"} 47 | ]) 48 | (deftask :date "echo date on cluster" [] 49 | (ssh "date")) 50 | ``` 51 | 52 | It defines a cluster named `default-cluster`,and defines a task named `date` to execute `date` command on remote machines.Run `date` task on `default-cluster` by: 53 | 54 | control run default-cluster date 55 | 56 | Output: 57 | ``` 58 | Performing default-cluster 59 | Performing date for localhost 60 | localhost:ssh: date 61 | localhost:stdout: Sun Jul 24 19:14:09 CST 2011 62 | localhost:exit: 0 63 | ``` 64 | Also,you can run the task with `user@host` instead of a pre-defined cluster: 65 | 66 | control run root@localhost date 67 | 68 | You may have to type password when running this task. You can setup ssh public keys to avoid typing a password when logining remote machines.please visit [HOWTO: set up ssh keys](http://pkeck.myweb.uga.edu/ssh/) 69 | 70 | Every task's running result is a map contains output and status,you can get them by: 71 | 72 | ```clj 73 | (let [rt (ssh "date")] 74 | (println (:status rt)) 75 | (println (:stdout rt)) 76 | (println (:stderr rt))) 77 | ``` 78 | 79 | 80 | You can do whatever you want with these values,for example,checking status is right or writing standard output to a file. 81 | 82 | ##Some practical tasks 83 | 84 | A task to ping mysql: 85 | 86 | ```clj 87 | 88 | (deftask :ping-mysql [] 89 | (let [stdout (:stdout (ssh "mysqladmin -u root -p'password' ping"))] 90 | (if (.contains stdout "is alive") 91 | 1 92 | 0))) 93 | ``` 94 | 95 | A task to deploy application: 96 | 97 | ```clj 98 | (deftask :deploy-app [] 99 | (local "tar zcvf app.tar.gz app/") 100 | (scp "app.tar.gz" "/home/user/") 101 | (ssh 102 | (run 103 | (cd "/home/user" 104 | (run 105 | (run "tar zxvf app.tar.gz") 106 | (env "JAVA_OPTS" "-XMaxPermSize=128m" 107 | (run "bin/app.sh restart"))))))) 108 | ``` 109 | 110 | Two tasks to install zookeeper c client: 111 | 112 | ```clj 113 | (deftask ldconfig 114 | [] 115 | (ssh "ldconfig" :sudo true)) 116 | 117 | (deftask install_zk_client 118 | [] 119 | (ssh 120 | (run 121 | (run "mkdir -p /home/deploy/dennis") 122 | (cd "/home/deploy/dennis" 123 | (run "wget http://labs.renren.com/apache-mirror//zookeeper/zookeeper-3.4.3/zookeeper-3.4.3.tar.gz")))) 124 | (ssh (cd "/home/deploy/dennis" 125 | (run "tar zxvf zookeeper-3.4.3.tar.gz"))) 126 | (ssh (cd "/home/deploy/dennis/zookeeper-3.4.3/src/c" 127 | (run 128 | (run "./configure --includedir=/usr/include") 129 | (run "make") 130 | (run "sudo make install")))) 131 | (call :ldconfig)) 132 | ``` 133 | 134 | ##Documents 135 | 136 | * [Getting started](https://github.com/killme2008/clojure-control/wiki/Getting-started) 137 | * [Define clusters](https://github.com/killme2008/clojure-control/wiki/Define-clusters) 138 | * [Define tasks](https://github.com/killme2008/clojure-control/wiki/Define-tasks) 139 | * [DSL commands](https://github.com/killme2008/clojure-control/wiki/commands) 140 | * [Clojure-Control shell commands](https://github.com/killme2008/clojure-control/wiki/Control-shell-commands) 141 | * [API document](http://fnil.net/clojure-control/) 142 | 143 | 144 | * [Wiki](https://github.com/killme2008/clojure-control/wiki) 145 | 146 | ## Contributors 147 | 148 | [sunng87](https://github.com/sunng87) 149 | 150 | [onycloud](https://github.com/onycloud/) 151 | 152 | [ljos](https://github.com/ljos) 153 | 154 | [dhilipsiva](https://github.com/dhilipsiva) 155 | 156 | ##License 157 | 158 | MIT licensed 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /src/control/main.clj: -------------------------------------------------------------------------------- 1 | (ns control.main 2 | (:use [control.core]) 3 | (:use [clojure.string :only [join]] 4 | [clojure.tools.cli :only [cli]] 5 | [clojure.java.io :only [file reader writer]]) 6 | (:gen-class)) 7 | 8 | (def ^:private special-control-file (atom nil)) 9 | 10 | (def ^{:private true :tag String} CONTROL-DIR "clojure-control.original.pwd") 11 | 12 | (defn- get-control-dir [] 13 | (System/getProperty CONTROL-DIR ".")) 14 | 15 | (defn- create-control-ns [] 16 | (create-ns (gensym "user-control"))) 17 | 18 | (defn- load-control-file [] 19 | (try 20 | (binding [*ns* (create-control-ns)] 21 | (refer-clojure) 22 | (use '[control core commands]) 23 | (load-file 24 | (or @special-control-file (str (get-control-dir) "/control.clj")))) 25 | (catch java.io.FileNotFoundException e (error "control file not found.")))) 26 | 27 | (defn version 28 | "Print version for Clojure-control and the current JVM." 29 | [] 30 | (println "Clojure-control" (System/getProperty "CONTROL_VER") 31 | "on Java" (System/getProperty "java.version") 32 | (System/getProperty "java.vm.name"))) 33 | 34 | (defn init 35 | "Initialize clojure-control, create a sample control file in current directory" 36 | [ & args] 37 | (let [control-file (file (get-control-dir) "control.clj")] 38 | (if (.exists control-file) 39 | (error "File control.clj exists") 40 | (do (spit control-file 41 | (str 42 | "(defcluster :default-cluster\n" 43 | " :clients [\n" 44 | " {:host \"localhost\" :user \"root\"}\n" 45 | " ])\n" 46 | "\n" 47 | "(deftask :date \"echo date on cluster\"" 48 | " []\n" 49 | " (ssh \"date\"))\n")) 50 | (println "Create file control.clj."))))) 51 | 52 | (defn show 53 | "Show cluster info" 54 | [ & args] 55 | (do 56 | (load-control-file) 57 | (if-let [cluster-name (first args)] 58 | (let [ cluster ((keyword cluster-name) @clusters) 59 | user (:user cluster)] 60 | (doseq 61 | [c (:clients cluster)] 62 | (println (ssh-client (:host c) (or (:user c) user)))) 63 | (doseq 64 | [a (:addresses cluster)] 65 | (println (ssh-client a user))) 66 | (doseq 67 | [c (:includes cluster)] 68 | (println (str "Cluster " c))))))) 69 | 70 | (defn- run-control [ & args] 71 | (do 72 | (load-control-file) 73 | (do-begin args) 74 | (shutdown-agents))) 75 | 76 | (defn- handle-conn 77 | [^java.net.Socket socket] 78 | (with-open [s socket 79 | rdr (reader socket) 80 | wtr (writer socket)] 81 | (try 82 | (let [line (.readLine rdr) 83 | args (seq (.split line " ")) 84 | rt (with-out-str 85 | (do-begin (vec args)))] 86 | (spit wtr rt)) 87 | (catch Throwable e 88 | (spit wtr (.getMessage e)))))) 89 | 90 | (defn server 91 | "Start a control server for handling requests: 92 | -p [--port] port , listen on which port" 93 | [ & args] 94 | (load-control-file) 95 | (let [[options _ banner] 96 | (cli args 97 | ["-p" "--port" "Which port to listen on." :default 8123 98 | :parse-fn #(Integer/parseInt %)]) 99 | {:keys [port]} options 100 | ^java.net.ServerSocket ss (java.net.ServerSocket. port) 101 | server (agent ss)] 102 | (set-error-handler! server 103 | (fn [_ e] 104 | (.printStackTrace e))) 105 | (send-off server 106 | (fn [^java.net.ServerSocket ss] 107 | (let [s (.accept ss)] 108 | (handle-conn s) 109 | (recur ss)))) 110 | (println (format "Control server listen at %d" port)) 111 | (await server))) 112 | 113 | (defn run 114 | "Run user-defined clojure-control tasks against certain cluster, 115 | -r [--[no-]remote],running commands on remote control server,default is false. 116 | -p [--port] port, control server port, 117 | -h [--host] host, control server host." 118 | [ & args] 119 | (let [[options extra-args] 120 | (cli args 121 | ["-p" "--port" "Which port to connect." :default 8123 :parse-fn #(Integer/parseInt %)] 122 | ["-r" "--[no-]remote" :default false] 123 | [ "-h" "--host" "Which host to connect." :default "localhost"]) 124 | {:keys [port host remote]} options] 125 | (if-not remote 126 | (apply run-control args) 127 | (let [^java.net.Socket sock (java.net.Socket. host port)] 128 | (.setTcpNoDelay sock true) 129 | (let [rdr (reader sock) 130 | wtr (writer sock)] 131 | (.write ^java.io.Writer wtr (str (join " " extra-args) "\n")) 132 | (.flush wtr) 133 | (println (slurp rdr)) 134 | (.close wtr) 135 | (.close rdr) 136 | (.close sock)))))) 137 | 138 | 139 | (defn print-help [] 140 | (println "Usage:control [-f control.clj] command args") 141 | (println "Commands available:") 142 | (println "init Initialize clojure-control, create a sample control file in current folder") 143 | (println "run Run user-defined clojure-control tasks against certain cluster") 144 | (println "show Show cluster info") 145 | (println "server Start a control server for handling requests from clients") 146 | (println "upgrade Upgrade clojure-control to a latest version.")) 147 | 148 | 149 | 150 | (defn -main [ & args] 151 | (let [[options extra-args] 152 | (cli args ["-f" "--file" "Which control file to be executed."]) 153 | {:keys [file]} options 154 | cmd (first extra-args) 155 | args (next extra-args)] 156 | (when file 157 | (reset! special-control-file file)) 158 | (case cmd 159 | "init" (apply init args) 160 | "run" (apply run args) 161 | "show" (apply show args) 162 | "server" (apply server args) 163 | "version" (apply version args) 164 | (print-help)))) 165 | -------------------------------------------------------------------------------- /bin/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #Clojure-control script. 3 | #Modified from leiningen:https://raw.github.com/technomancy/leiningen/preview/bin/lein 4 | #License: Eclipse Public License,same as leiningen and clojure. 5 | 6 | export CONTROL_VERSION="0.4.1" 7 | 8 | case $CONTROL_VERSION in 9 | *SNAPSHOT) SNAPSHOT="YES" ;; 10 | *) SNAPSHOT="NO" ;; 11 | esac 12 | 13 | if [ `id -u` -eq 0 ] && [ "$CONTROL_ROOT" = "" ]; then 14 | echo "WARNING: You're currently running as root; probably by accident." 15 | echo "Press control-C to abort or Enter to continue as root." 16 | echo "Set CONTROL_ROOT to disable this warning." 17 | read _ 18 | fi 19 | 20 | NOT_FOUND=1 21 | ORIGINAL_PWD="$PWD" 22 | while [ ! -r "$PWD/control.clj" ] && [ "$PWD" != "/" ] && [ $NOT_FOUND -ne 0 ] 23 | do 24 | cd .. 25 | if [ "$(dirname "$PWD")" = "/" ]; then 26 | NOT_FOUND=0 27 | cd "$ORIGINAL_PWD" 28 | fi 29 | done 30 | 31 | export CONTROL_HOME=${CONTROL_HOME:-"$HOME/.clojure-control"} 32 | 33 | if [ "$OSTYPE" = "cygwin" ]; then 34 | export CONTROL_HOME=`cygpath -w $CONTROL_HOME` 35 | fi 36 | 37 | CONTROL_JAR="$CONTROL_HOME/self-installs/control-$CONTROL_VERSION-standalone.jar" 38 | 39 | # normalize $0 on certain BSDs 40 | if [ "$(dirname "$0")" = "." ]; then 41 | SCRIPT="$(which $(basename "$0"))" 42 | else 43 | SCRIPT="$0" 44 | fi 45 | 46 | # resolve symlinks to the script itself portably 47 | while [ -h "$SCRIPT" ] ; do 48 | ls=`ls -ld "$SCRIPT"` 49 | link=`expr "$ls" : '.*-> \(.*\)$'` 50 | if expr "$link" : '/.*' > /dev/null; then 51 | SCRIPT="$link" 52 | else 53 | SCRIPT="$(dirname "$SCRIPT"$)/$link" 54 | fi 55 | done 56 | 57 | BIN_DIR="$(dirname "$SCRIPT")" 58 | 59 | if [ -r "$BIN_DIR/../src/control/main.clj" ]; then 60 | # Running from source checkout 61 | CONTROL_DIR="$(dirname "$BIN_DIR")" 62 | CONTROL_LIBS="$(find -H "$CONTROL_DIR/lib" -mindepth 1 -maxdepth 1 -print0 2> /dev/null | tr \\0 \:)" 63 | CLASSPATH="$CLASSPATH:$CONTROL_LIBS:$CONTROL_DIR/src:$CONTROL_DIR/classes:$CONTROL_DIR/resources:$CONTROL_JAR" 64 | 65 | if [ "$CONTROL_LIBS" = "" -a "$1" != "self-install" -a ! -r "$CONTROL_JAR" ]; then 66 | echo "Clojure control is missing its dependencies. Please see \"Building\" in the README." 67 | exit 1 68 | fi 69 | else 70 | # Not running from a checkout 71 | CLASSPATH="$CLASSPATH:$CONTROL_JAR" 72 | 73 | if [ ! -r "$CONTROL_JAR" -a "$1" != "self-install" ]; then 74 | "$0" self-install 75 | fi 76 | fi 77 | 78 | HTTP_CLIENT=${HTTP_CLIENT:-"wget -O"} 79 | if type -p curl >/dev/null 2>&1; then 80 | if [ "$https_proxy" != "" ]; then 81 | CURL_PROXY="-x $https_proxy" 82 | fi 83 | HTTP_CLIENT="curl $CURL_PROXY -f -L -o" 84 | fi 85 | 86 | export JAVA_CMD=${JAVA_CMD:-"java"} 87 | export CONTROL_JAVA_CMD=${CONTROL_JAVA_CMD:-$JAVA_CMD} 88 | 89 | # Support $JAVA_OPTS for backwards-compatibility. 90 | export JVM_OPTS="${JVM_OPTS:-"$JAVA_OPTS"}" 91 | 92 | # TODO: investigate http://skife.org/java/unix/2011/06/20/really_executable_jars.html 93 | # If you're packaging this for a package manager (.deb, homebrew, etc) 94 | # you need to remove the self-install and upgrade functionality or see lein-pkg. 95 | if [ "$1" = "self-install" ]; then 96 | if [ -r "$CONTROL_JAR" ]; then 97 | echo "The self-install jar already exists at $CONTROL_JAR." 98 | echo "If you wish to re-download, delete it and rerun \"$0 self-install\"." 99 | exit 1 100 | fi 101 | echo "Downloading Clojure-Control now..." 102 | CONTROL_DIR=`dirname "$CONTROL_JAR"` 103 | mkdir -p "$CONTROL_DIR" 104 | CONTROL_URL="https://github.com/killme2008/clojure-control/raw/master/downloads/control-$CONTROL_VERSION-standalone.jar" 105 | $HTTP_CLIENT "$CONTROL_JAR" "$CONTROL_URL" 106 | if [ $? != 0 ]; then 107 | echo "Failed to download $CONTROL_URL" 108 | echo "If you have an old version of libssl you may not have the correct" 109 | echo "certificate authority. Either upgrade or set HTTP_CLIENT to insecure:" 110 | echo " export HTTP_CLIENT=\"wget --no-check-certificate -O\" # or" 111 | echo " export HTTP_CLIENT=\"curl --insecure -f -L -o" 112 | if [ $SNAPSHOT = "YES" ]; then 113 | echo "If you have Maven installed, you can do" 114 | echo "mvn dependency:copy-dependencies; mv target/dependency lib" 115 | echo "See README.md for further SNAPSHOT build instructions." 116 | fi 117 | rm $CONTROL_JAR 2> /dev/null 118 | exit 1 119 | fi 120 | elif [ "$1" = "upgrade" ]; then 121 | if [ "$CONTROL_DIR" != "" ]; then 122 | echo "The upgrade task is not meant to be run from a checkout." 123 | exit 1 124 | fi 125 | if [ $SNAPSHOT = "YES" ]; then 126 | echo "The upgrade task is only meant for stable releases." 127 | echo "See the \"Hacking\" section of the README." 128 | exit 1 129 | fi 130 | if [ ! -w "$SCRIPT" ]; then 131 | echo "You do not have permission to upgrade the installation in $SCRIPT" 132 | exit 1 133 | else 134 | TARGET_VERSION="${2:-"stable"}" 135 | echo "The script at $SCRIPT will be upgraded to the latest $TARGET_VERSION version." 136 | echo -n "Do you want to continue [Y/n]? " 137 | read RESP 138 | case "$RESP" in 139 | y|Y|"") 140 | echo 141 | echo "Upgrading..." 142 | TARGET="/tmp/control-$$-upgrade" 143 | if ["$OSTYPE" = "cygwin" ]; then 144 | TARGET=`cygpath -w $TARGET` 145 | fi 146 | CONTROL_SCRIPT_URL="https://github.com/killme2008/clojure-control/raw/$TARGET_VERSION/bin/control" 147 | $HTTP_CLIENT "$TARGET" "$CONTROL_SCRIPT_URL" \ 148 | && mv "$TARGET" "$SCRIPT" \ 149 | && chmod +x "$SCRIPT" \ 150 | && echo && "$SCRIPT" self-install && echo && echo "Now running" `$SCRIPT version` 151 | exit $?;; 152 | *) 153 | echo "Aborted." 154 | exit 1;; 155 | esac 156 | fi 157 | else 158 | if [ "$OSTYPE" = "cygwin" ]; then 159 | # When running on Cygwin, use Windows-style paths for java 160 | ORIGINAL_PWD=`cygpath -w "$ORIGINAL_PWD"` 161 | CLASSPATH=`cygpath -wp "$CLASSPATH"` 162 | fi 163 | 164 | if [ $DEBUG ]; then 165 | echo "Classpath: $CLASSPATH" 166 | fi 167 | 168 | $CONTROL_JAVA_CMD \ 169 | -client -XX:+TieredCompilation \ 170 | $CONTROL_JVM_OPTS \ 171 | -Dfile.encoding=UTF-8 \ 172 | -Dmaven.wagon.http.ssl.easy=false \ 173 | -Dclojure-control.original.pwd="$ORIGINAL_PWD" \ 174 | -cp "$CLASSPATH" \ 175 | clojure.main -m control.main "$@" 176 | 177 | EXIT_CODE=$? 178 | 179 | exit $EXIT_CODE 180 | fi 181 | -------------------------------------------------------------------------------- /src/control/core.clj: -------------------------------------------------------------------------------- 1 | (ns control.core 2 | #^{ :doc "Clojure control core" 3 | :author " Dennis Zhuang "} 4 | (:use [clojure.java.io :only [reader]] 5 | [clojure.java.shell :only [sh]] 6 | [clojure.string :only [join blank? split]] 7 | [clojure.walk :only [walk postwalk]])) 8 | 9 | (def ^:dynamic *enable-color* true) 10 | ;;Error mode,:exit or :exception. 11 | (def ^:dynamic *error-mode* :exit) 12 | (def ^{:dynamic true} *enable-logging* true) 13 | (def ^:dynamic *debug* false) 14 | (def ^:private bash-reset "\033[0m") 15 | (def ^:private bash-bold "\033[1m") 16 | (def ^:private bash-redbold "\033[1;31m") 17 | (def ^:private bash-greenbold "\033[1;32m") 18 | ;;Global options for ssh,scp and rsync 19 | (def ^{:dynamic true :private true} *global-options* (atom {})) 20 | 21 | (defn error [msg] 22 | (if (= :exit (get @*global-options* :error-mode *error-mode*)) 23 | (do (doto System/err (.println msg)) (System/exit 1)) 24 | (throw (RuntimeException. msg)))) 25 | 26 | (defn ^:private check-valid-options 27 | "Throws an exception if the given option map contains keys not listed 28 | as valid, else returns nil." 29 | [options & valid-keys] 30 | (when (seq (apply disj (apply hash-set (keys options)) valid-keys)) 31 | (error (apply str "Only these options are valid: " 32 | (first valid-keys) 33 | (map #(str ", " %) (rest valid-keys)))))) 34 | 35 | (defmacro ^:private cli-bash-bold [& content] 36 | `(if *enable-color* 37 | (str bash-bold ~@content bash-reset) 38 | (str ~@content))) 39 | 40 | (defmacro ^:private cli-bash-redbold [& content] 41 | `(if *enable-color* 42 | (str bash-redbold ~@content bash-reset) 43 | (str ~@content))) 44 | 45 | (defmacro ^:private cli-bash-greenbold [& content] 46 | `(if *enable-color* 47 | (str bash-greenbold ~@content bash-reset) 48 | (str ~@content))) 49 | 50 | (defstruct ^:private ExecProcess :stdout :stderr :status) 51 | 52 | (defn gen-log [host tag content] 53 | (str (cli-bash-redbold host ":") 54 | (cli-bash-greenbold tag ": ") 55 | (join " " content))) 56 | 57 | (defn- log-with-tag [host tag & content] 58 | (when (and *enable-logging* (not (blank? (join " " content)))) 59 | (println (gen-log host tag content)))) 60 | 61 | (defn local 62 | "Execute command on local machine" 63 | [cmd] 64 | (when *enable-logging* (println (cli-bash-bold "Performing " cmd " on local"))) 65 | (let [rt (apply sh ["sh" "-c" cmd]) 66 | status (:exit rt) 67 | stdout (:out rt) 68 | stderr (:err rt) 69 | execp (struct-map ExecProcess :stdout stdout :stderr stderr :status status)] 70 | (log-with-tag "[Local]" "stdout" (:stdout execp)) 71 | (log-with-tag "[Local]" "stderr" (:stderr execp)) 72 | (log-with-tag "[Local]" "exit" status) 73 | execp)) 74 | 75 | (defn ^:dynamic exec [host user cmdcol] 76 | (let [rt (apply sh (remove nil? cmdcol)) 77 | status (:exit rt) 78 | stdout (:out rt) 79 | stderr (:err rt) 80 | execp (struct-map ExecProcess :stdout stdout :stderr stderr :status status)] 81 | (log-with-tag host "stdout" (:stdout execp)) 82 | (log-with-tag host "stderr" (:stderr execp)) 83 | (log-with-tag host "exit" status) 84 | execp)) 85 | 86 | (defn ssh-client [host user] 87 | (if-let [user (or user (:user @*global-options*))] 88 | (str user "@" host) 89 | (error "user is nil"))) 90 | 91 | (defn- user-at-host? [host user] 92 | (fn [m] 93 | (and (= (:user m) user) (= (:host m) host)))) 94 | 95 | (defn- find-client-options [host user cluster opt] 96 | (let [opt (keyword opt) 97 | m (first (filter (user-at-host? host user) (:clients cluster)))] 98 | (or (opt m) (opt cluster) (opt @*global-options*)))) 99 | 100 | (defn- make-cmd-array 101 | [cmd options others] 102 | (if (vector? options) 103 | (concat (cons cmd options) others) 104 | (cons cmd (cons options others)))) 105 | 106 | (defn set-options! 107 | "Set global options for ssh,scp and rsync, 108 | key and value could be: 109 | 110 | Key Value 111 | :ssh-options a ssh options string,for example \"-o ConnectTimeout=3000\" 112 | :scp-options a scp options string 113 | :rsync-options a rsync options string. 114 | :user global user for cluster,if cluster do not have :user ,it will use this by default. 115 | :parallel if to execute task on remote machines in parallel,default is false 116 | :error-mode mode-keyword,:exit (exit when error happends,the default error mode). or :exception (throw an exception). 117 | 118 | Example: 119 | (set-options! :ssh-options \"-o ConnectTimeout=3000\") 120 | 121 | " 122 | [key value & kvs] 123 | (let [options (apply hash-map key value kvs)] 124 | (check-valid-options options :user :ssh-options :scp-options :rsync-options :parallel :error-mode) 125 | (swap! *global-options* merge options))) 126 | 127 | (defn clear-options! 128 | "Clear global options" 129 | [] 130 | (reset! *global-options* {})) 131 | 132 | (defn ssh 133 | "Execute commands via ssh: 134 | (ssh \"date\") 135 | (ssh \"ps aux|grep java\") 136 | (ssh \"sudo apt-get update\" :sudo true) 137 | 138 | Valid options: 139 | :sudo whether to run commands as root,default is false 140 | :ssh-options -- ssh options string 141 | " 142 | {:arglists '([cmd & opts])} 143 | [host user cluster cmd & opts] 144 | (let [m (apply hash-map opts) 145 | sudo (:sudo m) 146 | cmd (if sudo 147 | (str "sudo " cmd) 148 | cmd) 149 | ssh-options (or (:ssh-options m) (find-client-options host user cluster :ssh-options))] 150 | (check-valid-options m :sudo :ssh-options :mode :scp-options) 151 | (log-with-tag host "ssh" ssh-options cmd) 152 | (exec host 153 | user 154 | (make-cmd-array "ssh" 155 | ssh-options 156 | [(ssh-client host user) cmd])))) 157 | 158 | (defn rsync 159 | "Rsync local files to remote machine's files,for example: 160 | (deftask :deploy \"scp files to remote machines\" [] 161 | (rsync \"src/\" \":/home/login\")) 162 | 163 | Valid options: 164 | :rsync-options -- rsync options string 165 | " 166 | {:arglists '([src dst & opts])} 167 | [host user cluster src dst & opts] 168 | (let [m (apply hash-map opts) 169 | rsync-options (or (:rsync-options m) (find-client-options host user cluster :rsync-options))] 170 | (check-valid-options m :rsync-options) 171 | (log-with-tag host "rsync" rsync-options (str src " ==>" dst)) 172 | (exec host 173 | user 174 | (make-cmd-array "rsync" 175 | rsync-options 176 | [src (str (ssh-client host user) ":" dst)])))) 177 | 178 | (def ^{:dynamic true} *tmp-dir* nil) 179 | 180 | (defn scp 181 | "Copy local files to remote machines: 182 | (scp \"test.txt\" \"remote.txt\") 183 | (scp [\"1.txt\" \"2.txt\"] \"/home/deploy/\" :sudo true :mode 755) 184 | 185 | Valid options: 186 | :sudo -- whether to copy files to remote machines as root 187 | :mode -- files permission on remote machines 188 | :scp-options -- scp options string 189 | " 190 | {:arglists '([local remote & opts])} 191 | [host user cluster local remote & opts] 192 | (let [files (if (coll? local) 193 | (vec local) 194 | [local]) 195 | m (apply hash-map opts) 196 | scp-options (or (:scp-options m) (find-client-options host user cluster :scp-options)) 197 | mode (:mode m) 198 | sudo (:sudo m) 199 | use-tmp (or sudo mode) 200 | tmp (if use-tmp 201 | (or *tmp-dir* (str "/tmp/control-" (System/currentTimeMillis) "/")) 202 | remote)] 203 | (check-valid-options m :scp-options :sudo :mode :ssh-options) 204 | (log-with-tag host "scp" scp-options 205 | (join " " (concat files [ " ==> " tmp]))) 206 | (when use-tmp 207 | (ssh host user cluster (str "mkdir -p " tmp))) 208 | (let [rt (exec host 209 | user 210 | (make-cmd-array "scp" 211 | scp-options 212 | (concat files [(str (ssh-client host user) ":" tmp)])))] 213 | (when mode 214 | (apply ssh host user cluster (str "chmod " mode " " tmp "*") opts)) 215 | (if use-tmp 216 | (apply ssh host user cluster (str "mv " tmp "* " remote " ; rm -rf " tmp) opts) 217 | rt)))) 218 | 219 | ;;All tasks defined in control file 220 | (defonce tasks (atom (hash-map))) 221 | ;;All clusters defined in control file 222 | (defonce clusters (atom (hash-map))) 223 | 224 | (def ^:private system-functions 225 | #{(symbol "scp") (symbol "ssh") (symbol "rsync") (symbol "call") (symbol "exists?")}) 226 | 227 | (defmacro 228 | ^{:doc "Define a task for executing on remote machines: 229 | (deftask :date \"Get date from remote machines\" 230 | (ssh \"date\")) 231 | 232 | Please see https://github.com/killme2008/clojure-control/wiki/Define-tasks 233 | " 234 | :arglists '([name doc-string? [params*] body]) 235 | :added "0.1"} 236 | deftask [tname & fdecl ] 237 | (let [tname (keyword tname) 238 | m (if (string? (first fdecl)) 239 | {:doc (first fdecl)} 240 | {}) 241 | fdecl (if (string? (first fdecl)) 242 | (next fdecl) 243 | fdecl) 244 | m (if (map? (first fdecl)) 245 | (conj m (first fdecl)) 246 | m) 247 | fdecl (if (map? (first fdecl)) 248 | (next fdecl) 249 | fdecl) 250 | arguments (first fdecl) 251 | arguments (vec (concat '[&host &user cluster] arguments)) 252 | m (conj {:arglists (list 'quote (list arguments))} m) 253 | body (next fdecl) 254 | new-body (postwalk (fn [item] 255 | (if (list? item) 256 | (let [cmd (first item)] 257 | (if (and (symbol? cmd) (get system-functions cmd)) 258 | (list* cmd '&host '&user 'cluster (rest item)) 259 | item)) 260 | item)) 261 | body)] 262 | (when-not (vector? arguments) 263 | (error (format "Task %s's arguments must be a vector" (name tname)))) 264 | (when *debug* 265 | (prn tname "new-body:" new-body)) 266 | `(do 267 | (swap! tasks 268 | assoc 269 | ~tname 270 | ~(list 271 | 'with-meta 272 | (list 273 | 'fn 274 | arguments 275 | (cons 'do new-body)) 276 | m)) 277 | (get @tasks ~tname)))) 278 | 279 | ;;commands that already run. 280 | (defonce run-tasks (atom #{})) 281 | 282 | (defn- run? [name] 283 | (@run-tasks name)) 284 | 285 | (defn- run-task [f n args] 286 | (swap! run-tasks conj n) 287 | (apply f args)) 288 | 289 | (declare perform) 290 | 291 | (defn call 292 | "Call other tasks in deftask,for example: 293 | (call :ps \"java\")" 294 | {:arglists '([task & args])} 295 | [host user cluster name & args] 296 | (if-let [t (get @tasks name)] 297 | (perform host user cluster t name args) 298 | (error (format "Task %s not found." name)))) 299 | 300 | (defn exists? 301 | "Check if a file or directory is exists" 302 | {:arglists '([file])} 303 | [host user cluster file] 304 | (zero? (:status (ssh host user cluster (str "test -e " file))))) 305 | 306 | 307 | (defn- unquote-cluster [args] 308 | (walk (fn [item] 309 | (cond (and (seq? item) (= `unquote (first item))) 310 | (second item) 311 | (or (seq? item) (symbol? item)) 312 | (list 'quote item) 313 | :else 314 | (unquote-cluster item))) 315 | identity 316 | args)) 317 | 318 | (defmacro 319 | ^{:doc "Define a cluster including some remote machines,for example: 320 | (defcluster :mycluster 321 | :user \"login\" 322 | :addresses [\"a.domain.com\" \"b.domain.com\"]) 323 | 324 | Please see https://github.com/killme2008/clojure-control/wiki/Define-clusters 325 | " 326 | :arglists '([cname & options]) 327 | :added "0.1"} 328 | defcluster [cname & args] 329 | (let [cname (keyword cname)] 330 | `(let [m# (apply hash-map ~(cons 'list (unquote-cluster args)))] 331 | (swap! clusters assoc ~cname (assoc m# :name ~cname))))) 332 | 333 | (defmacro ^:private when-exit 334 | ([test error] 335 | `(when-exit ~test ~error nil)) 336 | ([test error else] 337 | `(if ~test 338 | (do (error ~error)) 339 | ~else))) 340 | 341 | (defn- perform [host user cluster task taskName arguments] 342 | (when *enable-logging* 343 | (println (cli-bash-bold "Performing " (name taskName) " for " (ssh-client host user)))) 344 | (if (and (-> task meta :once) (run? taskName)) 345 | (println (cli-bash-bold "Ignored once task " (name taskName))) 346 | (run-task task taskName (concat [host user cluster] arguments)))) 347 | 348 | (defn- arg-count [f] 349 | (let [m (first (filter #(= (.getName %) "invoke") (.getDeclaredMethods (class f)))) 350 | p (when m (.getParameterTypes m))] 351 | (if p 352 | (alength p) 353 | 3))) 354 | 355 | (defn- is-parallel? [cluster] 356 | (or (:parallel cluster) (:parallel @*global-options*))) 357 | 358 | (defn- create-clients [arg] 359 | (when (> (.indexOf ^String arg "@") 0) 360 | (let [a (split arg #"@") 361 | user (first a) 362 | host (second a)] 363 | (when-exit (nil? user) "user is nil") 364 | (when-exit (nil? host) "host is nil") 365 | [{:user user :host host}]))) 366 | 367 | (defn do-begin [args] 368 | (when-exit (< (count args) 2) 369 | "Please offer cluster and task name" 370 | (let [cluster-name (keyword (first args)) 371 | task-name (keyword (second args)) 372 | task-args (next (next args)) 373 | cluster (cluster-name @clusters) 374 | parallel (is-parallel? cluster) 375 | user (:user cluster) 376 | addresses (:addresses cluster) 377 | clients (:clients cluster) 378 | task (task-name @tasks) 379 | includes (:includes cluster) 380 | debug (:debug cluster) 381 | log (or (:log cluster) true) 382 | clients (if (nil? cluster) (create-clients (first args)) clients)] 383 | (check-valid-options cluster :user :clients :addresses :parallel :includes :debug :log :ssh-options :scp-options :rsync-options :name :options) 384 | ;;if task is nil,exit 385 | (when-exit (nil? task) 386 | (str "No task named " (name task-name))) 387 | (when-exit (and (empty? addresses) 388 | (empty? includes) 389 | (empty? clients)) 390 | (str "Empty hosts for cluster " 391 | (name cluster-name))) 392 | ;;check task arguments count 393 | (let [task-arg-count (- (arg-count task) 3)] 394 | (when-exit (> task-arg-count (count task-args)) 395 | (str "Task " 396 | (name task-name) 397 | " just needs " 398 | task-arg-count 399 | " arguments"))) 400 | (binding [*enable-logging* (and *enable-logging* log) 401 | *debug* debug] 402 | (when *enable-logging* 403 | (println (str bash-bold 404 | "Performing " 405 | (name cluster-name) 406 | bash-reset 407 | (when parallel 408 | " in parallel")))) 409 | (let [map-fn (if parallel pmap map) 410 | a (doall (map-fn (fn [addr] [addr (perform addr user cluster task task-name task-args)]) 411 | addresses)) 412 | c (doall (map-fn (fn [cli] [(:host cli) (perform (:host cli) (:user cli) cluster task task-name task-args)]) 413 | clients))] 414 | (merge (into {} (concat a c)) 415 | (when includes 416 | (if (coll? includes) 417 | (mapcat #(do-begin (cons % (next args))) includes) 418 | (do-begin (cons (name includes) (next args))))))))))) 419 | 420 | (defn begin [] 421 | (do-begin *command-line-args*)) 422 | --------------------------------------------------------------------------------