]> git.openstreetmap.org Git - chef.git/blob - cookbooks/postgresql/libraries/postgresql.rb
Merge remote-tracking branch 'github/pull/710'
[chef.git] / cookbooks / postgresql / libraries / postgresql.rb
1 require "chef/mixin/shell_out"
2
3 module OpenStreetMap
4   class PostgreSQL
5     include Chef::Mixin::ShellOut
6
7     SCHEMA_PRIVILEGES = [
8       :create, :usage
9     ].freeze
10
11     TABLE_PRIVILEGES = [
12       :select, :insert, :update, :delete, :truncate, :references, :trigger
13     ].freeze
14
15     SEQUENCE_PRIVILEGES = [
16       :usage, :select, :update
17     ].freeze
18
19     def initialize(cluster)
20       @cluster = cluster
21     end
22
23     def version
24       @cluster.split("/").first.to_f
25     end
26
27     def execute(options)
28       # Create argument array
29       args = []
30
31       # Add the cluster
32       args.push("--cluster")
33       args.push(@cluster)
34
35       # Set output format
36       args.push("--no-align") unless options.fetch(:align, true)
37
38       # Add any SQL command to execute
39       if options[:command]
40         args.push("--command")
41         args.push(options[:command])
42       end
43
44       # Add any file to execute SQL commands from
45       if options[:file]
46         args.push("--file")
47         args.push(options[:file])
48       end
49
50       # Add the database name
51       args.push(options[:database] || "template1")
52
53       # Get the user and group to run as
54       user = options[:user] || "postgres"
55       group = options[:group] || "postgres"
56
57       # Run the command
58       shell_out!("/usr/bin/psql", *args, :user => user, :group => group)
59     end
60
61     def query(sql, options = {})
62       # Run the query
63       result = execute(options.merge(:command => sql, :align => false))
64
65       # Split the output into lines
66       lines = result.stdout.split("\n")
67
68       # Remove the "(N rows)" line from the end
69       lines.pop
70
71       # Get the field names
72       fields = lines.shift.split("|")
73
74       # Extract the record data
75       lines.collect do |line|
76         record = {}
77         fields.zip(line.split("|")) { |name, value| record[name.to_sym] = value }
78         record
79       end
80     end
81
82     def users
83       @users ||= query("SELECT *, ARRAY(SELECT groname FROM pg_group WHERE usesysid = ANY(grolist)) AS roles FROM pg_user").each_with_object({}) do |user, users|
84         users[user[:usename]] = {
85           :superuser => user[:usesuper] == "t",
86           :createdb => user[:usercreatedb] == "t",
87           :createrole => user[:usecatupd] == "t",
88           :replication => user[:userepl] == "t",
89           :roles => parse_array(user[:roles] || "{}")
90         }
91       end
92     end
93
94     def databases
95       @databases ||= query("SELECT d.datname, u.usename, d.encoding, d.datcollate, d.datctype FROM pg_database AS d INNER JOIN pg_user AS u ON d.datdba = u.usesysid").each_with_object({}) do |database, databases|
96         databases[database[:datname]] = {
97           :owner => database[:usename],
98           :encoding => database[:encoding],
99           :collate => database[:datcollate],
100           :ctype => database[:datctype]
101         }
102       end
103     end
104
105     def extensions(database)
106       @extensions ||= {}
107       @extensions[database] ||= query("SELECT extname, extversion FROM pg_extension", :database => database).each_with_object({}) do |extension, extensions|
108         extensions[extension[:extname]] = {
109           :version => extension[:extversion]
110         }
111       end
112     end
113
114     def tablespaces
115       @tablespaces ||= query("SELECT spcname, usename FROM pg_tablespace AS t INNER JOIN pg_user AS u ON t.spcowner = u.usesysid").each_with_object({}) do |tablespace, tablespaces|
116         tablespaces[tablespace[:spcname]] = {
117           :owner => tablespace[:usename]
118         }
119       end
120     end
121
122     def schemas(database)
123       @schemas ||= {}
124       @schemas[database] ||= query("SELECT n.nspname, pg_catalog.pg_get_userbyid(n.nspowner) AS usename, n.nspacl FROM pg_namespace AS n WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'", :database => database).each_with_object({}) do |schema, schemas|
125         name = "#{schema[:nspname]}"
126
127         schemas[name] = {
128           :owner => schema[:usename],
129           :permissions => parse_acl(schema[:nspacl] || "{}")
130         }
131       end
132     end
133
134     def tables(database)
135       @tables ||= {}
136       @tables[database] ||= query("SELECT n.nspname, c.relname, u.usename, c.relacl FROM pg_class AS c INNER JOIN pg_user AS u ON c.relowner = u.usesysid INNER JOIN pg_namespace AS n ON c.relnamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND c.relkind IN ('r', 'p')", :database => database).each_with_object({}) do |table, tables|
137         name = "#{table[:nspname]}.#{table[:relname]}"
138
139         tables[name] = {
140           :owner => table[:usename],
141           :permissions => parse_acl(table[:relacl] || "{}")
142         }
143       end
144     end
145
146     def sequences(database)
147       @sequences ||= {}
148       @sequences[database] ||= query("SELECT n.nspname, c.relname, u.usename, c.relacl FROM pg_class AS c INNER JOIN pg_user AS u ON c.relowner = u.usesysid INNER JOIN pg_namespace AS n ON c.relnamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND c.relkind = 'S'", :database => database).each_with_object({}) do |sequence, sequences|
149         name = "#{sequence[:nspname]}.#{sequence[:relname]}"
150
151         sequences[name] = {
152           :owner => sequence[:usename],
153           :permissions => parse_acl(sequence[:relacl] || "{}")
154         }
155       end
156     end
157
158     private
159
160     def parse_array(array)
161       array.sub(/^\{(.*)\}$/, "\\1").split(",")
162     end
163
164     def parse_acl(acl)
165       parse_array(acl).each_with_object({}) do |entry, permissions|
166         entry = entry.sub(/^"(.*)"$/) { Regexp.last_match[1].gsub(/\\"/, '"') }.sub(%r{/.*$}, "")
167         user, privileges = entry.split("=")
168
169         user = user.sub(/^"(.*)"$/, "\\1")
170         user = "public" if user == ""
171
172         permissions[user] = {
173           "r" => :select, "a" => :insert, "w" => :update, "d" => :delete,
174           "D" => :truncate, "x" => :references, "t" => :trigger,
175           "C" => :create, "c" => :connect, "T" => :temporary,
176           "X" => :execute, "U" => :usage, "s" => :set, "A" => :alter_system
177         }.values_at(*privileges.chars).compact
178       end
179     end
180   end
181 end