md-puppetdb-terminus 2.0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,350 @@
1
+ require 'puppet/resource/catalog'
2
+ require 'puppet/indirector/rest'
3
+ require 'puppet/util/puppetdb'
4
+
5
+ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
6
+ include Puppet::Util::Puppetdb
7
+ include Puppet::Util::Puppetdb::CommandNames
8
+
9
+ # Run initial checks
10
+ def initialize
11
+ Puppet::Util::Puppetdb::GlobalCheck.run
12
+ end
13
+
14
+ def save(request)
15
+ profile "catalog#save" do
16
+ catalog = munge_catalog(request.instance, extract_extra_request_data(request))
17
+
18
+ submit_command(request.key, catalog, CommandReplaceCatalog, 4)
19
+ end
20
+ end
21
+
22
+ def find(request)
23
+ nil
24
+ end
25
+
26
+ # @api private
27
+ def extract_extra_request_data(request)
28
+ {
29
+ :transaction_uuid => request.options[:transaction_uuid],
30
+ :environment => request.environment,
31
+ }
32
+ end
33
+
34
+ def munge_catalog(catalog, extra_request_data = {})
35
+ profile "Munge catalog" do
36
+ hash = profile "Convert catalog to PSON data hash" do
37
+ catalog.to_pson_data_hash
38
+ end
39
+
40
+ data = hash['data']
41
+
42
+ add_parameters_if_missing(data)
43
+ add_namevar_aliases(data, catalog)
44
+ stringify_titles(data)
45
+ stringify_version(data)
46
+ sort_unordered_metaparams(data)
47
+ munge_edges(data)
48
+ synthesize_edges(data, catalog)
49
+ filter_keys(data)
50
+ add_transaction_uuid(data, extra_request_data[:transaction_uuid])
51
+ add_environment(data, extra_request_data[:environment])
52
+
53
+ data
54
+ end
55
+ end
56
+
57
+ Relationships = {
58
+ :before => {:direction => :forward, :relationship => 'before'},
59
+ :require => {:direction => :reverse, :relationship => 'required-by'},
60
+ :notify => {:direction => :forward, :relationship => 'notifies'},
61
+ :subscribe => {:direction => :reverse, :relationship => 'subscription-of'},
62
+ }
63
+
64
+ # Metaparams that may contain arrays, but whose semantics are
65
+ # fundamentally unordered
66
+ UnorderedMetaparams = [:alias, :audit, :before, :check, :notify, :require, :subscribe, :tag]
67
+
68
+ # Include environment in hash, returning the complete hash.
69
+ #
70
+ # @param hash [Hash] original data hash
71
+ # @param environment [String] environment
72
+ # @return [Hash] returns original hash augmented with environment
73
+ # @api private
74
+ def add_environment(hash, environment)
75
+ hash['environment'] = environment
76
+
77
+ hash
78
+ end
79
+
80
+ # Include transaction_uuid in hash, returning the complete hash.
81
+ #
82
+ # @param hash [Hash] original data hash
83
+ # @param transaction_uuid [String] transaction_uuid
84
+ # @return [Hash] returns original hash augmented with transaction_uuid
85
+ # @api private
86
+ def add_transaction_uuid(hash, transaction_uuid)
87
+ hash['transaction-uuid'] = transaction_uuid
88
+
89
+ hash
90
+ end
91
+
92
+ # Version is an integer (time since epoch in millis). The wire
93
+ # format specifies version should be a string
94
+ #
95
+ # @param hash [Hash] original data hash
96
+ # @return [Hash] returns a modified original hash
97
+ def stringify_version(hash)
98
+ hash['version'] = hash['version'].to_s
99
+
100
+ hash
101
+ end
102
+
103
+ def stringify_titles(hash)
104
+ resources = hash['resources']
105
+ profile "Stringify titles (resource count: #{resources.count})" do
106
+ resources.each do |resource|
107
+ resource['title'] = resource['title'].to_s
108
+ end
109
+ end
110
+
111
+ hash
112
+ end
113
+
114
+ def add_parameters_if_missing(hash)
115
+ resources = hash['resources']
116
+ profile "Add parameters if missing (resource count: #{resources.count})" do
117
+ resources.each do |resource|
118
+ resource['parameters'] ||= {}
119
+ end
120
+ end
121
+
122
+ hash
123
+ end
124
+
125
+ def add_namevar_aliases(hash, catalog)
126
+ resources = hash['resources']
127
+ profile "Add namevar aliases (resource count: #{resources.count})" do
128
+ resources.each do |resource|
129
+ real_resource = catalog.resource(resource['type'], resource['title'])
130
+
131
+ # Resources with composite namevars can't be referred to by
132
+ # anything other than their title when declaring
133
+ # relationships. Trying to snag the :alias for these resources
134
+ # will only return _part_ of the name (a problem with Puppet
135
+ # proper), so skipping the adding of aliases for these resources
136
+ # is both an optimization and a safeguard.
137
+ next if real_resource.key_attributes.count > 1
138
+
139
+ aliases = [real_resource[:alias]].flatten.compact
140
+
141
+ # Non-isomorphic resources aren't unique based on namevar, so we can't
142
+ # use it as an alias
143
+ type = real_resource.resource_type
144
+ if !type.respond_to?(:isomorphic?) or type.isomorphic?
145
+ # This makes me a little sad. It turns out that the "to_hash" method
146
+ # of Puppet::Resource can have side effects. In particular, if the
147
+ # resource type specifies a title_pattern, calling "to_hash" will trigger
148
+ # the title_pattern processing, which can have the side effect of
149
+ # populating the namevar (potentially with a munged value). Thus,
150
+ # it is important that we search for namevar aliases in that hash
151
+ # rather than in the resource itself.
152
+ real_resource_hash = real_resource.to_hash
153
+
154
+ name = real_resource_hash[real_resource.send(:namevar)]
155
+ unless name.nil? or real_resource.title == name or aliases.include?(name)
156
+ aliases << name
157
+ end
158
+ end
159
+
160
+ resource['parameters']['alias'] = aliases unless aliases.empty?
161
+ end
162
+ end
163
+
164
+ hash
165
+ end
166
+
167
+ def sort_unordered_metaparams(hash)
168
+ resources = hash['resources']
169
+ profile "Sort unordered metaparams (resource count: #{resources.count})" do
170
+ resources.each do |resource|
171
+ params = resource['parameters']
172
+ UnorderedMetaparams.each do |metaparam|
173
+ if params[metaparam].kind_of? Array then
174
+ values = params[metaparam].sort
175
+ params[metaparam] = values unless values.empty?
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ hash
182
+ end
183
+
184
+ def munge_edges(hash)
185
+ edges = hash['edges']
186
+ profile "Munge edges (edge count: #{edges.count})" do
187
+ edges.each do |edge|
188
+ %w[source target].each do |vertex|
189
+ edge[vertex] = resource_ref_to_hash(edge[vertex]) if edge[vertex].is_a?(String)
190
+ end
191
+ edge['relationship'] ||= 'contains'
192
+ end
193
+
194
+ hash
195
+ end
196
+ end
197
+
198
+ def map_aliases_to_title(hash)
199
+ resources = hash['resources']
200
+ aliases = {}
201
+
202
+ profile "Map aliases to title (resource count: #{resources.count})" do
203
+ resources.each do |resource|
204
+ names = resource['parameters']['alias'] || []
205
+ resource_hash = {'type' => resource['type'], 'title' => resource['title']}
206
+ names.each do |name|
207
+ alias_array = [resource['type'], name]
208
+ aliases[alias_array] = resource_hash
209
+ end
210
+ end
211
+ end
212
+
213
+ aliases
214
+ end
215
+
216
+ def synthesize_edges(hash, catalog)
217
+ profile "Synthesize edges" do
218
+ aliases = map_aliases_to_title(hash)
219
+
220
+ resource_table = {}
221
+ profile "Build up resource_table" do
222
+ hash['resources'].each do |resource|
223
+ resource_table[ [resource['type'], resource['title']] ] = resource
224
+ end
225
+ end
226
+
227
+ profile "Primary synthesis" do
228
+ hash['resources'].each do |resource|
229
+ # Standard virtual resources don't appear in the catalog. However,
230
+ # exported resources which haven't been also collected will appears as
231
+ # exported and virtual (collected ones will only be exported). They will
232
+ # eventually be removed from the catalog, so we can't add edges involving
233
+ # them. Puppet::Resource#to_pson_data_hash omits 'virtual', so we have to
234
+ # look it up in the catalog to find that information. This isn't done in
235
+ # a separate step because we don't actually want to send the field (it
236
+ # will always be false). See ticket #16472.
237
+ #
238
+ # The outer conditional is here because Class[main] can't properly be
239
+ # looked up using catalog.resource and will return nil. See ticket
240
+ # #16473. Yay.
241
+ if real_resource = catalog.resource(resource['type'], resource['title'])
242
+ next if real_resource.virtual?
243
+ end
244
+
245
+ Relationships.each do |param,relation|
246
+ if value = resource['parameters'][param]
247
+ [value].flatten.each do |other_ref|
248
+ edge = {'relationship' => relation[:relationship]}
249
+
250
+ resource_hash = {'type' => resource['type'], 'title' => resource['title']}
251
+ other_hash = resource_ref_to_hash(other_ref)
252
+
253
+ # Puppet doesn't always seem to check this correctly. If we don't
254
+ # users will later get an invalid relationship error instead.
255
+ #
256
+ # Primarily we are trying to catch the non-capitalized resourceref
257
+ # case problem here: http://projects.puppetlabs.com/issues/19474
258
+ # Once that problem is solved and older versions of Puppet that have
259
+ # the bug are no longer supported we can probably remove this code.
260
+ unless other_ref =~ /^[A-Z][a-z0-9_]*(::[A-Z][a-z0-9_]*)*\[.*\]/
261
+ rel = edge_to_s(resource_hash_to_ref(resource_hash), other_ref, param)
262
+ raise Puppet::Error, "Invalid relationship: #{rel}, because " +
263
+ "#{other_ref} doesn't seem to be in the correct format. " +
264
+ "Resource references should be formatted as: " +
265
+ "Classname['title'] or Modulename::Classname['title'] (take " +
266
+ "careful note of the capitalization)."
267
+ end
268
+
269
+ # This is an unfortunate hack. Puppet does some weird things w/rt
270
+ # munging off trailing slashes from file resources, and users may
271
+ # legally specify relationships using a different number of trailing
272
+ # slashes than the resource was originally declared with.
273
+ # We do know that for any file resource in the catalog, there should
274
+ # be a canonical entry for it that contains no trailing slashes. So,
275
+ # here, in case someone has specified a relationship to a file resource
276
+ # and has used one or more trailing slashes when specifying the
277
+ # relationship, we will munge off the trailing slashes before
278
+ # we look up the resource in the catalog to create the edge.
279
+ if other_hash['type'] == 'File' and other_hash['title'] =~ /\/$/
280
+ other_hash['title'] = other_hash['title'].sub(/\/+$/, '')
281
+ end
282
+
283
+ other_array = [other_hash['type'], other_hash['title']]
284
+
285
+ # Try to find the resource by type/title or look it up as an alias
286
+ # and try that
287
+ other_resource = resource_table[other_array]
288
+ if other_resource.nil? and alias_hash = aliases[other_array]
289
+ other_resource = resource_table[ alias_hash.values_at('type', 'title') ]
290
+ end
291
+
292
+ raise Puppet::Error, "Invalid relationship: #{edge_to_s(resource_hash_to_ref(resource_hash), other_ref, param)}, because #{other_ref} doesn't seem to be in the catalog" unless other_resource
293
+
294
+ # As above, virtual exported resources will eventually be removed,
295
+ # so if a real resource refers to one, it's wrong. Non-virtual
296
+ # exported resources are exported resources that were also
297
+ # collected in this catalog, so they're okay. Virtual non-exported
298
+ # resources can't appear in the catalog in the first place, so it
299
+ # suffices to check for virtual.
300
+ if other_real_resource = catalog.resource(other_resource['type'], other_resource['title'])
301
+ if other_real_resource.virtual?
302
+ raise Puppet::Error, "Invalid relationship: #{edge_to_s(resource_hash_to_ref(resource_hash), other_ref, param)}, because #{other_ref} is exported but not collected"
303
+ end
304
+ end
305
+
306
+ # If the ref was an alias, it will have a different title, so use
307
+ # that
308
+ other_hash['title'] = other_resource['title']
309
+
310
+ if relation[:direction] == :forward
311
+ edge.merge!('source' => resource_hash, 'target' => other_hash)
312
+ else
313
+ edge.merge!('source' => other_hash, 'target' => resource_hash)
314
+ end
315
+ hash['edges'] << edge
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ profile "Make edges unique" do
323
+ hash['edges'].uniq!
324
+ end
325
+
326
+ hash
327
+ end
328
+ end
329
+
330
+ def filter_keys(hash)
331
+ profile "Filter extraneous keys from the catalog" do
332
+ hash.delete_if do |k,v|
333
+ ! ['name', 'version', 'edges', 'resources'].include?(k)
334
+ end
335
+ end
336
+ end
337
+
338
+ def resource_ref_to_hash(ref)
339
+ ref =~ /^([^\[\]]+)\[(.+)\]$/m
340
+ {'type' => $1, 'title' => $2}
341
+ end
342
+
343
+ def resource_hash_to_ref(hash)
344
+ "#{hash['type']}[#{hash['title']}]"
345
+ end
346
+
347
+ def edge_to_s(specifier_resource, referred_resource, param)
348
+ "#{specifier_resource} { #{param} => #{referred_resource} }"
349
+ end
350
+ end
@@ -0,0 +1,134 @@
1
+ require 'uri'
2
+ require 'puppet/node/facts'
3
+ require 'puppet/indirector/rest'
4
+ require 'puppet/util/puppetdb'
5
+ require 'json'
6
+
7
+ class Puppet::Node::Facts::Puppetdb < Puppet::Indirector::REST
8
+ include Puppet::Util::Puppetdb
9
+ include Puppet::Util::Puppetdb::CommandNames
10
+
11
+ # Run initial checks
12
+ def initialize
13
+ Puppet::Util::Puppetdb::GlobalCheck.run
14
+ end
15
+
16
+ def save(request)
17
+ profile "facts#save" do
18
+ payload = profile "Encode facts command submission payload" do
19
+ facts = request.instance.dup
20
+ facts.values = facts.values.dup
21
+ facts.stringify
22
+ {
23
+ "name" => facts.name,
24
+ "values" => facts.values,
25
+ # PDB-453: we call to_s to avoid a 'stack level too deep' error
26
+ # when we attempt to use ActiveSupport 2.3.16 on RHEL 5 with
27
+ # legacy storeconfigs.
28
+ "environment" => request.environment.to_s,
29
+ }
30
+ end
31
+
32
+ submit_command(request.key, payload, CommandReplaceFacts, 2)
33
+ end
34
+ end
35
+
36
+ def find(request)
37
+ profile "facts#find" do
38
+ begin
39
+ url = "/v3/nodes/#{CGI.escape(request.key)}/facts"
40
+ response = profile "Query for nodes facts: #{url}" do
41
+ http_get(request, url, headers)
42
+ end
43
+ log_x_deprecation_header(response)
44
+
45
+ if response.is_a? Net::HTTPSuccess
46
+ profile "Parse fact query response (size: #{response.body.size})" do
47
+ result = JSON.parse(response.body)
48
+ # Note: the Inventory Service API appears to expect us to return nil here
49
+ # if the node isn't found. However, PuppetDB returns an empty array in
50
+ # this case; for now we will just look for that condition and assume that
51
+ # it means that the node wasn't found, so we will return nil. In the
52
+ # future we may want to improve the logic such that we can distinguish
53
+ # between the "node not found" and the "no facts for this node" cases.
54
+ if result.empty?
55
+ return nil
56
+ end
57
+ facts = result.inject({}) do |a,h|
58
+ a.merge(h['name'] => h['value'])
59
+ end
60
+ Puppet::Node::Facts.new(request.key, facts)
61
+ end
62
+ else
63
+ # Newline characters cause an HTTP error, so strip them
64
+ raise "[#{response.code} #{response.message}] #{response.body.gsub(/[\r\n]/, '')}"
65
+ end
66
+ rescue => e
67
+ raise Puppet::Error, "Failed to find facts from PuppetDB at #{self.class.server}:#{self.class.port}: #{e}"
68
+ end
69
+ end
70
+ end
71
+
72
+ # Search for nodes matching a set of fact constraints. The constraints are
73
+ # specified as a hash of the form:
74
+ #
75
+ # `{type.name.operator => value`
76
+ #
77
+ # The only accepted `type` is 'facts'.
78
+ #
79
+ # `name` must be the fact name to query against.
80
+ #
81
+ # `operator` may be one of {eq, ne, lt, gt, le, ge}, and will default to 'eq'
82
+ # if unspecified.
83
+ def search(request)
84
+ profile "facts#search" do
85
+ return [] unless request.options
86
+ operator_map = {
87
+ 'eq' => '=',
88
+ 'gt' => '>',
89
+ 'lt' => '<',
90
+ 'ge' => '>=',
91
+ 'le' => '<=',
92
+ }
93
+ filters = request.options.sort.map do |key,value|
94
+ type, name, operator = key.to_s.split('.')
95
+ operator ||= 'eq'
96
+ raise Puppet::Error, "Fact search against keys of type '#{type}' is unsupported" unless type == 'facts'
97
+ if operator == 'ne'
98
+ ['not', ['=', ['fact', name], value]]
99
+ else
100
+ [operator_map[operator], ['fact', name], value]
101
+ end
102
+ end
103
+
104
+ query = ["and"] + filters
105
+ query_param = CGI.escape(query.to_json)
106
+
107
+ begin
108
+ url = "/v3/nodes?query=#{query_param}"
109
+ response = profile "Fact query request: #{URI.unescape(url)}" do
110
+ http_get(request, url, headers)
111
+ end
112
+ log_x_deprecation_header(response)
113
+
114
+ if response.is_a? Net::HTTPSuccess
115
+ profile "Parse fact query response (size: #{response.body.size})" do
116
+ JSON.parse(response.body).collect {|s| s["name"]}
117
+ end
118
+ else
119
+ # Newline characters cause an HTTP error, so strip them
120
+ raise "[#{response.code} #{response.message}] #{response.body.gsub(/[\r\n]/, '')}"
121
+ end
122
+ rescue => e
123
+ raise Puppet::Error, "Could not perform inventory search from PuppetDB at #{self.class.server}:#{self.class.port}: #{e}"
124
+ end
125
+ end
126
+ end
127
+
128
+ def headers
129
+ {
130
+ "Accept" => "application/json",
131
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
132
+ }
133
+ end
134
+ end