All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pallet.ssh.transport.clj Maven / Gradle / Ivy

(ns pallet.ssh.transport
  "Implementation of execution over ssh"
  (:require
   [clj-ssh.ssh :as ssh]
   [clojure.java.io :as io]
   [clojure.string :as string]
   [clojure.tools.logging :as logging])
  (:use
   [slingshot.slingshot :only [throw+]]))

(defonce default-agent-atom (atom nil))

(defn default-agent
  []
  (or @default-agent-atom
      (swap! default-agent-atom
             (fn [agent]
               (if agent
                 agent
                 (ssh/create-ssh-agent false))))))

(defn possibly-add-identity
  [agent private-key-path passphrase]
  (if passphrase
    (ssh/add-identity agent private-key-path passphrase)
    (when private-key-path
      (ssh/add-identity-with-keychain agent private-key-path))))

(defn ssh-user-credentials
  "Middleware to user the session :user credentials for SSH authentication."
  [authentication]
  (let [user (:user authentication)]
    (logging/infof "SSH user %s %s" (:username user) (:private-key-path user))
    (possibly-add-identity
     (default-agent) (:private-key-path user) (:passphrase user))))

(defn connect-ssh-session
  [ssh-session endpoint authentication]
  (when-not (ssh/connected? ssh-session)
    (logging/debugf "SSH connecting %s" endpoint)
    (try
      (ssh/connect ssh-session)
      (catch Exception e
        (throw+
         {:type :pallet/ssh-connection-failure
          :cause e}
         (format
          "SSH connect : server %s, port %s, user %s"
          (:server endpoint)
          (:port endpoint 22)
          (-> authentication :user :username)))))))

(defn connect-sftp-channel
  [sftp-channel endpoint authentication]
  (when-not (ssh/connected? sftp-channel)
    (try
      (ssh/connect sftp-channel)
      (catch Exception e
        (throw+
         {:type :pallet/sftp-channel-failure
          :cause e}
         (format
          "SSH connect SFTP : server %s, port %s, user %s"
          (:server endpoint)
          (:port endpoint 22)
          (-> authentication :user :username)))))))

(defn connect
  ([endpoint authentication options]
     (let [ssh-session (ssh/session
                        (default-agent)
                        (:server endpoint)
                        :username (-> authentication :user :username)
                        :strict-host-key-checking (:strict-host-key-checking
                                                   options :no)
                        :port (:port endpoint 22)
                        :password (-> authentication :user :password))
           _ (.setDaemonThread ssh-session true)
           _ (connect-ssh-session ssh-session endpoint authentication)
           sftp-channel (ssh/ssh-sftp ssh-session)]
       (connect-sftp-channel sftp-channel endpoint authentication)
       {:ssh-session ssh-session
        :sftp-channel sftp-channel
        :endpoint endpoint
        :authentication authentication
        :options options}))
  ([state]
     (let [ssh-session (:ssh-session state)
           _ (connect-ssh-session ssh-session)
           sftp-channel (:sftp-channel state)]
       (connect-sftp-channel sftp-channel)
       state)))

(defn close
  "Close any ssh connection to the server specified in the session."
  [{:keys [ssh-session sftp-channel endpoint] :as state}]
  (logging/debugf "SSH close %s" endpoint)
  (when sftp-channel
    (logging/debugf "SSH disconnect SFTP %s" endpoint)
    (ssh/disconnect sftp-channel))
  (when ssh-session
    (logging/debugf "SSH disconnect SSH %s" endpoint)
    (ssh/disconnect ssh-session))
  state)

(defn send-stream
  [{:keys [sftp-channel] :as state} source destination]
  (ssh/sftp
   sftp-channel
   :put source
   destination
   :return-map true))

(defn send-text
  [{:keys [sftp-channel] :as state} source destination]
  (ssh/sftp
   sftp-channel
   :put (java.io.ByteArrayInputStream. (.getBytes source))
   destination
   :return-map true))

(defn receive
  [{:keys [sftp-channel] :as state} source destination]
  (ssh/sftp sftp-channel :get source (io/output-stream (io/file destination))))

(def
  ^{:doc "Specifies the buffer size used to read the ssh output stream.
    Defaults to 10K, to match clj-ssh.ssh/*piped-stream-buffer-size*"}
  ssh-output-buffer-size (atom (* 1024 10)))

(def
  ^{:doc "Specifies the polling period for retrieving ssh command output.
    Defaults to 1000ms."}
  output-poll-period (atom 1000))

(defn exec
  [{:keys [ssh-session sftp-channel endpoint authentication] :as state}
   {:keys [execv in] :as code}
   {:keys [output-f pty] :as options}]
  (logging/tracef "ssh/exec %s" code)
  (logging/tracef "ssh/exec %s" (pr-str state))
  (logging/tracef "ssh/exec session connected %s" (ssh/connected? ssh-session))
  (if output-f
    (let [[shell stream] (apply
                          ssh/ssh
                          ssh-session
                          (concat
                           (when-let [execv (seq execv)]
                             [(apply
                              str (interpose " " (map str execv)))])
                           [:in in :return-map true
                            :pty (:pty options true) :out :stream]))
          sb (StringBuilder.)
          buffer-size @ssh-output-buffer-size
          period @output-poll-period
          bytes (byte-array buffer-size)
          read-ouput (fn []
                       (when (pos? (.available stream))
                         (let [num-read (.read stream bytes 0 buffer-size)
                               s (String. bytes 0 num-read "UTF-8")]
                           (output-f s)
                           (.append sb s)
                           s)))]
      (while (ssh/connected? shell)
        (Thread/sleep period)
        (read-ouput))
      (while (read-ouput))
      (.close stream)
      (let [exit (.getExitStatus shell)
            stdout (str sb)]
        (if (zero? exit)
          {:out stdout :exit exit}
          (do
            (logging/errorf "Exit status  : %s" exit)
            {:out stdout :exit exit
             :error {:message (format
                               "Error executing script :\n :cmd %s\n :out %s\n"
                               code stdout)
                     :type :pallet-script-excution-error
                     :script-exit exit
                     :script-out stdout
                     :server (:server endpoint)}}))))
    (let [{:keys [out exit] :as result} (apply
                                         ssh/ssh
                                         ssh-session
                                         (concat
                                          (when-let [execv (seq execv)]
                                            [(apply
                                              str
                                              (interpose " " (map str execv)))])
                                          [:in in :return-map true
                                           :pty (:pty options true)]))]

      (if (zero? exit)
        result
        (do
          (logging/errorf "Exit status  : %s" exit)
          {:out out :exit exit
           :error {:message (format
                             "Error executing script :\n :cmd %s\n :out %s\n"
                             code out)
                   :type :pallet-script-excution-error
                   :script-exit exit
                   :script-out out
                   :server (:server endpoint)}})))))

(defn forward-to-local
  [{:keys [ssh-session sftp-channel endpoint authentication] :as state}
   remote-port
   local-port]
  (.setPortForwardingL ssh-session local-port (:server endpoint) remote-port))

(defn unforward-to-local
  [{:keys [ssh-session sftp-channel endpoint authentication] :as state}
   remote-port
   local-port]
  (.delPortForwardingL ssh-session local-port))

(defn connected?
  [state]
  (ssh/connected? (:ssh-session state)))

(defn open [endpoint authentication options]
  (ssh-user-credentials authentication)
  (connect endpoint authentication options))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy