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

pallet.crate.postgres.clj Maven / Gradle / Ivy

There is a newer version: 0.7.0-beta.2
Show newest version
(ns pallet.crate.postgres
  "Install and configure PostgreSQL."
  (:require
   [pallet.resource.package :as package]
   [pallet.request-map :as request-map]
   [pallet.resource :as resource]
   [pallet.resource.file :as file]
   [pallet.resource.exec-script :as exec-script]
   [pallet.resource.remote-file :as remote-file]
   [pallet.resource.resource-when :as resource-when]
   [pallet.stevedore :as stevedore]
   [pallet.parameter :as parameter]
   [clojure.contrib.condition :as condition]
   [clojure.contrib.logging :as logging]
   [clojure.string :as str])
  (:use
   pallet.thread-expr
   [pallet.script :only [defscript]]))

(defn postgres
  "version should be a string identifying the major.minor version number desired
   (e.g. \"9.0\")."
  [request version]
  (let [os-family (request-map/os-family request)]
    (-> request
        (when-> (= os-family :ubuntu)
                (when-> (= version "9.0")
                        (package/package-source
                         "Martin Pitt backports"
                         :aptitude {:url "ppa:pitti/postgresql"}))
                (package/package-manager :update))
        (package/packages :aptitude [(str "postgresql-" version)])
        (assoc-in [:parameters :postgresql :version] version))))

(def ^{:private true} pallet-cfg-preamble
"# This file was auto-generated by Pallet. Do not edit it manually unless you
# know what you are doing. If you are still using Pallet, you probably want to
# edit your Pallet scripts and rerun them.\n")

;;
;; pg_hba.conf
;;

(def ^{:private true}
  auth-methods #{"trust" "reject" "md5" "password" "gss" "sspi" "krb5"
                                     "ident" "ldap" "radius" "cert" "pam"})
(def ^{:private true}
  ip-addr-regex #"[0-9]{1,3}.[0-9]{1,3}+.[0-9]{1,3}+.[0-9]{1,3}+")

(defn- valid-hba-record?
  "Takes an hba-record as input and minimally checks that it could be a valid
   record."
  [{:keys [connection-type database user auth-method address ip-mask]
    :as record-map}]
  (and (#{"local" "host" "hostssl" "hostnossl"} (name connection-type))
       (every? #(not (nil? %)) [database user auth-method])
       (auth-methods (name auth-method))))

(defn- record-to-map
  "Takes a record given as a map or vector, and turns it into the map version."
  [record]
  (cond
   (map? record) record
   (vector? record) (case (name (first record))
                      "local" (apply
                               hash-map
                               (interleave
                                [:connection-type :database :user :auth-method
                                 :auth-options]
                                record))
                      ("host"
                       "hostssl"
                       "hostnossl") (let [[connection-type database user address
                                           & remainder] record]
                                      (if (re-matches
                                           ip-addr-regex (first remainder))
                                        ;; Not nil so must be an IP mask.
                                        (apply
                                         hash-map
                                         (interleave
                                          [:connection-type :database :user
                                           :address :ip-mask :auth-method
                                           :auth-options]
                                          record))
                                        ;; Otherwise, it may be an auth-method.
                                        (if (auth-methods
                                             (name (first remainder)))
                                          (apply
                                           hash-map
                                           (interleave
                                            [:connection-type :database :user
                                             :address :auth-method
                                             :auth-options]
                                            record))
                                          (condition/raise
                                           :type :postgres-invalid-hba-record
                                           :message
                                           (format
                                            "The fifth item in %s does not appear to be an IP mask or auth method."
                                            (name record))))))
                       (condition/raise
                        :type :postgres-invalid-hba-record
                        :message (format
                                  "The first item in %s is not a valid connection type."
                                  (name record))))
   :else
   (condition/raise :type :postgres-invalid-hba-record
                    :message (format "The record %s must be a vector or map."
                                     (name record)))))

(defn- format-auth-options
  "Given the auth-options map, returns a string suitable for inserting into the
   file."
  [auth-options]
  (str/join "," (map #(str (first %) "=" (second %)) auth-options)))

(defn- format-hba-record
  [record]
  (let [record-map (record-to-map record)
        record-map (assoc record-map :auth-options
                          (format-auth-options (:auth-options record-map)))
        ordered-fields (map record-map
                            [:connection-type :database :user :address :ip-mask
                             :auth-method :auth-options])
        ordered-fields (map name ordered-fields)]
    (if (valid-hba-record? record-map)
      (str (str/join "\t" ordered-fields) "\n"))))

(defn hba-conf
  "Generates a pg_hba.conf file from the arguments. Each record is either a
   vector or map of keywords/args.

   Note that pg_hba.conf is case-sensitive: all means all databases, ALL is a
   database named ALL.

   Also note that if you intend to execute subsequent commands, you'd do best to
   include entries in here that allow the admin user you are using easy access
   to the database. For example, allow the postgres user to have ident access
   over local.

   Options:
   :records     - A sequence of records (either vectors or maps of
                  keywords/strings).
   :conf-path   - A string suitable for passing to format before a string of the
                  version."
  [request & {:keys [records conf-path]
              :or {records []
                   conf-path "/etc/postgresql/%s/main/pg_hba.conf"}}]
  (let [hba-contents (apply str pallet-cfg-preamble
                            (map format-hba-record records))
        version (parameter/get-for request [:postgresql :version])]
    (-> request
        (remote-file/remote-file (format conf-path version)
         :content hba-contents
         :literal true))))

;;
;; postgresql.conf
;;

(defn- parameter-escape-string
  "Given a string, escapes any single-quotes."
  [string]
  (apply str (replace {\' "''"} string)))

(defn- format-parameter-value
  [value]
  (cond (number? value)
        (str value)
        (string? value)
        (str "'" value "'")
        (vector? value)
        (str "'" (str/join "," (map name value)) "'")
        :else
        (condition/raise
         :type :postgres-invalid-parameter
         :message "Parameters must be numbers, strings, or vectors of such.")))

(defn- format-parameter
  "Given a key/value pair in a vector, formats it suitably for the
   postgresql.conf file.
   The value should be either a number, a string, or a vector of such."
  [[key value]]
  (let [key-str (name key)
        parameter-str (format-parameter-value value)]
    (str key-str " = " parameter-str "\n")))

(defn postgresql-conf
  "Generates a postgresql.conf file from the arguments.
   Example: (postgresql-conf
              :options {:listen_address [\"10.0.1.1\",\"localhost\"]})
         => listen_address = '10.0.1.1,localhost'

   Options:
   :options     - A map of parameters (string(able)s, numbers, or vectors of
                  such).
   :conf-path   - A string suitable for passing to format before a string of
                  the version."
  [request & {:keys [options conf-path]
              :or {options {}
                   conf-path "/etc/postgresql/%s/main/postgresql.conf"}}]
  (let [contents (apply str pallet-cfg-preamble
                        (map format-parameter options))
        version (parameter/get-for request [:postgresql :version])]
    (-> request
        (remote-file/remote-file (format conf-path version)
                                 :content contents
                                 :literal true))))

;;
;; Scripts
;;

(defn postgresql-script
  "Execute a postgresql script.

   Options for how this script should be run:
     :as-user username       - Run this script having sudoed to this (system)
                               user. Default: postgres
     :ignore-result          - Ignore any error return value out of psql."
  [request sql-script & {:keys [as-user ignore-result]
                         :as options
                         :or {as-user "postgres"}}]
  (-> request
      (exec-script/exec-checked-script
       "PostgreSQL temp command file"
       (var psql_commands (file/make-temp-file "postgresql")))
      (remote-file/remote-file
       (stevedore/script @psql_commands)
       :no-versioning true
       :literal true
       :content sql-script)
      (exec-script/exec-script
       ;; Note that we stuff all output. This is because certain commands in
       ;; PostgreSQL are idempotent but spit out an error and an error exit
       ;; anyways (eg, create database on a database that already exists does
       ;; nothing, but is counted as an error).
       ("{\n" sudo "-u" ~as-user psql "-f" @psql_commands > "/dev/null" "2>&1"
        ~(when ignore-result "|| 0") "\n}"))
      (remote-file/remote-file
       (stevedore/script @psql_commands)
       :action :delete)))

(defn create-database
  "Create a database if it does not exist.

   You can specify database parameters by including a keyed parameter called
   :db-parameters, which indicates a vector of strings or keywords that will get
   translated in order to the options to the create database command. Passes on
   key/value arguments it does not understand to postgresql-script.

   Example: (create-database
              \"my-database\" :db-parameters [:encoding \"'LATIN1'\"])"
  [request db-name & rest]
  (let [{:keys [db-parameters] :as options} rest
        db-parameters-str (str/join " " (map name db-parameters))]
    ;; Postgres simply has no way to check if a database exists and issue a
    ;; "CREATE DATABASE" only in the case that it doesn't. That would require a
    ;; function, but create database can't be done within a transaction, so
    ;; you're screwed. Instead, we just use the fact that trying to create an
    ;; existing database does nothing and stuff the output/error return.
    (apply postgresql-script
           request
           (format "CREATE DATABASE %s %s;" db-name db-parameters-str)
           (conj (vec rest) :ignore-result true))))

;; This is a format string that generates a temporary PL/pgsql function to
;; check if a given role exists, and if not create it. The first argument
;; should be the role name, the second should be any user-parameters.
(def ^{:private true} create-role-pgsql
"create or replace function pg_temp.createuser() returns void as $$
 declare user_rec record;
 begin
 select into user_rec * from pg_user where usename='%1$s';
 if user_rec.usename is null then
     create role %1$s %2$s;
 end if;
 end;
 $$ language plpgsql;
 select pg_temp.createuser();")

(defn create-role
  "Create a postgres role if it does not exist.

   You can specify user parameters by including a keyed parameter called
   :user-parameters, which indicates a vector of strings or keywords that will
   get translated in order to the options to the create user command. Passes on
   key/value arguments to postgresql-script.

   Example (create-role
             \"myuser\" :user-parameters [:encrypted :password \"'mypasswd'\"])"
  [request username & rest]
  (let [{:keys [user-parameters] :as options} rest
        user-parameters-str (str/join " " (map name user-parameters))]
    (apply postgresql-script
           request
           (format create-role-pgsql username user-parameters-str)
           rest)))




© 2015 - 2024 Weber Informatics LLC | Privacy Policy