quixoten-puppetdb-terminus 3.2.4 → 4.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 910dde6a5e9dd41e559bccccafa56f87803d3102
4
- data.tar.gz: fe5dac1f457c8fae51a09a75f1d9597c042f5a3d
3
+ metadata.gz: 4470280424f9b67fc3543f3009a0f91916257ff4
4
+ data.tar.gz: 7bfa68983d4a6f301b16250e78019299e5d2d88f
5
5
  SHA512:
6
- metadata.gz: ad7d999498f478abf34e4a91549211e749d092f94136c25cee2d64eb3d212a13ad2dfe2ec8564909f3e579067eb8f568a9b03e30cc9369f559a6914cda3080ba
7
- data.tar.gz: 52a1d74294eedb0a0970bc7e0a41af0f15ff9bd76c7610803a911b7d48490a2966cdb95ddac9380c9245b653c24c9816bcd3ad916843269cb70d7a5c14cabbd4
6
+ metadata.gz: 11c510c0300fc56d183a5c8a80322cd16115111e45c4cf238ffdb842bb9de465aab2ae84199b356bae065a8167fef637290b65ff724820b394e8099018f7574f
7
+ data.tar.gz: 670591770ccaebb59f44f00fdcb7e7e5ec3969c878845edcb47599e6094cc89221977084585a45b7f1b7926d3c3d48514663c17de4f323857357da65baa57cd1
@@ -19,7 +19,7 @@ Puppet::Face.define(:node, '0.0.1') do
19
19
 
20
20
  args.map do |node|
21
21
  begin
22
- response = Puppet::Util::Puppetdb::Http.action("/pdb/query/v4/nodes/#{CGI.escape(node)}") do |http_instance, path|
22
+ response = Puppet::Util::Puppetdb::Http.action("/pdb/query/v4/nodes/#{CGI.escape(node)}", :query) do |http_instance, path|
23
23
  http_instance.get(path, headers)
24
24
  end
25
25
  if response.is_a? Net::HTTPSuccess
@@ -0,0 +1,6 @@
1
+ require 'puppet/util/puppetdb'
2
+ Puppet::Functions.create_function(:puppetdb_query) do
3
+ def puppetdb_query(query)
4
+ Puppet::Util::Puppetdb.query_puppetdb(query)
5
+ end
6
+ end
@@ -10,7 +10,7 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
10
10
  def save(request)
11
11
  profile("catalog#save", [:puppetdb, :catalog, :save, request.key]) do
12
12
  catalog = munge_catalog(request.instance, extract_extra_request_data(request))
13
- submit_command(request.key, catalog, CommandReplaceCatalog, 7)
13
+ submit_command(request.key, catalog, CommandReplaceCatalog, 8)
14
14
  end
15
15
  end
16
16
 
@@ -24,7 +24,6 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
24
24
  :transaction_uuid => request.options[:transaction_uuid],
25
25
  :environment => request.environment.to_s,
26
26
  :producer_timestamp => request.options[:producer_timestamp] || Time.now.iso8601(5),
27
- :code_id => request.options[:code_id],
28
27
  }
29
28
  end
30
29
 
@@ -39,6 +38,8 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
39
38
  catalog.to_data_hash
40
39
  end
41
40
 
41
+ add_code_id_if_missing(data)
42
+ add_catalog_uuid_if_missing(data, extra_request_data[:transaction_uuid])
42
43
  add_parameters_if_missing(data)
43
44
  add_namevar_aliases(data, catalog)
44
45
  stringify_titles(data)
@@ -47,12 +48,11 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
47
48
  sort_unordered_metaparams(data)
48
49
  munge_edges(data)
49
50
  synthesize_edges(data, catalog)
51
+ change_name_to_certname(data)
50
52
  filter_keys(data)
51
53
  add_transaction_uuid(data, extra_request_data[:transaction_uuid])
52
54
  add_environment(data, extra_request_data[:environment])
53
55
  add_producer_timestamp(data, extra_request_data[:producer_timestamp])
54
- change_name_to_certname(data)
55
- add_code_id(data, extra_request_data[:code_id])
56
56
 
57
57
  data
58
58
  end
@@ -119,11 +119,25 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
119
119
  # Include code_id in hash, returning the complete hash.
120
120
  #
121
121
  # @param hash [Hash] original data hash
122
- # @param code_id [String] code_id
123
- # @return [Hash] returns original hash augmented with transaction_uuid
122
+ # @return [Hash] returns original hash with a gaurunteed code_id key
123
+ # @api private
124
+ def add_code_id_if_missing(hash)
125
+ # This weird code ensure that `hash` will always have a `code_id` key and if
126
+ # it already had a `code_id` key we use that as the value. If `hash` didn't
127
+ # have a `code_id` key the lookup will return nil and hash['code_id'] == nil
128
+ hash['code_id'] = hash['code_id']
129
+
130
+ hash
131
+ end
132
+
133
+ # Include code_id in hash, returning the complete hash.
134
+ #
135
+ # @param hash [Hash] original data hash
136
+ # @param default [String] default catalog_uuid to use if hash doesn't have one
137
+ # @return [Hash] returns original hash with a gaurunteed catalog_uuid key/value
124
138
  # @api private
125
- def add_code_id(hash, code_id)
126
- hash['code_id'] = code_id
139
+ def add_catalog_uuid_if_missing(hash, default)
140
+ hash['catalog_uuid'] = hash['catalog_uuid'] || default
127
141
 
128
142
  hash
129
143
  end
@@ -305,7 +319,7 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
305
319
  # case problem here: http://projects.puppetlabs.com/issues/19474
306
320
  # Once that problem is solved and older versions of Puppet that have
307
321
  # the bug are no longer supported we can probably remove this code.
308
- unless other_ref =~ /^[A-Z][a-z0-9_-]*(::[A-Z][a-z0-9_-]*)*\[.*\]/m
322
+ unless other_ref =~ /^[A-Z][a-z0-9_]*(::[A-Z][a-z0-9_]*)*\[.*\]/m
309
323
  rel = edge_to_s(resource_hash_to_ref(resource_hash), other_ref, param)
310
324
  raise Puppet::Error, "Invalid relationship: #{rel}, because " +
311
325
  "#{other_ref} doesn't seem to be in the correct format. " +
@@ -380,7 +394,12 @@ class Puppet::Resource::Catalog::Puppetdb < Puppet::Indirector::REST
380
394
  profile("Filter extraneous keys from the catalog",
381
395
  [:puppetdb, :keys, :filter_extraneous]) do
382
396
  hash.delete_if do |k,v|
383
- ! ['name', 'version', 'edges', 'resources'].include?(k)
397
+ ! ['certname',
398
+ 'version',
399
+ 'edges',
400
+ 'resources',
401
+ 'code_id',
402
+ 'catalog_uuid'].include?(k)
384
403
  end
385
404
  end
386
405
  end
@@ -16,24 +16,13 @@ class Puppet::Node::Facts::Puppetdb < Puppet::Indirector::REST
16
16
  trusted.to_h
17
17
  end
18
18
 
19
- def maybe_strip_internal(facts)
20
- if Puppet::Node::Facts.method_defined? :strip_internal
21
- facts.strip_internal
22
- else
23
- facts.values
24
- end
25
- end
26
-
27
19
  def save(request)
28
20
  profile("facts#save", [:puppetdb, :facts, :save, request.key]) do
29
21
  payload = profile("Encode facts command submission payload",
30
22
  [:puppetdb, :facts, :encode]) do
31
23
  facts = request.instance.dup
32
- facts.values = facts.strip_internal.dup
33
-
34
- if ! Puppet::Util::Puppetdb.puppet3compat? || Puppet[:trusted_node_data]
35
- facts.values[:trusted] = get_trusted_info(request.node)
36
- end
24
+ facts.values = facts.values.dup
25
+ facts.values[:trusted] = get_trusted_info(request.node)
37
26
  {
38
27
  "certname" => facts.name,
39
28
  "values" => facts.values,
@@ -52,7 +41,7 @@ class Puppet::Node::Facts::Puppetdb < Puppet::Indirector::REST
52
41
  def find(request)
53
42
  profile("facts#find", [:puppetdb, :facts, :find, request.key]) do
54
43
  begin
55
- response = Http.action("/pdb/query/v4/nodes/#{CGI.escape(request.key)}/facts") do |http_instance, path|
44
+ response = Http.action("/pdb/query/v4/nodes/#{CGI.escape(request.key)}/facts", :query) do |http_instance, path|
56
45
  profile("Query for nodes facts: #{URI.unescape(path)}",
57
46
  [:puppetdb, :facts, :find, :query_nodes, request.key]) do
58
47
  http_instance.get(path, headers)
@@ -120,7 +109,7 @@ class Puppet::Node::Facts::Puppetdb < Puppet::Indirector::REST
120
109
  query_param = CGI.escape(query.to_json)
121
110
 
122
111
  begin
123
- response = Http.action("/pdb/query/v4/nodes?query=#{query_param}") do |http_instance, path|
112
+ response = Http.action("/pdb/query/v4/nodes?query=#{query_param}", :query) do |http_instance, path|
124
113
  profile("Fact query request: #{URI.unescape(path)}",
125
114
  [:puppetdb, :facts, :search, :query_request, request.key]) do
126
115
  http_instance.get(path, headers)
@@ -27,7 +27,7 @@ class Puppet::Resource::Puppetdb < Puppet::Indirector::REST
27
27
  query_param = CGI.escape(expr.to_json)
28
28
 
29
29
  begin
30
- response = Http.action("/pdb/query/v4/resources?query=#{query_param}") do |http_instance, path|
30
+ response = Http.action("/pdb/query/v4/resources?query=#{query_param}", :query) do |http_instance, path|
31
31
  profile("Resources query: #{URI.unescape(path)}",
32
32
  [:puppetdb, :resource, :search, :query, request.key]) do
33
33
  http_instance.get(path, headers)
@@ -19,7 +19,7 @@ Puppet::Reports.register_report(:puppetdb) do
19
19
  # @return [void]
20
20
  def process
21
21
  profile("report#process", [:puppetdb, :report, :process]) do
22
- submit_command(self.host, report_to_hash, CommandStoreReport, 6)
22
+ submit_command(self.host, report_to_hash, CommandStoreReport, 7)
23
23
  end
24
24
 
25
25
  nil
@@ -40,21 +40,28 @@ Puppet::Reports.register_report(:puppetdb) do
40
40
  resources = build_resources_list
41
41
  is_noop = resources.any? { |rs| has_noop_event?(rs) } and resources.none? { |rs| has_failed_event?(rs) }
42
42
 
43
+
44
+ defaulted_catalog_uuid = defined?(catalog_uuid) ? catalog_uuid : transaction_uuid
45
+ defaulted_code_id = defined?(code_id) ? code_id : nil
46
+ defaulted_cached_catalog_status = defined?(cached_catalog_status) ? cached_catalog_status : nil
43
47
  {
44
- "certname" => host,
45
- "puppet_version" => puppet_version,
46
- "report_format" => report_format,
47
- "configuration_version" => configuration_version.to_s,
48
- "producer_timestamp" => Puppet::Util::Puppetdb.to_wire_time(Time.now),
49
- "start_time" => Puppet::Util::Puppetdb.to_wire_time(time),
50
- "end_time" => Puppet::Util::Puppetdb.to_wire_time(time + run_duration),
51
- "environment" => environment,
52
- "transaction_uuid" => transaction_uuid,
53
- "status" => status,
54
- "noop" => is_noop,
55
- "logs" => build_logs_list,
56
- "metrics" => build_metrics_list,
57
- "resources" => resources,
48
+ "certname" => host,
49
+ "puppet_version" => puppet_version,
50
+ "report_format" => report_format,
51
+ "configuration_version" => configuration_version.to_s,
52
+ "producer_timestamp" => Puppet::Util::Puppetdb.to_wire_time(Time.now),
53
+ "start_time" => Puppet::Util::Puppetdb.to_wire_time(time),
54
+ "end_time" => Puppet::Util::Puppetdb.to_wire_time(time + run_duration),
55
+ "environment" => environment,
56
+ "transaction_uuid" => transaction_uuid,
57
+ "status" => status,
58
+ "noop" => is_noop,
59
+ "logs" => build_logs_list,
60
+ "metrics" => build_metrics_list,
61
+ "resources" => resources,
62
+ "catalog_uuid" => defaulted_catalog_uuid,
63
+ "code_id" => defaulted_code_id,
64
+ "cached_catalog_status" => defaulted_cached_catalog_status,
58
65
  }
59
66
  end
60
67
  end
@@ -6,6 +6,7 @@ require 'puppet/util/puppetdb/command'
6
6
  require 'puppet/util/puppetdb/config'
7
7
  require 'digest/sha1'
8
8
  require 'time'
9
+ require 'json'
9
10
  require 'fileutils'
10
11
 
11
12
  module Puppet::Util::Puppetdb
@@ -28,10 +29,6 @@ module Puppet::Util::Puppetdb
28
29
  @config
29
30
  end
30
31
 
31
- def self.puppet3compat?
32
- defined?(Puppet::Parser::AST::HashOrArrayAccess)
33
- end
34
-
35
32
  # Given an instance of ruby's Time class, this method converts it to a String
36
33
  # that conforms to PuppetDB's wire format for representing a date/time.
37
34
  def self.to_wire_time(time)
@@ -68,6 +65,21 @@ module Puppet::Util::Puppetdb
68
65
  end
69
66
  end
70
67
 
68
+ # Query PuppetDB.
69
+ #
70
+ # @param query [String, Array] The PQL or AST query for PuppetDB
71
+ # @return [Array<Hash>]
72
+ def self.query_puppetdb(query)
73
+ Puppet::Util::Profiler.profile("Submitted query '#{query}'", [:puppetdb, :query, query]) do
74
+ headers = { "Accept" => "application/json",
75
+ "Content-Type" => "application/json; charset=UTF-8" }
76
+ response = Puppet::Util::Puppetdb::Http.action("/pdb/query/v4", :query) do |http_instance, path|
77
+ http_instance.post(path, { 'query' => query }.to_json, headers)
78
+ end
79
+ JSON.parse(response.body)
80
+ end
81
+ end
82
+
71
83
  # Profile a block of code and log the time it took to execute.
72
84
  #
73
85
  # This outputs logs entries to the Puppet masters logging destination
@@ -0,0 +1,22 @@
1
+ require 'thread'
2
+
3
+ module Puppet::Util::Puppetdb
4
+ class Atom
5
+ def initialize(value)
6
+ @value = value
7
+ @mutex = Mutex.new
8
+ end
9
+
10
+ def deref()
11
+ @mutex.synchronize {
12
+ @value
13
+ }
14
+ end
15
+
16
+ def reset(value)
17
+ @mutex.synchronize {
18
+ @value = value
19
+ }
20
+ end
21
+ end
22
+ end
@@ -65,7 +65,7 @@ module CharEncoding
65
65
  # @param bad_char_range a range indicating a block of invalid characters
66
66
  # @return String
67
67
  def self.error_char_context(str, bad_char_range)
68
-
68
+
69
69
  gap = bad_char_range.to_a.length
70
70
 
71
71
  start_char = [0, bad_char_range.begin-100].max
@@ -106,9 +106,10 @@ module CharEncoding
106
106
  # information using error_context_str
107
107
  #
108
108
  # @param str A string coming from to_pson, likely a command to be submitted to PDB
109
- # @param error_context_str information about where this string came from for use in error messages
109
+ # @param error_context_str information about where this string came from for
110
+ # use in error messages. Defaults to nil, in which case no error is reported.
110
111
  # @return Str
111
- def self.coerce_to_utf8(str, error_context_str)
112
+ def self.coerce_to_utf8(str, error_context_str=nil)
112
113
  str_copy = str.dup
113
114
  # This code is passed in a string that was created by
114
115
  # to_pson. to_pson calls force_encoding('ASCII-8BIT') on the
@@ -127,30 +128,27 @@ module CharEncoding
127
128
  # byte related issues that could arise from mis-interpreting a
128
129
  # random extra byte as part of a multi-byte UTF-8 character
129
130
  str_copy.force_encoding("US-ASCII")
130
- warn_if_invalid_chars(str_copy.encode!("UTF-8",
131
- :invalid => :replace,
132
- :undef => :replace,
133
- :replace => DEFAULT_INVALID_CHAR),
134
- error_context_str)
131
+
132
+ str_lossy = str_copy.encode!("UTF-8",
133
+ :invalid => :replace,
134
+ :undef => :replace,
135
+ :replace => DEFAULT_INVALID_CHAR)
136
+ if !error_context_str.nil?
137
+ warn_if_invalid_chars(str_lossy, error_context_str)
138
+ else
139
+ str_lossy
140
+ end
135
141
  end
136
142
  end
137
143
 
138
144
  def self.utf8_string(str, error_context_str)
139
- if RUBY_VERSION =~ /^1.8/
140
- # Ruby 1.8 doesn't have String#encode and related methods, and there
141
- # appears to be a bug in iconv that will interpret some byte sequences
142
- # as 6-byte characters. Thus, we are forced to resort to some unfortunate
143
- # manual chicanery.
144
- warn_if_changed(str, ruby18_clean_utf8(str))
145
- else
146
- begin
147
- coerce_to_utf8(str, error_context_str)
148
- rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
149
- # If we got an exception, the string is either invalid or not
150
- # convertible to UTF-8, so drop those bytes.
145
+ begin
146
+ coerce_to_utf8(str, error_context_str)
147
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
148
+ # If we got an exception, the string is either invalid or not
149
+ # convertible to UTF-8, so drop those bytes.
151
150
 
152
- warn_if_changed(str, str.encode('UTF-8', :invalid => :replace, :undef => :replace))
153
- end
151
+ warn_if_changed(str, str.encode('UTF-8', :invalid => :replace, :undef => :replace))
154
152
  end
155
153
  end
156
154
 
@@ -162,137 +160,6 @@ module CharEncoding
162
160
  converted_str
163
161
  end
164
162
 
165
- # @api private
166
- def self.ruby18_clean_utf8(str)
167
- #iconv_to_utf8(str)
168
- #ruby18_manually_clean_utf8(str)
169
-
170
- # So, we've tried doing this UTF8 cleaning for ruby 1.8 a few different
171
- # ways. Doing it via IConv, we don't do a good job of handling characters
172
- # whose codepoints would exceed the legal maximum for UTF-8. Doing it via
173
- # our manual scrubbing process is slower and doesn't catch overlong
174
- # encodings. Since this code really shouldn't even exist in the first place
175
- # we've decided to simply compose the two scrubbing methods for now, rather
176
- # than trying to add detection of overlong encodings. It'd be a non-trivial
177
- # chunk of code, and it'd have to do a lot of bitwise arithmetic (which Ruby
178
- # is not blazingly fast at).
179
- ruby18_manually_clean_utf8(iconv_to_utf8(str))
180
- end
181
-
182
-
183
- # @todo we're not using this anymore, but I wanted to leave it around
184
- # for a little while just to make sure that the new code pans out.
185
- # @api private
186
- def self.iconv_to_utf8(str)
187
- iconv = Iconv.new('UTF-8//IGNORE', 'UTF-8')
188
-
189
- # http://po-ru.com/diary/fixing-invalid-utf-8-in-ruby-revisited/
190
- iconv.iconv(str + " ")[0..-2]
191
- end
192
-
193
- # @api private
194
- def self.get_char_len(byte)
195
- Utf8CharLens[byte]
196
- end
197
-
198
- # Manually cleans a string by stripping any byte sequences that are
199
- # not valid UTF-8 characters. If you'd prefer for the invalid bytes to be
200
- # replaced with the unicode replacement character rather than being stripped,
201
- # you may pass `false` for the optional second parameter (`strip`, which
202
- # defaults to `true`).
203
- #
204
- # @api private
205
- def self.ruby18_manually_clean_utf8(str, strip = true)
206
-
207
- # This is a hack to allow this code to work with either ruby 1.8 or 1.9,
208
- # which is useful for debugging and benchmarking. For more info see the
209
- # comments in the #get_byte method below.
210
- @has_get_byte = str.respond_to?(:getbyte)
211
-
212
-
213
- i = 0
214
- len = str.length
215
- result = ""
216
-
217
- while i < len
218
- byte = get_byte(str, i)
219
-
220
- i += 1
221
-
222
- char_len = get_char_len(byte)
223
- case char_len
224
- when 0
225
- result.concat(Utf8ReplacementChar) unless strip
226
- when 1
227
- result << byte
228
- when 2..4
229
- ruby18_handle_multibyte_char(result, byte, str, i, char_len, strip)
230
- i += char_len - 1
231
- else
232
- raise Puppet::DevError, "Unhandled UTF8 char length: '#{char_len}'"
233
- end
234
-
235
- end
236
-
237
- result
238
- end
239
-
240
- # @api private
241
- def self.ruby18_handle_multibyte_char(result_str, byte, str, i, char_len, strip = true)
242
- # keeping an array of bytes for now because we need to do some
243
- # bitwise math on them.
244
- char_additional_bytes = []
245
-
246
- # If we don't have enough bytes left to read the full character, we
247
- # put on a replacement character and bail.
248
- if i + (char_len - 1) > str.length
249
- result_str.concat(Utf8ReplacementChar) unless strip
250
- return
251
- end
252
-
253
- # we've already read the first byte, so we need to set up a range
254
- # from 0 to (n-2); e.g. if it's a 2-byte char, we will have a range
255
- # from 0 to 0 which will result in reading 1 more byte
256
- (0..char_len - 2).each do |x|
257
- char_additional_bytes << get_byte(str, i + x)
258
- end
259
-
260
- if (is_valid_multibyte_suffix(byte, char_additional_bytes))
261
- result_str << byte
262
- result_str.concat(char_additional_bytes.pack("c*"))
263
- else
264
- result_str.concat(Utf8ReplacementChar) unless strip
265
- end
266
- end
267
-
268
- # @api private
269
- def self.is_valid_multibyte_suffix(byte, additional_bytes)
270
- # This is heinous, but the UTF-8 spec says that codepoints greater than
271
- # 0x10FFFF are illegal. The first character that is over that limit is
272
- # 0xF490bfbf, so if the first byte is F4 then we have to check for
273
- # that condition.
274
- if byte == 0xF4
275
- val = additional_bytes.inject(0) { |result, b | (result << 8) + b}
276
- if val >= 0x90bfbf
277
- return false
278
- end
279
- end
280
- additional_bytes.all? { |b| ((b & 0xC0) == 0x80) }
281
- end
282
-
283
- # @api private
284
- def self.get_byte(str, index)
285
- # This method is a hack to allow this code to work with either ruby 1.8
286
- # or 1.9. In production this code path should never be exercised by
287
- # 1.9 because it has a much more sane way to accomplish our goal, but
288
- # for testing, it is useful to be able to run the 1.8 codepath in 1.9.
289
- if @has_get_byte
290
- str.getbyte(index)
291
- else
292
- str[index]
293
- end
294
- end
295
-
296
163
  end
297
164
  end
298
165
  end
@@ -24,13 +24,11 @@ class Puppet::Util::Puppetdb::Command
24
24
  # primitive (numeric type, string, array, or hash) that is natively supported
25
25
  # by JSON serialization / deserialization libraries.
26
26
  def initialize(command, version, certname, payload)
27
- @command = command
28
- @version = version
29
- @certname = certname
30
27
  profile("Format payload", [:puppetdb, :payload, :format]) do
31
- @payload = Puppet::Util::Puppetdb::CharEncoding.utf8_string({
28
+ @checksum_payload = Puppet::Util::Puppetdb::CharEncoding.utf8_string({
32
29
  :command => command,
33
30
  :version => version,
31
+ :certname => certname,
34
32
  :payload => payload,
35
33
  # We use to_pson still here, to work around the support for shifting
36
34
  # binary data from a catalog to PuppetDB. Attempting to use to_json
@@ -44,21 +42,25 @@ class Puppet::Util::Puppetdb::Command
44
42
  # Puppet 4.1.0. We need a better answer to non-utf8 data end-to-end.
45
43
  }.to_pson, "Error encoding a '#{command}' command for host '#{certname}'")
46
44
  end
45
+ @command = Puppet::Util::Puppetdb::CharEncoding.coerce_to_utf8(command).gsub(" ", "_")
46
+ @version = version
47
+ @certname = Puppet::Util::Puppetdb::CharEncoding.coerce_to_utf8(certname)
48
+ @payload = Puppet::Util::Puppetdb::CharEncoding.coerce_to_utf8(payload.to_pson)
47
49
  end
48
50
 
49
- attr_reader :command, :version, :certname, :payload
51
+ attr_reader :command, :version, :certname, :payload, :checksum_payload
50
52
 
51
53
  # Submit the command, returning the result hash.
52
54
  #
53
55
  # @return [Hash <String, String>]
54
56
  def submit
55
- checksum = Digest::SHA1.hexdigest(payload)
57
+ checksum = Digest::SHA1.hexdigest(checksum_payload)
56
58
 
57
59
  for_whom = " for #{certname}" if certname
58
-
60
+ params = "checksum=#{checksum}&version=#{version}&certname=#{certname}&command=#{command}"
59
61
  begin
60
62
  response = profile("Submit command HTTP post", [:puppetdb, :command, :submit]) do
61
- Http.action("#{CommandsUrl}?checksum=#{checksum}") do |http_instance, path|
63
+ Http.action("#{CommandsUrl}?#{params}", :command) do |http_instance, path|
62
64
  http_instance.post(path, payload, headers)
63
65
  end
64
66
  end
@@ -9,11 +9,14 @@ module Puppet::Util::Puppetdb
9
9
 
10
10
  def self.load(config_file = nil)
11
11
  defaults = {
12
- :server => "puppetdb",
13
- :port => 8081,
14
- :soft_write_failure => false,
15
- :server_url_timeout => 30,
12
+ :server_urls => "https://puppetdb:8081",
13
+ :soft_write_failure => false,
14
+ :server_url_timeout => 30,
16
15
  :include_unchanged_resources => false,
16
+ :min_successful_submissions => 1,
17
+ :submit_only_server_urls => "",
18
+ :command_broadcast => false,
19
+ :sticky_read_failover => false
17
20
  }
18
21
 
19
22
  config_file ||= File.join(Puppet[:confdir], "puppetdb.conf")
@@ -22,7 +25,7 @@ module Puppet::Util::Puppetdb
22
25
  Puppet.debug("Configuring PuppetDB terminuses with config file #{config_file}")
23
26
  content = File.read(config_file)
24
27
  else
25
- Puppet.debug("No #{config_file} file found; falling back to default server and port #{defaults[:server]}:#{defaults[:port]}")
28
+ Puppet.debug("No #{config_file} file found; falling back to default server_urls #{defaults[:server_urls]}")
26
29
  content = ''
27
30
  end
28
31
 
@@ -51,31 +54,53 @@ module Puppet::Util::Puppetdb
51
54
  main_section = result['main'] || {}
52
55
  # symbolize the keys
53
56
  main_section = main_section.inject({}) {|h, (k,v)| h[k.to_sym] = v ; h}
57
+
54
58
  # merge with defaults but filter out anything except the legal settings
55
59
  config_hash = defaults.merge(main_section).reject do |k, v|
56
- !([:server,
57
- :port,
60
+ !([:server_urls,
58
61
  :ignore_blacklisted_events,
59
62
  :include_unchanged_resources,
60
63
  :soft_write_failure,
61
- :server_urls,
62
- :server_url_timeout].include?(k))
64
+ :server_url_timeout,
65
+ :min_successful_submissions,
66
+ :submit_only_server_urls,
67
+ :command_broadcast,
68
+ :sticky_read_failover].include?(k))
63
69
  end
64
70
 
65
- if config_hash[:server_urls]
66
- uses_server_urls = true
67
- config_hash[:server_urls] = config_hash[:server_urls].split(",").map {|s| s.strip}
68
- else
69
- uses_server_urls = false
70
- config_hash[:server_urls] = ["https://#{config_hash[:server].strip}:#{config_hash[:port].to_s}"]
71
- end
72
- config_hash[:server_urls] = convert_and_validate_urls(config_hash[:server_urls])
71
+ parsed_urls = config_hash[:server_urls].split(",").map {|s| s.strip}
72
+ config_hash[:server_urls] = convert_and_validate_urls(parsed_urls)
73
73
 
74
74
  config_hash[:server_url_timeout] = config_hash[:server_url_timeout].to_i
75
75
  config_hash[:include_unchanged_resources] = Puppet::Util::Puppetdb.to_bool(config_hash[:include_unchanged_resources])
76
76
  config_hash[:soft_write_failure] = Puppet::Util::Puppetdb.to_bool(config_hash[:soft_write_failure])
77
77
 
78
- self.new(config_hash, uses_server_urls)
78
+ config_hash[:submit_only_server_urls] = convert_and_validate_urls(config_hash[:submit_only_server_urls].split(",").map {|s| s.strip})
79
+ config_hash[:min_successful_submissions] = config_hash[:min_successful_submissions].to_i
80
+ config_hash[:command_broadcast] = Puppet::Util::Puppetdb.to_bool(config_hash[:command_broadcast])
81
+ config_hash[:sticky_read_failover] = Puppet::Util::Puppetdb.to_bool(config_hash[:sticky_read_failover])
82
+
83
+ if config_hash[:soft_write_failure] and config_hash[:min_successful_submissions] > 1
84
+ raise "soft_write_failure cannot be enabled when min_successful_submissions is greater than 1"
85
+ end
86
+
87
+ overlapping_server_urls = config_hash[:server_urls] & config_hash[:submit_only_server_urls]
88
+ if overlapping_server_urls.length > 0
89
+ overlapping_server_urls_strs = overlapping_server_urls.map { |u| u.to_s }
90
+ raise "Server URLs must be in either server_urls or submit_only_server_urls, not both. "\
91
+ "(#{overlapping_server_urls_strs.to_s} are in both)"
92
+ end
93
+
94
+ if config_hash[:min_successful_submissions] > 1 and not config_hash[:command_broadcast]
95
+ raise "command_broadcast must be set to true to use min_successful_submissions"
96
+ end
97
+
98
+ if config_hash[:min_successful_submissions] > config_hash[:server_urls].length
99
+ raise "min_successful_submissions (#{config_hash[:min_successful_submissions]}) must be less than "\
100
+ "or equal to the number of server_urls (#{config_hash[:server_urls].length})"
101
+ end
102
+
103
+ self.new(config_hash)
79
104
  rescue => detail
80
105
  Puppet.warning "Could not configure PuppetDB terminuses: #{detail}"
81
106
  Puppet.warning detail.backtrace if Puppet[:trace]
@@ -84,21 +109,8 @@ module Puppet::Util::Puppetdb
84
109
 
85
110
  # @!group Public instance methods
86
111
 
87
- def initialize(config_hash = {}, uses_server_urls=nil)
112
+ def initialize(config_hash = {})
88
113
  @config = config_hash
89
- if !uses_server_urls
90
- Puppet.warning("Specification of server and port in puppetdb.conf is deprecated. Use the setting server_urls.")
91
- end
92
- # To provide accurate error messages to users about HTTP failures, we
93
- # need to know whether they initially defined their config via the old
94
- # server/port combo or the new server_urls. This boolean keeps track
95
- # of how the user defined that config so that we can give them a
96
- # better error message
97
- @server_url_config = uses_server_urls
98
- end
99
-
100
- def server_url_config?
101
- @server_url_config
102
114
  end
103
115
 
104
116
  def server_urls
@@ -117,6 +129,22 @@ module Puppet::Util::Puppetdb
117
129
  config[:soft_write_failure]
118
130
  end
119
131
 
132
+ def min_successful_submissions
133
+ config[:min_successful_submissions]
134
+ end
135
+
136
+ def submit_only_server_urls
137
+ config[:submit_only_server_urls]
138
+ end
139
+
140
+ def command_broadcast
141
+ config[:command_broadcast]
142
+ end
143
+
144
+ def sticky_read_failover
145
+ config[:sticky_read_failover]
146
+ end
147
+
120
148
  # @!group Private instance methods
121
149
 
122
150
  # @!attribute [r] count
@@ -3,14 +3,17 @@ require 'puppet/network/http_pool'
3
3
  require 'net/http'
4
4
  require 'timeout'
5
5
  require 'pp'
6
+ require 'thread'
7
+ require 'puppet/util/puppetdb/atom'
6
8
 
7
9
  module Puppet::Util::Puppetdb
8
10
  class Http
11
+ SERVER_URL_FAIL_MSG = "Failing over to the next PuppetDB server_url in the 'server_urls' list"
9
12
 
10
- SERVER_URL_FAIL_MSG = "Failing over to the next PuppetDB url in the 'server_urls' list"
13
+ @@last_good_query_server_url_index = Atom.new(0)
11
14
 
12
- # Concat two url snippets, taking into account a trailing/leading slash to
13
- # ensure a correct url is constructed
15
+ # Concat two server_url snippets, taking into account a trailing/leading slash to
16
+ # ensure a correct server_url is constructed
14
17
  #
15
18
  # @param snippet1 [String] first URL snippet
16
19
  # @param snippet2 [String] second URL snippet
@@ -26,94 +29,199 @@ module Puppet::Util::Puppetdb
26
29
  end
27
30
  end
28
31
 
29
- # Setup an http connection, provide a block that will do something with that http
30
- # connection. The block should be a two argument block, accepting the connection (which
31
- # you can call get or post on for example) and the properly constructed path, which
32
- # will be the concatenated version of any url_prefix and the path passed in.
33
- #
34
- # @param path_suffix [String] path for the get/post of the http action
35
- # @param http_callback [Proc] proc containing the code calling the action on the http connection
36
- # @return [Response] returns http response
37
- def self.action(path_suffix, &http_callback)
32
+ # Run the given block (cb) in a begin/rescue, catching common network
33
+ # exceptions and logging useful information about them. If an expected
34
+ # exception was caught, it's returned. An unexpected exception will be
35
+ # re-thrown. Returns nil on success.
36
+ def self.with_http_error_logging(server_url, route, &cb)
37
+ config = Puppet::Util::Puppetdb.config
38
38
 
39
+ begin
40
+ cb.call()
41
+ rescue Timeout::Error => e
42
+ Puppet.warning("Request to #{server_url.host} on #{server_url.port} at route #{route} timed out " \
43
+ "after #{config.server_url_timeout} seconds. #{SERVER_URL_FAIL_MSG}")
44
+ return e
45
+
46
+ rescue SocketError, OpenSSL::SSL::SSLError, SystemCallError, Net::ProtocolError, IOError, Net::HTTPNotFound => e
47
+ Puppet.warning("Error connecting to #{server_url.host} on #{server_url.port} at route #{route}, " \
48
+ "error message received was '#{e.message}'. #{SERVER_URL_FAIL_MSG}")
49
+ return e
50
+
51
+ rescue Puppet::Util::Puppetdb::InventorySearchError => e
52
+ Puppet.warning("Could not perform inventory search from PuppetDB at #{server_url.host}:#{server_url.port}: " \
53
+ "'#{e.message}' #{SERVER_URL_FAIL_MSG}")
54
+ return e
55
+
56
+ rescue Puppet::Util::Puppetdb::CommandSubmissionError => e
57
+ error = "Failed to submit '#{e.context[:command]}' command for '#{e.context[:for_whom]}' to PuppetDB " \
58
+ "at #{server_url.host}:#{server_url.port}: '#{e.message}'."
59
+ if config.soft_write_failure
60
+ Puppet.err error
61
+ else
62
+ Puppet.warning(error + " #{SERVER_URL_FAIL_MSG}")
63
+ end
64
+ return e
65
+
66
+ rescue Puppet::Util::Puppetdb::SoftWriteFailError => e
67
+ Puppet.warning("Failed to submit '#{e.context[:command]}' command for '#{e.context[:for_whom]}' to PuppetDB " \
68
+ "at #{server_url.host}:#{server_url.port}: '#{e.message}' #{SERVER_URL_FAIL_MSG}")
69
+ return e
70
+
71
+ rescue Puppet::Error => e
72
+ if e.message =~ /did not match server certificate; expected one of/
73
+ Puppet.warning("Error connecting to #{server_url.host} on #{server_url.port} at route #{route}, " \
74
+ "error message received was '#{e.message}'. #{SERVER_URL_FAIL_MSG}")
75
+ return e
76
+ else
77
+ raise
78
+ end
79
+ end
80
+
81
+ nil
82
+ end
83
+
84
+ # Check an http reponse from puppetdb; log a useful message if it looks like
85
+ # something went wrong. Return a symbol indicating the problem
86
+ # (:server_error, :notfound, or :other_404), or nil if there wasn't one.
87
+ def self.check_http_response(response, server_url, route)
88
+ if response.is_a? Net::HTTPServerError
89
+ Puppet.warning("Error connecting to #{server_url.host} on #{server_url.port} at route #{route}, " \
90
+ "error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG}")
91
+ :server_error
92
+ elsif response.is_a? Net::HTTPNotFound
93
+ if response.body && response.body.chars.first == "{"
94
+ # If it appears to be json, we've probably gotten an authentic 'not found' message.
95
+ Puppet.debug("HTTP 404 (probably normal) when connecting to #{server_url.host} on #{server_url.port} " \
96
+ "at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG}")
97
+ :notfound
98
+ else
99
+ # But we can also get 404s when conneting to a puppetdb that's still starting or due to misconfiguration.
100
+ Puppet.warning("Error connecting to #{server_url.host} on #{server_url.port} at route #{route}, " \
101
+ "error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG}")
102
+ :other_404
103
+ end
104
+ else
105
+ nil
106
+ end
107
+ end
108
+
109
+ def self.raise_request_error(response, response_error, path_suffix)
110
+ server_url_strings = Puppet::Util::Puppetdb.config.server_urls.map {|server_url| server_url.to_s}.join(', ')
111
+ if response_error == :notfound
112
+ raise NotFoundError, "Failed to find '#{path_suffix}' on any of the following 'server_urls': #{server_url_strings}"
113
+ else
114
+ min_successful_submissions = Puppet::Util::Puppetdb.config.min_successful_submissions
115
+ raise Puppet::Error, "Failed to execute '#{path_suffix}' on at least #{min_successful_submissions} of the following 'server_urls': #{server_url_strings}"
116
+ end
117
+ end
118
+
119
+ def self.failover_action(path_suffix, server_urls, sticky, http_callback)
39
120
  response = nil
121
+ response_error = nil
40
122
  config = Puppet::Util::Puppetdb.config
41
- server_url_config = config.server_url_config?
123
+ last_good_index = 0
124
+
125
+ if sticky
126
+ last_good_index = @@last_good_query_server_url_index.deref()
127
+ end
128
+
129
+ server_count = server_urls.length
130
+ server_try_order = (0...server_count).map { |i| (i + last_good_index) % server_count }
42
131
 
43
- for url in Puppet::Util::Puppetdb.config.server_urls
44
- begin
45
- route = concat_url_snippets(url.request_uri, path_suffix)
46
- http = Puppet::Network::HttpPool.http_instance(url.host, url.port)
47
- request_timeout = config.server_url_timeout
132
+ for server_url_index in server_try_order
133
+ server_url = server_urls[server_url_index]
134
+ route = concat_url_snippets(server_url.request_uri, path_suffix)
48
135
 
49
- response = timeout(request_timeout) do
136
+ request_exception = with_http_error_logging(server_url, route) {
137
+ http = Puppet::Network::HttpPool.http_instance(server_url.host, server_url.port)
138
+
139
+ response = Timeout.timeout(config.server_url_timeout) do
50
140
  http_callback.call(http, route)
51
141
  end
142
+ }
52
143
 
53
- if response.is_a? Net::HTTPServerError
54
- Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
55
- response = nil
56
- elsif response.is_a? Net::HTTPNotFound
57
- if response.body && response.body.chars.first == "{"
58
- # If it appears to be json, we've probably gotten an authentic 'not found' message.
59
- Puppet.debug("HTTP 404 (probably normal) when connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
60
- response = :notfound
61
- else
62
- # But we can also get 404s when conneting to a puppetdb that's still starting or due to misconfiguration.
63
- Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
64
- response = nil
144
+ if request_exception.nil?
145
+ response_error = check_http_response(response, server_url, route)
146
+ if response_error.nil?
147
+ if server_url_index != server_try_order.first()
148
+ @@last_good_query_server_url_index.reset(server_url_index)
65
149
  end
66
- else
67
150
  break
68
151
  end
69
- rescue Timeout::Error => e
70
- Puppet.warning("Request to #{url.host} on #{url.port} at route #{route} timed out after #{request_timeout} seconds. #{SERVER_URL_FAIL_MSG if server_url_config}")
152
+ end
153
+ end
154
+
155
+ if response.nil? or not(response_error.nil?)
156
+ raise_request_error(response, response_error, path_suffix)
157
+ end
158
+
159
+ response
160
+ end
161
+
162
+ def self.broadcast_action(path_suffix, server_urls, http_callback)
163
+ response = nil
164
+ response_error = nil
165
+ config = Puppet::Util::Puppetdb.config
166
+ successful_submit_count = 0
71
167
 
72
- rescue SocketError, OpenSSL::SSL::SSLError, SystemCallError, Net::ProtocolError, IOError, Net::HTTPNotFound => e
73
- Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{e.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
168
+ for server_url in server_urls
169
+ route = concat_url_snippets(server_url.request_uri, path_suffix)
74
170
 
75
- rescue Puppet::Util::Puppetdb::InventorySearchError => e
76
- Puppet.warning("Could not perform inventory search from PuppetDB at #{url.host}:#{url.port}: '#{e.message}' #{SERVER_URL_FAIL_MSG if server_url_config}")
171
+ request_exception = with_http_error_logging(server_url, route) {
172
+ http = Puppet::Network::HttpPool.http_instance(server_url.host, server_url.port)
77
173
 
78
- rescue Puppet::Util::Puppetdb::CommandSubmissionError => e
79
- error = "Failed to submit '#{e.context[:command]}' command for '#{e.context[:for_whom]}' to PuppetDB at #{url.host}:#{url.port}: '#{e.message}'."
80
- if config.soft_write_failure
81
- Puppet.err error
82
- else
83
- Puppet.warning(error + " #{SERVER_URL_FAIL_MSG if server_url_config}")
174
+ response = Timeout.timeout(config.server_url_timeout) do
175
+ http_callback.call(http, route)
84
176
  end
85
- rescue Puppet::Util::Puppetdb::SoftWriteFailError => e
86
- Puppet.warning("Failed to submit '#{e.context[:command]}' command for '#{e.context[:for_whom]}' to PuppetDB at #{url.host}:#{url.port}: '#{e.message}' #{SERVER_URL_FAIL_MSG if server_url_config}")
87
- rescue Puppet::Error => e
88
- if e.message =~ /did not match server certificate; expected one of/
89
- Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{e.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
90
- else
91
- raise
177
+ }
178
+
179
+ if request_exception.nil?
180
+ response_error = check_http_response(response, server_url, route)
181
+ if response_error.nil?
182
+ successful_submit_count += 1
92
183
  end
93
184
  end
94
185
  end
95
186
 
96
- if response.nil? or response == :notfound
97
- if server_url_config
98
- server_url_strings = Puppet::Util::Puppetdb.config.server_urls.map {|url| url.to_s}.join(', ')
99
- if response == :notfound
100
- raise NotFoundError, "Failed to find '#{path_suffix}' on any of the following 'server_urls': #{server_url_strings}"
101
- else
102
- raise Puppet::Error, "Failed to execute '#{path_suffix}' on any of the following 'server_urls': #{server_url_strings}"
103
- end
104
- else
105
- uri = Puppet::Util::Puppetdb.config.server_urls.first
106
- if response == :notfound
107
- raise NotFoundError, "Failed to find '#{path_suffix}' on server: '#{uri.host}' and port: '#{uri.port}'"
108
- else
109
- raise Puppet::Error, "Failed to execute '#{path_suffix}' on server: '#{uri.host}' and port: '#{uri.port}'"
110
- end
111
- end
187
+ if successful_submit_count < config.min_successful_submissions
188
+ raise_request_error(response, response_error, path_suffix)
112
189
  end
113
190
 
114
191
  response
192
+ end
115
193
 
194
+ # Setup an http connection, provide a block that will do something with that http
195
+ # connection. The block should be a two argument block, accepting the connection (which
196
+ # you can call get or post on for example) and the properly constructed path, which
197
+ # will be the concatenated version of any url_prefix and the path passed in.
198
+ #
199
+ # @param path_suffix [String] path for the get/post of the http action
200
+ # @param request_type [Symbol] :query or :command
201
+ # @param http_callback [Proc] proc containing the code calling the action on the http connection
202
+ # @return [Response] returns http response
203
+ def self.action(path_suffix, request_mode, &http_callback)
204
+ config = Puppet::Util::Puppetdb.config
205
+
206
+ case request_mode
207
+ when :query
208
+ self.failover_action(path_suffix, config.server_urls, config.sticky_read_failover, http_callback)
209
+ when :command
210
+ submit_server_urls = config.server_urls + config.submit_only_server_urls
211
+ if config.command_broadcast
212
+ self.broadcast_action(path_suffix, submit_server_urls, http_callback)
213
+ else
214
+ self.failover_action(path_suffix, submit_server_urls, false, http_callback)
215
+ end
216
+ else
217
+ raise Puppet::Error, "Unknown request mode: #{request_mode}"
218
+ end
219
+ end
220
+
221
+ def self.reset_query_failover()
222
+ @@last_good_query_server_url_index.reset(0)
116
223
  end
224
+
117
225
  end
118
226
 
119
227
  class NotFoundError < Puppet::Error
@@ -1,6 +1,6 @@
1
1
  module PuppetDB
2
2
  module Terminus
3
- VERSION = "3.2.4"
3
+ VERSION = "4.0.0"
4
4
  UPSTREAM_VERSION = VERSION.split(".")[0..2].join(".")
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quixoten-puppetdb-terminus
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.4
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Devin Christensen
@@ -50,10 +50,9 @@ files:
50
50
  - LICENSE.txt
51
51
  - README.md
52
52
  - Rakefile
53
- - lib/puppet/application/storeconfigs.rb
54
53
  - lib/puppet/face/node/deactivate.rb
55
54
  - lib/puppet/face/node/status.rb
56
- - lib/puppet/face/storeconfigs.rb
55
+ - lib/puppet/functions/puppetdb_query.rb
57
56
  - lib/puppet/indirector/catalog/puppetdb.rb
58
57
  - lib/puppet/indirector/facts/puppetdb.rb
59
58
  - lib/puppet/indirector/facts/puppetdb_apply.rb
@@ -61,6 +60,7 @@ files:
61
60
  - lib/puppet/indirector/resource/puppetdb.rb
62
61
  - lib/puppet/reports/puppetdb.rb
63
62
  - lib/puppet/util/puppetdb.rb
63
+ - lib/puppet/util/puppetdb/atom.rb
64
64
  - lib/puppet/util/puppetdb/char_encoding.rb
65
65
  - lib/puppet/util/puppetdb/command.rb
66
66
  - lib/puppet/util/puppetdb/command_names.rb
@@ -1,4 +0,0 @@
1
- require 'puppet/application/face_base'
2
-
3
- class Puppet::Application::Storeconfigs < Puppet::Application::FaceBase
4
- end
@@ -1,193 +0,0 @@
1
- require 'puppet/util/puppetdb'
2
- require 'puppet/face'
3
-
4
- if Puppet::Util::Puppetdb.puppet3compat?
5
- require 'tmpdir'
6
-
7
- Puppet::Face.define(:storeconfigs, '0.0.1') do
8
- copyright "Puppet Labs", 2011
9
- license "Apache 2 license"
10
-
11
- summary "Interact with the storeconfigs database"
12
- description <<-DESC
13
- This subcommand interacts with the ActiveRecord storeconfigs database, and
14
- can be used to export a dump of that data which is suitable for import by
15
- PuppetDB.
16
- DESC
17
-
18
- action :export do
19
- summary "Export the storeconfigs database"
20
- description <<-DESC
21
- Generate a dump of all catalogs from the storeconfigs database, as a
22
- tarball which can be imported by PuppetDB. Only exported resources are
23
- included; non-exported resources, edges, facts, or other data are
24
- omitted. Returns the location of the output.
25
- DESC
26
-
27
- when_invoked do |options|
28
-
29
- require 'puppet/rails'
30
-
31
- tmpdir = Dir.mktmpdir
32
- workdir = File.join(tmpdir, 'puppetdb-bak')
33
- Dir.mkdir(workdir)
34
-
35
- begin
36
- Puppet::Rails.connect
37
-
38
- timestamp = Time.now
39
-
40
- # Fetch all nodes, including exported resources and their params
41
- nodes = Puppet::Rails::Host.all(:include => {:resources => [:param_values, :puppet_tags]},
42
- :conditions => {:resources => {:exported => true}})
43
-
44
- catalogs = nodes.map { |node| node_to_catalog_hash(node, timestamp.iso8601(5)) }
45
-
46
- catalog_dir = File.join(workdir, 'catalogs')
47
- FileUtils.mkdir(catalog_dir)
48
-
49
- catalogs.each do |catalog|
50
- filename = File.join(catalog_dir, "#{catalog[:certname]}.json")
51
-
52
- File.open(filename, 'w') do |file|
53
- file.puts catalog.to_json
54
- end
55
- end
56
-
57
- node_names = nodes.map(&:name).sort
58
-
59
- File.open(File.join(workdir, 'export-metadata.json'), 'w') do |file|
60
- metadata = {
61
- 'timestamp' => timestamp,
62
- 'command_versions' => {
63
- 'replace_catalog' => 6,
64
- }
65
- }
66
-
67
- file.puts metadata.to_json
68
- end
69
-
70
- tarfile = destination_file(timestamp)
71
-
72
- if tar = Puppet::Util.which('tar')
73
- execute("cd #{tmpdir} && #{tar} -cf #{tarfile} puppetdb-bak")
74
-
75
- FileUtils.rm_rf(workdir)
76
-
77
- if gzip = Puppet::Util.which('gzip')
78
- execute("#{gzip} #{tarfile}")
79
- "#{tarfile}.gz"
80
- else
81
- Puppet.warning "Can't find the `gzip` command to compress the tarball; output will not be compressed"
82
- tarfile
83
- end
84
- else
85
- Puppet.warning "Can't find the `tar` command to produce a tarball; output will remain in the temporary working directory"
86
- workdir
87
- end
88
- rescue => e
89
- # Clean up if something goes wrong. We don't want to ensure this,
90
- # because we want the directory to stick around in the case where they
91
- # don't have tar.
92
- FileUtils.rm_rf(workdir)
93
- raise
94
- end
95
- end
96
-
97
- when_rendering :console do |filename|
98
- "Exported storeconfigs data to #{filename}"
99
- end
100
- end
101
-
102
- # Returns the location to leave the output. This is really only here for testing. :/
103
- def destination_file(timestamp)
104
- File.expand_path("storeconfigs-#{timestamp.strftime('%Y%m%d%H%M%S')}.tar")
105
- end
106
-
107
- # Execute a command using Puppet's execution static method.
108
- #
109
- # @param command [Array<String>, String] the command to execute. If it is
110
- # an Array the first element should be the executable and the rest of the
111
- # elements should be the individual arguments to that executable.
112
- # @return [Puppet::Util::Execution::ProcessOutput] output as specified by options
113
- # @raise [Puppet::ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is
114
- # `true`.
115
- def execute(command)
116
- Puppet::Util::Execution.execute(command)
117
- end
118
-
119
- def node_to_catalog_hash(node, timestamp)
120
- resources = node.resources.map { |resource| resource_to_hash(resource) }
121
- edges = node.resources.map { |resource| resource_to_edge_hash(resource) }
122
-
123
- {
124
- :environment => "production",
125
- :metadata => {
126
- :api_version => 1,
127
- },
128
- :certname => node.name,
129
- :version => node.last_compile || Time.now,
130
- :edges => edges,
131
- :resources => resources + [stage_main_hash],
132
- :timestamp => timestamp,
133
- :producer_timestamp => timestamp,
134
- }
135
- end
136
-
137
- def resource_to_hash(resource)
138
- parameters = resource.param_values.inject({}) do |params,param_value|
139
- if params.has_key?(param_value.param_name.name)
140
- value = [params[param_value.param_name.name],param_value.value].flatten
141
- else
142
- value = param_value.value
143
- end
144
- params.merge(param_value.param_name.name => value)
145
- end
146
-
147
- tags = resource.puppet_tags.map(&:name).uniq.sort
148
-
149
- hash = {
150
- :type => resource.restype,
151
- :title => resource.title,
152
- :exported => true,
153
- :parameters => parameters,
154
- :tags => tags,
155
- }
156
-
157
- hash[:file] = resource.file if resource.file
158
- hash[:line] = resource.line if resource.line
159
-
160
- hash
161
- end
162
-
163
- # The catalog *must* have edges, so everything is contained by Stage[main]!
164
- def resource_to_edge_hash(resource)
165
- {
166
- 'source' => {'type' => 'Stage', 'title' => 'main'},
167
- 'target' => {'type' => resource.restype, 'title' => resource.title},
168
- 'relationship' => 'contains',
169
- }
170
- end
171
-
172
- def stage_main_hash
173
- {
174
- :type => 'Stage',
175
- :title => 'main',
176
- :exported => false,
177
- :parameters => {},
178
- :tags => ['stage', 'main'],
179
- }
180
- end
181
- end
182
- else
183
- Puppet::Face.define(:storeconfigs, '0.0.1') do
184
- copyright "Puppet Labs", 2011
185
- license "Apache 2 license"
186
-
187
- summary "storeconfigs is not supported on Puppet 4.0.0+"
188
- description <<-DESC
189
- Users needing this feature should migrate using Puppet 3.7.2 or a more recent
190
- 3.7 release.
191
- DESC
192
- end
193
- end