md-puppetdb-terminus 2.0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ module Puppet::Util::Puppetdb
2
+ class Blacklist
3
+
4
+ BlacklistedEvent = Struct.new(:resource_type, :resource_title, :status, :property)
5
+
6
+ # Initialize our blacklist of events to filter out of reports. This is needed
7
+ # because older versions of puppet always generate a swath of (meaningless)
8
+ # 'skipped' Schedule events on every agent run. As of puppet 3.3, these
9
+ # events should no longer be generated, but this is here for backward compat.
10
+ BlacklistedEvents =
11
+ [BlacklistedEvent.new("Schedule", "never", "skipped", nil),
12
+ BlacklistedEvent.new("Schedule", "puppet", "skipped", nil),
13
+ BlacklistedEvent.new("Schedule", "hourly", "skipped", nil),
14
+ BlacklistedEvent.new("Schedule", "daily", "skipped", nil),
15
+ BlacklistedEvent.new("Schedule", "weekly", "skipped", nil),
16
+ BlacklistedEvent.new("Schedule", "monthly", "skipped", nil)]
17
+
18
+ def initialize(events)
19
+ @events = events.inject({}) do |m, e|
20
+ m[e.resource_type] ||= {}
21
+ m[e.resource_type][e.resource_title] ||= {}
22
+ m[e.resource_type][e.resource_title][e.status] ||= {}
23
+ m[e.resource_type][e.resource_title][e.status][e.property] = true
24
+ m
25
+ end
26
+ end
27
+
28
+ def is_event_blacklisted?(event)
29
+ @events.fetch(event["resource-type"], {}).
30
+ fetch(event["resource-title"], {}).
31
+ fetch(event["status"], {}).
32
+ fetch(event["property"], false)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,212 @@
1
+ require 'puppet'
2
+
3
+ module Puppet
4
+ module Util
5
+ module Puppetdb
6
+ module CharEncoding
7
+
8
+
9
+ # Some of this code is modeled after:
10
+ # https://github.com/brianmario/utf8/blob/ef10c033/ext/utf8/utf8proc.c
11
+ # https://github.com/brianmario/utf8/blob/ef10c033/ext/utf8/string_utf8.c
12
+
13
+ Utf8CharLens = [
14
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
15
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
16
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
17
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
18
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
19
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
20
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
21
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
22
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
23
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
24
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
25
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
26
+ 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
27
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
28
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
29
+ 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
30
+ ]
31
+
32
+ Utf8ReplacementChar = [ 0xEF, 0xBF, 0xBD ].pack("c*")
33
+
34
+
35
+ def self.utf8_string(str)
36
+ if RUBY_VERSION =~ /1.8/
37
+ # Ruby 1.8 doesn't have String#encode and related methods, and there
38
+ # appears to be a bug in iconv that will interpret some byte sequences
39
+ # as 6-byte characters. Thus, we are forced to resort to some unfortunate
40
+ # manual chicanery.
41
+ warn_if_changed(str, ruby18_clean_utf8(str))
42
+ elsif str.encoding == Encoding::UTF_8
43
+ # If we get here, we're in ruby 1.9+, so we have the string encoding methods
44
+ # available. However, just because a ruby String object is already
45
+ # marked as UTF-8, that doesn't guarantee that its contents are actually
46
+ # valid; and if you call ruby's ".encode" method with an encoding of
47
+ # "utf-8" for a String that ruby already believes is UTF-8, ruby
48
+ # seems to optimize that to be a no-op. So, we have to do some more
49
+ # complex handling...
50
+
51
+ # If the string already has valid encoding then we're fine.
52
+ return str if str.valid_encoding?
53
+
54
+ # If not, we basically have to walk over the characters and replace
55
+ # them by hand.
56
+ warn_if_changed(str, str.each_char.map { |c| c.valid_encoding? ? c : "\ufffd"}.join)
57
+ else
58
+ # if we get here, we're ruby 1.9 and the current string is *not* encoded
59
+ # as UTF-8. Thus we can actually rely on ruby's "encode" method.
60
+ begin
61
+ str.encode('UTF-8')
62
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
63
+ # If we got an exception, the string is either invalid or not
64
+ # convertible to UTF-8, so drop those bytes.
65
+ warn_if_changed(str, str.encode('UTF-8', :invalid => :replace, :undef => :replace))
66
+ end
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def self.warn_if_changed(str, converted_str)
72
+ if converted_str != str
73
+ Puppet.warning "Ignoring invalid UTF-8 byte sequences in data to be sent to PuppetDB"
74
+ end
75
+ converted_str
76
+ end
77
+
78
+ # @api private
79
+ def self.ruby18_clean_utf8(str)
80
+ #iconv_to_utf8(str)
81
+ #ruby18_manually_clean_utf8(str)
82
+
83
+ # So, we've tried doing this UTF8 cleaning for ruby 1.8 a few different
84
+ # ways. Doing it via IConv, we don't do a good job of handling characters
85
+ # whose codepoints would exceed the legal maximum for UTF-8. Doing it via
86
+ # our manual scrubbing process is slower and doesn't catch overlong
87
+ # encodings. Since this code really shouldn't even exist in the first place
88
+ # we've decided to simply compose the two scrubbing methods for now, rather
89
+ # than trying to add detection of overlong encodings. It'd be a non-trivial
90
+ # chunk of code, and it'd have to do a lot of bitwise arithmetic (which Ruby
91
+ # is not blazingly fast at).
92
+ ruby18_manually_clean_utf8(iconv_to_utf8(str))
93
+ end
94
+
95
+
96
+ # @todo we're not using this anymore, but I wanted to leave it around
97
+ # for a little while just to make sure that the new code pans out.
98
+ # @api private
99
+ def self.iconv_to_utf8(str)
100
+ iconv = Iconv.new('UTF-8//IGNORE', 'UTF-8')
101
+
102
+ # http://po-ru.com/diary/fixing-invalid-utf-8-in-ruby-revisited/
103
+ iconv.iconv(str + " ")[0..-2]
104
+ end
105
+
106
+ # @api private
107
+ def self.get_char_len(byte)
108
+ Utf8CharLens[byte]
109
+ end
110
+
111
+ # Manually cleans a string by stripping any byte sequences that are
112
+ # not valid UTF-8 characters. If you'd prefer for the invalid bytes to be
113
+ # replaced with the unicode replacement character rather than being stripped,
114
+ # you may pass `false` for the optional second parameter (`strip`, which
115
+ # defaults to `true`).
116
+ #
117
+ # @api private
118
+ def self.ruby18_manually_clean_utf8(str, strip = true)
119
+
120
+ # This is a hack to allow this code to work with either ruby 1.8 or 1.9,
121
+ # which is useful for debugging and benchmarking. For more info see the
122
+ # comments in the #get_byte method below.
123
+ @has_get_byte = str.respond_to?(:getbyte)
124
+
125
+
126
+ i = 0
127
+ len = str.length
128
+ result = ""
129
+
130
+ while i < len
131
+ byte = get_byte(str, i)
132
+
133
+ i += 1
134
+
135
+ char_len = get_char_len(byte)
136
+ case char_len
137
+ when 0
138
+ result.concat(Utf8ReplacementChar) unless strip
139
+ when 1
140
+ result << byte
141
+ when 2..4
142
+ ruby18_handle_multibyte_char(result, byte, str, i, char_len, strip)
143
+ i += char_len - 1
144
+ else
145
+ raise Puppet::DevError, "Unhandled UTF8 char length: '#{char_len}'"
146
+ end
147
+
148
+ end
149
+
150
+ result
151
+ end
152
+
153
+ # @api private
154
+ def self.ruby18_handle_multibyte_char(result_str, byte, str, i, char_len, strip = true)
155
+ # keeping an array of bytes for now because we need to do some
156
+ # bitwise math on them.
157
+ char_additional_bytes = []
158
+
159
+ # If we don't have enough bytes left to read the full character, we
160
+ # put on a replacement character and bail.
161
+ if i + (char_len - 1) > str.length
162
+ result_str.concat(Utf8ReplacementChar) unless strip
163
+ return
164
+ end
165
+
166
+ # we've already read the first byte, so we need to set up a range
167
+ # from 0 to (n-2); e.g. if it's a 2-byte char, we will have a range
168
+ # from 0 to 0 which will result in reading 1 more byte
169
+ (0..char_len - 2).each do |x|
170
+ char_additional_bytes << get_byte(str, i + x)
171
+ end
172
+
173
+ if (is_valid_multibyte_suffix(byte, char_additional_bytes))
174
+ result_str << byte
175
+ result_str.concat(char_additional_bytes.pack("c*"))
176
+ else
177
+ result_str.concat(Utf8ReplacementChar) unless strip
178
+ end
179
+ end
180
+
181
+ # @api private
182
+ def self.is_valid_multibyte_suffix(byte, additional_bytes)
183
+ # This is heinous, but the UTF-8 spec says that codepoints greater than
184
+ # 0x10FFFF are illegal. The first character that is over that limit is
185
+ # 0xF490bfbf, so if the first byte is F4 then we have to check for
186
+ # that condition.
187
+ if byte == 0xF4
188
+ val = additional_bytes.inject(0) { |result, b | (result << 8) + b}
189
+ if val >= 0x90bfbf
190
+ return false
191
+ end
192
+ end
193
+ additional_bytes.all? { |b| ((b & 0xC0) == 0x80) }
194
+ end
195
+
196
+ # @api private
197
+ def self.get_byte(str, index)
198
+ # This method is a hack to allow this code to work with either ruby 1.8
199
+ # or 1.9. In production this code path should never be exercised by
200
+ # 1.9 because it has a much more sane way to accomplish our goal, but
201
+ # for testing, it is useful to be able to run the 1.8 codepath in 1.9.
202
+ if @has_get_byte
203
+ str.getbyte(index)
204
+ else
205
+ str[index]
206
+ end
207
+ end
208
+
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,113 @@
1
+ require 'puppet/error'
2
+ require 'puppet/network/http_pool'
3
+ require 'puppet/util/puppetdb'
4
+ require 'puppet/util/puppetdb/command_names'
5
+ require 'puppet/util/puppetdb/char_encoding'
6
+ require 'json'
7
+
8
+ class Puppet::Util::Puppetdb::Command
9
+ include Puppet::Util::Puppetdb
10
+ include Puppet::Util::Puppetdb::CommandNames
11
+
12
+ Url = "/v3/commands"
13
+
14
+ # Public instance methods
15
+
16
+ # Initialize a Command object, for later submission.
17
+ #
18
+ # @param command String the name of the command; should be one of the
19
+ # constants defined in `Puppet::Util::Puppetdb::CommandNames`
20
+ # @param version Integer the command version number
21
+ # @param certname The certname that this command operates on (is not
22
+ # included in the actual submission)
23
+ # @param payload Object the payload of the command. This object should be a
24
+ # primitive (numeric type, string, array, or hash) that is natively supported
25
+ # by JSON serialization / deserialization libraries.
26
+ def initialize(command, version, certname, payload)
27
+ @command = command
28
+ @version = version
29
+ @certname = certname
30
+ profile "Format payload" do
31
+ @payload = self.class.format_payload(command, version, payload)
32
+ end
33
+ end
34
+
35
+ attr_reader :command, :version, :certname, :payload
36
+
37
+ # Submit the command, returning the result hash.
38
+ #
39
+ # @return [Hash <String, String>]
40
+ def submit
41
+ checksum = Digest::SHA1.hexdigest(payload)
42
+
43
+ for_whom = " for #{certname}" if certname
44
+
45
+ begin
46
+ response = profile "Submit command HTTP post" do
47
+ http = Puppet::Network::HttpPool.http_instance(config.server, config.port)
48
+ http.post(Url + "?checksum=#{checksum}", payload, headers)
49
+ end
50
+
51
+ Puppet::Util::Puppetdb.log_x_deprecation_header(response)
52
+
53
+ if response.is_a? Net::HTTPSuccess
54
+ result = JSON.parse(response.body)
55
+ Puppet.info "'#{command}' command#{for_whom} submitted to PuppetDB with UUID #{result['uuid']}"
56
+ result
57
+ else
58
+ # Newline characters cause an HTTP error, so strip them
59
+ error = "[#{response.code} #{response.message}] #{response.body.gsub(/[\r\n]/, '')}"
60
+ if config.soft_write_failure
61
+ Puppet.err "'#{command}'command#{for_whom} failed during submission to PuppetDB: #{error}"
62
+ else
63
+ raise Puppet::Error, error
64
+ end
65
+ end
66
+ rescue => e
67
+ error = "Failed to submit '#{command}' command#{for_whom} to PuppetDB at #{config.server}:#{config.port}: #{e}"
68
+ if config.soft_write_failure
69
+ Puppet.err error
70
+ else
71
+ # TODO: Use new exception handling methods from Puppet 3.0 here as soon as
72
+ # we are able to do so (can't call them yet w/o breaking backwards
73
+ # compatibility.) We should either be using a nested exception or calling
74
+ # Puppet::Util::Logging#log_exception or #log_and_raise here; w/o them
75
+ # we lose context as to where the original exception occurred.
76
+ puts e, e.backtrace if Puppet[:trace]
77
+ raise Puppet::Error, error
78
+ end
79
+ end
80
+ end
81
+
82
+
83
+ # @!group Private class methods
84
+
85
+ # @api private
86
+ def self.format_payload(command, version, payload)
87
+ message = {
88
+ :command => command,
89
+ :version => version,
90
+ :payload => payload,
91
+ }.to_pson
92
+
93
+ Puppet::Util::Puppetdb::CharEncoding.utf8_string(message)
94
+ end
95
+
96
+ # @!group Private instance methods
97
+
98
+ # @api private
99
+ def headers
100
+ {
101
+ "Accept" => "application/json",
102
+ "Content-Type" => "application/json",
103
+ }
104
+ end
105
+
106
+ # @api private
107
+ def config
108
+ # Would prefer to pass this to the constructor or acquire it some other
109
+ # way besides this pseudo-global reference.
110
+ Puppet::Util::Puppetdb.config
111
+ end
112
+
113
+ end
@@ -0,0 +1,8 @@
1
+ module Puppet::Util::Puppetdb
2
+ module CommandNames
3
+ CommandReplaceCatalog = "replace catalog"
4
+ CommandReplaceFacts = "replace facts"
5
+ CommandDeactivateNode = "deactivate node"
6
+ CommandStoreReport = "store report"
7
+ end
8
+ end
@@ -0,0 +1,112 @@
1
+ require 'puppet/util/puppetdb/command_names'
2
+ require 'puppet/util/puppetdb/blacklist'
3
+
4
+ module Puppet::Util::Puppetdb
5
+ class Config
6
+ include Puppet::Util::Puppetdb::CommandNames
7
+
8
+ # Public class methods
9
+
10
+ def self.load(config_file = nil)
11
+ defaults = {
12
+ :server => "puppetdb",
13
+ :port => 8081,
14
+ :soft_write_failure => false,
15
+ :ignore_blacklisted_events => true,
16
+ }
17
+
18
+ config_file ||= File.join(Puppet[:confdir], "puppetdb.conf")
19
+
20
+ if File.exists?(config_file)
21
+ Puppet.debug("Configuring PuppetDB terminuses with config file #{config_file}")
22
+ content = File.read(config_file)
23
+ else
24
+ Puppet.debug("No #{config_file} file found; falling back to default server and port #{defaults[:server]}:#{defaults[:port]}")
25
+ content = ''
26
+ end
27
+
28
+ result = {}
29
+ section = nil
30
+ content.lines.each_with_index do |line,number|
31
+ # Gotta track the line numbers properly
32
+ number += 1
33
+ case line
34
+ when /^\[(\w+)\s*\]$/
35
+ section = $1
36
+ result[section] ||= {}
37
+ when /^\s*(\w+)\s*=\s*(\S+)\s*$/
38
+ raise "Setting '#{line}' is illegal outside of section in PuppetDB config #{config_file}:#{number}" unless section
39
+ result[section][$1] = $2
40
+ when /^\s*[#;]/
41
+ # Skip comments
42
+ when /^\s*$/
43
+ # Skip blank lines
44
+ else
45
+ raise "Unparseable line '#{line}' in PuppetDB config #{config_file}:#{number}"
46
+ end
47
+ end
48
+
49
+
50
+ main_section = result['main'] || {}
51
+ # symbolize the keys
52
+ main_section = main_section.inject({}) {|h, (k,v)| h[k.to_sym] = v ; h}
53
+ # merge with defaults but filter out anything except the legal settings
54
+ config_hash = defaults.merge(main_section).reject do |k, v|
55
+ !([:server, :port, :ignore_blacklisted_events, :soft_write_failure].include?(k))
56
+ end
57
+
58
+ config_hash[:server] = config_hash[:server].strip
59
+ config_hash[:port] = config_hash[:port].to_i
60
+ config_hash[:ignore_blacklisted_events] =
61
+ Puppet::Util::Puppetdb.to_bool(config_hash[:ignore_blacklisted_events])
62
+ config_hash[:soft_write_failure] =
63
+ Puppet::Util::Puppetdb.to_bool(config_hash[:soft_write_failure])
64
+
65
+ self.new(config_hash)
66
+ rescue => detail
67
+ puts detail.backtrace if Puppet[:trace]
68
+ Puppet.warning "Could not configure PuppetDB terminuses: #{detail}"
69
+ raise
70
+ end
71
+
72
+ # @!group Public instance methods
73
+
74
+ def initialize(config_hash = {})
75
+ @config = config_hash
76
+ initialize_blacklisted_events()
77
+ end
78
+
79
+ def server
80
+ config[:server]
81
+ end
82
+
83
+ def port
84
+ config[:port]
85
+ end
86
+
87
+ def ignore_blacklisted_events?
88
+ config[:ignore_blacklisted_events]
89
+ end
90
+
91
+ def is_event_blacklisted?(event)
92
+ @blacklist.is_event_blacklisted? event
93
+ end
94
+
95
+ def soft_write_failure
96
+ config[:soft_write_failure]
97
+ end
98
+
99
+ # @!group Private instance methods
100
+
101
+ # @!attribute [r] count
102
+ # @api private
103
+ attr_reader :config
104
+
105
+ Blacklist = Puppet::Util::Puppetdb::Blacklist
106
+
107
+ # @api private
108
+ def initialize_blacklisted_events(events = Blacklist::BlacklistedEvents)
109
+ @blacklist = Blacklist.new(events)
110
+ end
111
+ end
112
+ end