com.palletops/runit-crate

0.8.0-alpha.3


Pallet crate to install, configure and use runit

dependencies

org.clojure/clojure
1.4.0
com.palletops/pallet
0.8.0-RC.7



(this space intentionally left almost blank)
 

A pallet crate to install and configure runit.

runit is not configured to replace init as PID 1.

(ns pallet.crate.runit
  (:require
   [clj-schema.schema :refer [def-map-schema optional-path sequence-of]]
   [clojure.tools.logging :refer [debugf warnf]]
   [pallet.action :refer [with-action-options]]
   [pallet.actions
    :refer [directory exec-checked-script plan-when plan-when-not
            remote-directory remote-file remote-file-arguments symbolic-link
            wait-for-file]
    :as actions]
   [pallet.actions-impl :refer [service-script-path]]
   [pallet.action-plan :as action-plan]
   [pallet.actions.direct.service :refer [service-impl]]
   [pallet.api :refer [plan-fn] :as api]
   [pallet.contracts :refer [any-value check-spec]]
   [pallet.crate :refer [assoc-settings defmethod-plan defplan get-settings
                         target-flag? update-settings]]
   [pallet.crate-install :as crate-install :refer [crate-install-settings]]
   [pallet.crate.initd :refer [init-script-path]]
   [pallet.crate.service
    :refer [service-supervisor service-supervisor-available?
            service-supervisor-config]]
   [pallet.stevedore :refer [fragment script]]
   [pallet.script.lib :refer [config-root file] :as lib]
   [pallet.utils :refer [apply-map]]
   [pallet.version-dispatch :refer [defmethod-version-plan
                                    defmulti-version-plan]]))

Settings

(def-map-schema :loose runit-settings
  crate-install-settings
  [[:user] string?
   [:group] string?
   [:owner] string?
   [:sv] string?
   [:sv-dir] string?
   [:service-dir] string?])

Provides default settings, that are merged with any user supplied settings.

(defn default-settings
  []
  ;; TODO add configuration options here
  {:user "runit"
   :group "runit"
   :owner "runit"
   :sv "/usr/bin/sv"
   :sv-dir (fragment (file (config-root) "sv"))
   :service-dir (fragment (file (config-root) "service"))})
(defmulti-version-plan settings-map [version settings])
(defmethod-version-plan
    settings-map {:os :linux}
    [os os-version version settings]
  (let [settings (update-in
                  settings [:runsvdir]
                  #(or % (fragment (file (config-root) "runit" "runsvdir"))))]
    (cond
     (:install-strategy settings) settings
     :else (assoc settings
             :install-strategy :packages
             :packages ["runit"]))))
(defmethod-version-plan
    settings-map {:os :debian-base}
    [os os-version version settings]
  (let [settings (update-in
                  settings [:runsvdir]
                  #(or % (fragment (file (config-root) "event.d" "runsvdir"))))]
    (cond
     (:install-strategy settings) settings
     :else (assoc settings
             :install-strategy :packages
             :packages ["runit"]
             :preseeds [{:line "runit runit/signalinit boolean true"}]))))
(defmethod-version-plan
    settings-map {:os :rh-base}
    [os os-version version settings]
  (let [settings (update-in
                  settings [:runsvdir]
                  #(or % (fragment (file (config-root) "runit" "runsvdir"))))]
    (cond
     (:install-strategy settings) settings
     :else (assoc settings
             :install-strategy ::build))))
(defmethod-version-plan
    settings-map {:os :os-x}
    [os os-version version settings]
  (let [settings (->
                  settings
                  (update-in
                   settings [:runsvdir]
                   #(or % (fragment
                           (file "usr" "local"  "var" "runit" "runsvdir"))))
                  (update-in
                   settings [:service-dir]
                   #(or % (fragment (file "usr" "local"  "var" "service" ))))
                  (update-in
                   settings [:sv-dir]
                   #(or % (fragment (file "usr" "local"  "var" "sv" )))))]
    (cond
     (:install-strategy settings) settings
     :else (assoc settings
             :install-strategy :packages
             :packages ["runit"]))))

Settings for runit

(defplan settings
  [{:keys [instance-id] :as settings}]
  (let [settings (merge (default-settings) settings)
        settings (settings-map (:version settings) settings)]
    (assoc-settings :runit settings {:instance-id instance-id})))

Create the runit user

User

(defplan user
  [{:keys [instance-id] :as options}]
  (let [{:keys [user owner group home]} (get-settings :runit options)]
    (debugf "Create runit owner %s user %s group %s" owner user group)
    (actions/group group :system true)
    (when (not= owner user)
      (actions/user owner :group group :system true))
    (actions/user
     user :group group :system true :create-home true :shell :bash)))

Install runit

Install

(defplan install
  [{:keys [instance-id]}]
  (let [settings (get-settings :runit {:instance-id instance-id})]
    (debugf "Install runit settings %s" settings)
    (crate-install/install :runit instance-id)))

Configure

Service Supervisor Implementation

(defmethod service-supervisor-available? :runit
  [_]
  true)
(def-map-schema runit-service-options
  :strict
  [[:service-name] string?
   [:run-file] remote-file-arguments
   (optional-path [:log-run-file]) remote-file-arguments])
(defmacro check-runit-service-options
  [m]
  (check-spec m `runit-service-options &form))

Add a service directory to runit

(defn- add-service
  [{:keys [service-name run-file] :as service-options}
   {:keys [instance-id] :as options}]
  (debugf "Adding service settings for %s" service-name)
  (check-runit-service-options service-options)
  (update-settings
   :runit options assoc-in [:jobs (keyword service-name)] service-options))
(defmethod service-supervisor-config :runit
  [_ {:keys [service-name run-file] :as service-options} options]
  (add-service service-options options))

Add a service directory to runit

(defn- write-service
  [service-name
   {:keys [run-file log-run-file]}
   {:keys [instance-id] :as options}]
  (debugf "Writing service files for %s" service-name)
  (let [{:keys [sv-dir owner group]} (get-settings :runit options)]
    (directory                          ; create the service directory
     (fragment (file ~sv-dir ~service-name))
     :owner owner :group group)
    (apply-map                          ; create the run file
     remote-file (fragment (file ~sv-dir ~service-name "run"))
     :owner owner :group group
     :mode "0755"
     run-file)
    (when log-run-file
      (directory (fragment (file ~sv-dir ~service-name "log"))
                 :owner owner :group group :mode "0755")
      (apply-map                        ; create the run file
       remote-file (fragment (file ~sv-dir ~service-name "log" "run"))
       :owner owner :group group
       :mode "0755"
       log-run-file))
    ;; (directory (fragment (file ~sv-dir ~service-name "supervise"))
    ;;            :owner owner :group group :mode "0755")
    (symbolic-link                      ; link to /etc/init.d
     "/usr/bin/sv" (init-script-path service-name)
     :no-deref true)))

Write out job definitions.

(defn configure
  [{:keys [instance-id] :as options}]
  (let [{:keys [jobs]} (get-settings :runit {:instance-id instance-id})]
    (debugf "Writing service files for %s jobs" (count jobs))
    (doseq [[job {:keys [run-file] :as service-options}] jobs
            :let [service-name (name job)]]
      (write-service service-name service-options options))))
(def action-names
  {:reload :hup})
(defmethod service-supervisor :runit
  [_ {:keys [service-name]}
   {:keys [action if-flag if-stopped instance-id wait]
    :or {action :start wait true}
    :as options}]
  (debugf "Controlling service %s, :action %s" service-name action)
  (let [{:keys [sv sv-dir service-dir]} (get-settings :runit options)]
    (case action
      :enable (do
                (exec-checked-script
                 (format "Enable service %s" service-name)
                 (lib/ln (file ~sv-dir ~service-name)
                         (file ~service-dir ~service-name)
                         :symbolic true :force true))
                ;; Check for supervise/ok to be present. According to the docs,
                ;; this should take less than five seconds.
                (when wait
                  (wait-for-file
                   (fragment (file ~sv-dir ~service-name "supervise" "ok"))
                   :standoff 5)))
      :disable (exec-checked-script
                (format "Disable service %s" service-name)
                (sv down ~service-name)
                (lib/rm (file ~service-dir ~service-name) :force true))
      :start-stop (warnf ":start-stop not implemented for runit")
      (if if-flag
        (plan-when (target-flag? if-flag)
          (exec-checked-script
           (str (name action) " " service-name)
           (sv ~(name (action action-names action)) ~service-name)))
        (if if-stopped
          (exec-checked-script
           (str (name action) " " service-name)
           (if-not ("sv" "status" ~service-name)
             (sv ~(name (action action-names action)) ~service-name)))
          (exec-checked-script
           (str (name action) " " service-name)
           (sv ~(name (action action-names action)) ~service-name)))))))

Returns a server-spec that installs and configures runit.

Server Spec

(defn server-spec
  [settings & {:keys [instance-id] :as options}]
  (api/server-spec
   :phases
   {:settings (plan-fn
                (pallet.crate.runit/settings (merge settings options)))
    :install (plan-fn
               (user options)
               (install options))
    :configure (plan-fn
                 (configure options))}))
 

A pallet crate to install and configure socklog for use with runit.

(ns pallet.crate.socklog
  (:require
   [clj-schema.schema :refer [def-map-schema map-schema optional-path
                              sequence-of]]
   [clojure.tools.logging :refer [debugf warnf]]
   [pallet.action :refer [with-action-options]]
   [pallet.actions
    :refer [directory exec-checked-script plan-when plan-when-not
            remote-directory remote-file remote-file-arguments symbolic-link
            wait-for-file]
    :as actions]
   [pallet.actions-impl :refer [service-script-path]]
   [pallet.action-plan :as action-plan]
   [pallet.actions.direct.service :refer [service-impl]]
   [pallet.api :refer [plan-fn] :as api]
   [pallet.contracts :refer [any-value check-spec*]]
   [pallet.crate :refer [assoc-settings defmethod-plan defplan get-settings
                         target-flag? update-settings]]
   [pallet.crate-install :as crate-install :refer [crate-install-settings]]
   [pallet.crate.initd :refer [init-script-path]]
   [pallet.crate.service :as service
    :refer [supervisor-config supervisor-config-map]]
   [pallet.stevedore :refer [fragment script]]
   [pallet.script.lib :refer [log-root file] :as lib]
   [pallet.utils :refer [apply-map]]
   [pallet.version-dispatch :refer [defmethod-version-plan
                                    defmulti-version-plan]]))
(defmulti socklog-schema
  (fn socklog-schema-eval [facility] facility))
(def-map-schema socklog-base-schema
   crate-install-settings
   [[:facility] keyword?
    [:user] string?
    [:log-user] string?
    [:service-name] string?
    [:supervisor] keyword?])
(defmethod socklog-schema :unix
  [_]
  (map-schema
   :strict
   socklog-base-schema
   [[:socket] string?]))
(defmethod socklog-schema :inet
  [_]
  socklog-base-schema)
(defmethod socklog-schema :klog
  [_]
  socklog-base-schema)
(defmethod socklog-schema :ucspi
  [_]
  socklog-base-schema)
(defmacro check-socklog-settings
  [m]
  `(let [m# ~m
         f# (:facility m#)]
    (check-spec* m# (socklog-schema f#)
                 (str "socklog-" (name f#) "-schema")
                 ~(:line (meta &form)) ~*file*)))

Provides default settings, that are merged with any user supplied settings.

(defmulti default-settings
  (fn default-settings-eval [facility] facility))
(defn default-settings-base
  [facility]
  {:user "nobody"
   :log-user "log"
   :supervisor :runit
   ;; :log-base (fragment (file (log-root) "socklog"))
   :service-name (str "socklog-" (name facility))})
(defmethod default-settings :default
  [facility]
  (default-settings-base facility))
(defmethod default-settings :unix
  [facility]
  (assoc (default-settings-base facility)
    :socket "/dev/log"))
(defmulti-version-plan settings-map [version settings])
(defmethod-version-plan
    settings-map {:os :debian-base}
    [os os-version version settings]
  (cond
   (:install-strategy settings) settings
   :else (assoc settings
           :install-strategy :packages
           :packages ["socklog"])))
(defn- facility-kw
  [facility]
  {:pre [(keyword? facility)]}
  (keyword (str "socklog-" (name facility))))

Settings for socklog. The :facility keyword must be one of :unix, :inet, :klog or :ucspi

(defplan settings
  [{:keys [facility] :as settings} & {:keys [instance-id] :as options}]
  {:pre [(#{:unix :inet :klog :ucspi} facility)]}
  (let [settings (merge (default-settings facility) settings)
        settings (settings-map (:version settings) settings)]
    (check-socklog-settings settings)
    (assoc-settings (facility-kw facility)
                    settings {:instance-id instance-id})
    (supervisor-config (facility-kw facility) settings (or options {}))))

Create the socklog users

User

(defplan user
  [{:keys [facility instance-id] :as options}]
  (let [{:keys [user log-user]} (get-settings (facility-kw facility) options)]
    (debugf "Create socklog user %s log-user %s" user log-user)
    (actions/user user :system true :create-home false :shell :bash)
    (actions/user log-user :system true :create-home false :shell :bash)))

Install socklog

Install

(defplan install
  [{:keys [facility instance-id] :as options}]
  (let [{:keys [log-user] :as settings}
        (get-settings (facility-kw facility) options)]
    (debugf "Install socklog settings %s" settings)
    (check-socklog-settings settings)
    (directory "/var/log/socklog" :mode "0755" :owner log-user)
    (directory "/var/log/socklog/main" :mode "0755" :owner log-user)
    (crate-install/install (facility-kw facility) instance-id)))

Provide a configuration for the socklog daemon

(defmethod supervisor-config-map [:socklog-unix :runit]
  [_ {:keys [service-name log-user user socket] :as settings} options]
  {:service-name service-name
   :run-file {:content (str "#!/bin/sh\nexec 2>&1\nexec chpst -U "
                            user " socklog unix " socket)}
   :log-run-file {:content (str "#!/bin/sh\nexec chpst -u "
                                log-user " svlogd -t main/*")}})
(defmethod supervisor-config-map [:socklog-inet :runit]
  [_ {:keys [service-name log-user user] :as settings} options]
  {:service-name service-name
   :run-file {:content (str "#!/bin/sh\nexec 2>&1\nexec chpst -U "
                            user " socklog inet")}
   :log-run-file {:content (str "#!/bin/sh\nexec chpst -u "
                                log-user " svlogd -t main/*")}})
(defmethod supervisor-config-map [:socklog-klog :runit]
  [_ {:keys [service-name log-user user] :as settings} options]
  {:service-name service-name
   :run-file {:content (str "#!/bin/sh\nexec 2>&1\nexec chpst -U "
                            user " socklog klog")}
   :log-run-file {:content (str "#!/bin/sh\nexec chpst -u "
                                log-user " svlogd -t main/*")}})
(defmethod supervisor-config-map [:socklog-ucspi :runit]
  [_ {:keys [service-name log-user user] :as settings} options]
  {:service-name service-name
   :run-file {:content (str "#!/bin/sh\nexec 2>&1\nexec chpst -U "
                            user " socklog ucspi")}
   :log-run-file {:content (str "#!/bin/sh\nexec chpst -u "
                                log-user " svlogd -t main/*")}})

Run the socklog service.

Run

(defplan service
  [& {:keys [action facility if-flag if-stopped instance-id]
      :or {action :manage}
      :as options}]
  (let [{:keys [supervision-options] :as settings}
        (get-settings (facility-kw facility) {:instance-id instance-id})]
    (check-socklog-settings settings)
    (service/service settings (merge supervision-options
                                     (dissoc options :instance-id)))))

Returns a server-spec that installs and configures socklog.

(defn server-spec
  [{:keys [facility] :as settings} & {:keys [instance-id] :as options}]
  (let [options (assoc options :facility facility)]
    (api/server-spec
     :phases
     (merge
      {:settings (plan-fn
                   (apply-map pallet.crate.socklog/settings settings options))
       :install (plan-fn
                  (user options)
                  (install options))
       :configure (plan-fn
                    ;; (configure options)
                    ;; (apply-map service :action :enable options))
       :run (plan-fn
              (apply-map service :action :start options))}))))