md-puppetdb-terminus 2.0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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