quixoten-puppetdb-terminus 3.2.4 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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