ml-puppetdb-terminus 3.2.1

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/NOTICE.txt +17 -0
  4. data/README.md +22 -0
  5. data/puppet/lib/puppet/application/storeconfigs.rb +4 -0
  6. data/puppet/lib/puppet/face/node/deactivate.rb +37 -0
  7. data/puppet/lib/puppet/face/node/status.rb +80 -0
  8. data/puppet/lib/puppet/face/storeconfigs.rb +193 -0
  9. data/puppet/lib/puppet/indirector/catalog/puppetdb.rb +400 -0
  10. data/puppet/lib/puppet/indirector/facts/puppetdb.rb +152 -0
  11. data/puppet/lib/puppet/indirector/facts/puppetdb_apply.rb +25 -0
  12. data/puppet/lib/puppet/indirector/node/puppetdb.rb +19 -0
  13. data/puppet/lib/puppet/indirector/resource/puppetdb.rb +108 -0
  14. data/puppet/lib/puppet/reports/puppetdb.rb +188 -0
  15. data/puppet/lib/puppet/util/puppetdb.rb +108 -0
  16. data/puppet/lib/puppet/util/puppetdb/char_encoding.rb +316 -0
  17. data/puppet/lib/puppet/util/puppetdb/command.rb +116 -0
  18. data/puppet/lib/puppet/util/puppetdb/command_names.rb +8 -0
  19. data/puppet/lib/puppet/util/puppetdb/config.rb +148 -0
  20. data/puppet/lib/puppet/util/puppetdb/http.rb +121 -0
  21. data/puppet/spec/README.markdown +8 -0
  22. data/puppet/spec/spec.opts +6 -0
  23. data/puppet/spec/spec_helper.rb +38 -0
  24. data/puppet/spec/unit/face/node/deactivate_spec.rb +28 -0
  25. data/puppet/spec/unit/face/node/status_spec.rb +43 -0
  26. data/puppet/spec/unit/face/storeconfigs_spec.rb +199 -0
  27. data/puppet/spec/unit/indirector/catalog/puppetdb_spec.rb +703 -0
  28. data/puppet/spec/unit/indirector/facts/puppetdb_apply_spec.rb +27 -0
  29. data/puppet/spec/unit/indirector/facts/puppetdb_spec.rb +347 -0
  30. data/puppet/spec/unit/indirector/node/puppetdb_spec.rb +61 -0
  31. data/puppet/spec/unit/indirector/resource/puppetdb_spec.rb +199 -0
  32. data/puppet/spec/unit/reports/puppetdb_spec.rb +249 -0
  33. data/puppet/spec/unit/util/puppetdb/char_encoding_spec.rb +212 -0
  34. data/puppet/spec/unit/util/puppetdb/command_spec.rb +98 -0
  35. data/puppet/spec/unit/util/puppetdb/config_spec.rb +227 -0
  36. data/puppet/spec/unit/util/puppetdb/http_spec.rb +138 -0
  37. data/puppet/spec/unit/util/puppetdb_spec.rb +33 -0
  38. metadata +115 -0
@@ -0,0 +1,25 @@
1
+ require 'puppet/indirector/facts/puppetdb'
2
+
3
+ # This class provides an alternative implementation of the Facts::Puppetdb
4
+ # terminus that better suits execution via `puppet apply`.
5
+ #
6
+ # This terminus is designed to be used as a cache terminus, to ensure that facts
7
+ # are stored in PuppetDB. It does not act as a real cache itself however, it
8
+ # tells Puppet to fallback to the `terminus` instead.
9
+ class Puppet::Node::Facts::PuppetdbApply < Puppet::Node::Facts::Puppetdb
10
+ attr_writer :dbstored
11
+
12
+ # Here we override the normal save, only saving the first time, as a `save`
13
+ # can be called multiple times in a puppet run.
14
+ def save(args)
15
+ unless @dbstored
16
+ @dbstored = true
17
+ super(args)
18
+ end
19
+ end
20
+
21
+ # By returning nil, we force puppet to use the real terminus.
22
+ def find(args)
23
+ nil
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ require 'puppet/node'
2
+ require 'puppet/indirector/rest'
3
+ require 'puppet/util/puppetdb'
4
+
5
+ class Puppet::Node::Puppetdb < Puppet::Indirector::REST
6
+ include Puppet::Util::Puppetdb
7
+
8
+ def find(request)
9
+ end
10
+
11
+ def save(request)
12
+ end
13
+
14
+ def destroy(request)
15
+ payload = { :certname => request.key,
16
+ :producer_timestamp => request.options[:producer_timestamp] || Time.now.iso8601(5) }
17
+ submit_command(request.key, payload, CommandDeactivateNode, 3)
18
+ end
19
+ end
@@ -0,0 +1,108 @@
1
+ require 'puppet/indirector/rest'
2
+ require 'puppet/util/puppetdb'
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ class Puppet::Resource::Puppetdb < Puppet::Indirector::REST
7
+ include Puppet::Util::Puppetdb
8
+
9
+ def search(request)
10
+ profile("resource#search",
11
+ [:puppetdb, :resource, :search, request.key]) do
12
+ type = request.key
13
+ host = request.options[:host]
14
+ filter = request.options[:filter]
15
+ scope = request.options[:scope]
16
+
17
+ # At minimum, we want to filter to the right type of exported resources.
18
+ expr = ['and',
19
+ ['=', 'type', type],
20
+ ['=', 'exported', true],
21
+ ['not',
22
+ ['=', 'certname', host]]]
23
+
24
+ filter_expr = build_expression(filter)
25
+ expr << filter_expr if filter_expr
26
+
27
+ query_param = CGI.escape(expr.to_json)
28
+
29
+ begin
30
+ response = Http.action("/pdb/query/v4/resources?query=#{query_param}") do |http_instance, path|
31
+ profile("Resources query: #{URI.unescape(path)}",
32
+ [:puppetdb, :resource, :search, :query, request.key]) do
33
+ http_instance.get(path, headers)
34
+ end
35
+ end
36
+
37
+ log_x_deprecation_header(response)
38
+
39
+ unless response.is_a? Net::HTTPSuccess
40
+ # Newline characters cause an HTTP error, so strip them
41
+ raise "[#{response.code} #{response.message}] #{response.body.gsub(/[\r\n]/, '')}"
42
+ end
43
+ rescue => e
44
+ raise Puppet::Error, "Could not retrieve resources from the PuppetDB at #{self.class.server}:#{self.class.port}: #{e}"
45
+ end
46
+
47
+ resources = profile("Parse resource query response (size: #{response.body.size})",
48
+ [:puppetdb, :resource, :search, :parse_query_response, request.key]) do
49
+ JSON.load(response.body)
50
+ end
51
+
52
+ profile("Build up collected resource objects (count: #{resources.count})",
53
+ [:puppetdb, :resource, :search, :build_up_collected_objects, request.key]) do
54
+ resources.map do |res|
55
+ params = res['parameters'] || {}
56
+ params = params.map do |name,value|
57
+ Puppet::Parser::Resource::Param.new(:name => name, :value => value)
58
+ end
59
+ attrs = {:parameters => params, :scope => scope}
60
+ result = Puppet::Parser::Resource.new(res['type'], res['title'], attrs)
61
+ result.collector_id = "#{res['certname']}|#{res['type']}|#{res['title']}"
62
+ result
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def build_expression(filter)
69
+ return nil unless filter
70
+
71
+ lhs, op, rhs = filter
72
+
73
+ case op
74
+ when '==', '!='
75
+ build_predicate(op, lhs, rhs)
76
+ when 'and', 'or'
77
+ build_join(op, lhs, rhs)
78
+ else
79
+ raise Puppet::Error, "Operator #{op} in #{filter.inspect} not supported"
80
+ end
81
+ end
82
+
83
+ def build_predicate(op, field, value)
84
+ # Title and tag aren't parameters, so we have to special-case them.
85
+ expr = case field
86
+ when "tag"
87
+ # Tag queries are case-insensitive, so downcase them
88
+ ["=", "tag", value.downcase]
89
+ when "title"
90
+ ["=", "title", value]
91
+ else
92
+ ["=", ['parameter', field], value]
93
+ end
94
+
95
+ op == '!=' ? ['not', expr] : expr
96
+ end
97
+
98
+ def build_join(op, lhs, rhs)
99
+ lhs = build_expression(lhs)
100
+ rhs = build_expression(rhs)
101
+
102
+ [op, lhs, rhs]
103
+ end
104
+
105
+ def headers
106
+ {'Accept' => 'application/json'}
107
+ end
108
+ end
@@ -0,0 +1,188 @@
1
+ require 'puppet'
2
+ require 'puppet/util/puppetdb'
3
+ require 'puppet/util/puppetdb/command_names'
4
+
5
+ Puppet::Reports.register_report(:puppetdb) do
6
+ include Puppet::Util::Puppetdb
7
+
8
+ CommandStoreReport = Puppet::Util::Puppetdb::CommandNames::CommandStoreReport
9
+
10
+ desc <<-DESC
11
+ Send report information to PuppetDB via the REST API. Reports are serialized to
12
+ JSON format, and then submitted to puppetdb using the '#{CommandStoreReport}'
13
+ command.
14
+ DESC
15
+
16
+ # Process the report by formatting it into a PuppetDB 'store report'
17
+ # command and submitting it.
18
+ #
19
+ # @return [void]
20
+ def process
21
+ profile("report#process", [:puppetdb, :report, :process]) do
22
+ submit_command(self.host, report_to_hash, CommandStoreReport, 6)
23
+ end
24
+
25
+ nil
26
+ end
27
+
28
+ # Convert `self` (an instance of `Puppet::Transaction::Report`) to a hash
29
+ # suitable for sending over the wire to PuppetDB
30
+ #
31
+ # @return Hash[<String, Object>]
32
+ # @api private
33
+ def report_to_hash
34
+ profile("Convert report to wire format hash",
35
+ [:puppetdb, :report, :convert_to_wire_format_hash]) do
36
+ if environment.nil?
37
+ raise Puppet::Error, "Environment is nil, unable to submit report. This may be due a bug with Puppet. Ensure you are running the latest revision, see PUP-2508 for more details."
38
+ end
39
+
40
+ resources = build_resources_list
41
+ is_noop = resources.any? { |rs| has_noop_event?(rs) } and resources.none? { |rs| has_failed_event?(rs) }
42
+
43
+ {
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,
58
+ }
59
+ end
60
+ end
61
+
62
+ # @return TrueClass
63
+ # @api private
64
+ def has_noop_event?(resource)
65
+ resource["events"].any? { |event| event["status"] == 'noop' }
66
+ end
67
+
68
+ # @return TrueClass
69
+ # @api private
70
+ def has_failed_event?(resource)
71
+ resource["events"].any? { |event| event["status"] == 'failed' }
72
+ end
73
+
74
+ # @return Array[Hash]
75
+ # @api private
76
+ def build_resources_list
77
+ profile("Build resources list (count: #{resource_statuses.count})",
78
+ [:puppetdb, :resources_list, :build]) do
79
+ resources = resource_statuses.values.map { |resource| resource_status_to_hash(resource) }
80
+ if ! config.include_unchanged_resources?
81
+ resources.select{ |resource| (! resource["events"].empty?) or resource["skipped"] }
82
+ else
83
+ resources
84
+ end
85
+ end
86
+ end
87
+
88
+ # @return Array[Hash]
89
+ # @api private
90
+ def build_logs_list
91
+ profile("Build logs list (count: #{logs.count})",
92
+ [:puppetdb, :logs_list, :build]) do
93
+ logs.map do |log|
94
+ {
95
+ 'file' => log.file,
96
+ 'line' => log.line,
97
+ 'level' => log.level,
98
+ 'message' => log.message,
99
+ 'source' => log.source,
100
+ 'tags' => [*log.tags],
101
+ 'time' => Puppet::Util::Puppetdb.to_wire_time(log.time),
102
+ }
103
+ end
104
+ end
105
+ end
106
+
107
+ # @return Array[Hash}
108
+ # @api private
109
+ def build_metrics_list
110
+ profile("Build metrics list (count: #{metrics.count})",
111
+ [:puppetdb, :metrics_list, :build]) do
112
+ metrics_list = []
113
+ metrics.each do |name, data|
114
+ metric_hashes = data.values.map {|x| {"category" => data.name, "name" => x.first, "value" => x.last}}
115
+ metrics_list.concat(metric_hashes)
116
+ end
117
+ metrics_list
118
+ end
119
+ end
120
+
121
+
122
+ # @return Number
123
+ # @api private
124
+ def run_duration
125
+ # TODO: this is wrong in puppet. I am consistently seeing reports where
126
+ # start-time + this value is less than the timestamp on the individual
127
+ # resource events. Not sure what the best short-term fix is yet; the long
128
+ # term fix is obviously to make the correct data available in puppet.
129
+ # I've filed a ticket against puppet here:
130
+ # http://projects.puppetlabs.com/issues/16480
131
+ #
132
+ # NOTE: failed reports have an empty metrics hash. Just send 0 for run time,
133
+ # since we don't have access to any better information.
134
+ if metrics["time"] and metrics["time"]["total"]
135
+ metrics["time"]["total"]
136
+ else
137
+ 0
138
+ end
139
+ end
140
+
141
+ # Convert an instance of `Puppet::Transaction::Event` to a hash
142
+ # suitable for sending over the wire to PuppetDB
143
+ #
144
+ # @return Hash[<String, Object>]
145
+ # @api private
146
+ def event_to_hash(event)
147
+ {
148
+ "status" => event.status,
149
+ "timestamp" => Puppet::Util::Puppetdb.to_wire_time(event.time),
150
+ "property" => event.property,
151
+ "new_value" => event.desired_value,
152
+ "old_value" => event.previous_value,
153
+ "message" => event.message,
154
+ }
155
+ end
156
+
157
+ def build_events_list(events)
158
+ profile("Build events list (count: #{events.count})",
159
+ [:puppetdb, :events_list, :build]) do
160
+ events.map { |event| event_to_hash(event) }
161
+ end
162
+ end
163
+
164
+ # Convert an instance of `Puppet::Resource::Status` to a hash
165
+ # suitable for sending over the wire to PuppetDB
166
+ #
167
+ # @return Hash[<String, Object>]
168
+ # @api private
169
+ def resource_status_to_hash(resource_status)
170
+ {
171
+ "skipped" => resource_status.skipped,
172
+ "timestamp" => Puppet::Util::Puppetdb.to_wire_time(resource_status.time),
173
+ "resource_type" => resource_status.resource_type,
174
+ "resource_title" => resource_status.title.to_s,
175
+ "file" => resource_status.file,
176
+ "line" => resource_status.line,
177
+ "containment_path" => resource_status.containment_path,
178
+ "events" => build_events_list(resource_status.events),
179
+ }
180
+ end
181
+
182
+ # Helper method for accessing the puppetdb configuration
183
+ #
184
+ # @api private
185
+ def config
186
+ Puppet::Util::Puppetdb.config
187
+ end
188
+ end
@@ -0,0 +1,108 @@
1
+ require 'puppet/util'
2
+ require 'puppet/util/logging'
3
+ require 'puppet/util/profiler'
4
+ require 'puppet/util/puppetdb/command_names'
5
+ require 'puppet/util/puppetdb/command'
6
+ require 'puppet/util/puppetdb/config'
7
+ require 'digest/sha1'
8
+ require 'time'
9
+ require 'fileutils'
10
+
11
+ module Puppet::Util::Puppetdb
12
+
13
+ class CommandSubmissionError < Puppet::Error
14
+ def initialize(msg, context)
15
+ super(msg)
16
+ @context = context
17
+ end
18
+ end
19
+
20
+ class InventorySearchError < Puppet::Error
21
+ end
22
+ class SoftWriteFailError < Puppet::Error
23
+ end
24
+
25
+
26
+ def self.config
27
+ @config ||= Puppet::Util::Puppetdb::Config.load
28
+ @config
29
+ end
30
+
31
+ def self.puppet3compat?
32
+ defined?(Puppet::Parser::AST::HashOrArrayAccess)
33
+ end
34
+
35
+ # Given an instance of ruby's Time class, this method converts it to a String
36
+ # that conforms to PuppetDB's wire format for representing a date/time.
37
+ def self.to_wire_time(time)
38
+ # The current implementation simply calls iso8601, but having this method
39
+ # allows us to change that in the future if needed w/o being forced to
40
+ # update all of the date objects elsewhere in the code.
41
+ time.iso8601(9)
42
+ end
43
+
44
+ # Convert a value (usually a string) to a boolean
45
+ def self.to_bool(value)
46
+ case value
47
+ when true, "true"; return true
48
+ when false, "false"; return false
49
+ else
50
+ raise ArgumentError.new("invalid value for Boolean: \"#{val}\"")
51
+ end
52
+ end
53
+
54
+ # @!group Public instance methods
55
+
56
+ # Submit a command to PuppetDB.
57
+ #
58
+ # @param certname [String] The certname this command operates on
59
+ # @param payload [String] payload
60
+ # @param command_name [String] name of command
61
+ # @param version [Number] version number of command
62
+ # @return [Hash <String, String>]
63
+ def submit_command(certname, payload, command_name, version)
64
+ profile("Submitted command '#{command_name}' version '#{version}'",
65
+ [:puppetdb, :command, :submit, command_name, version]) do
66
+ command = Puppet::Util::Puppetdb::Command.new(command_name, version, certname, payload)
67
+ command.submit
68
+ end
69
+ end
70
+
71
+ # Profile a block of code and log the time it took to execute.
72
+ #
73
+ # This outputs logs entries to the Puppet masters logging destination
74
+ # providing the time it took, a message describing the profiled code
75
+ # and a leaf location marking where the profile method was called
76
+ # in the profiled hierachy.
77
+ #
78
+ # @param message [String] A description of the profiled event
79
+ # @param metric_id [Array] A list of strings making up the ID of a metric to profile
80
+ # @param block [Block] The segment of code to profile
81
+ # @api public
82
+ def profile(message, metric_id, &block)
83
+ message = "PuppetDB: " + message
84
+ arity = Puppet::Util::Profiler.method(:profile).arity
85
+ case arity
86
+ when 1
87
+ Puppet::Util::Profiler.profile(message, &block)
88
+ when 2, -2
89
+ Puppet::Util::Profiler.profile(message, metric_id, &block)
90
+ end
91
+ end
92
+
93
+ # @!group Private instance methods
94
+
95
+ # @api private
96
+ def config
97
+ Puppet::Util::Puppetdb.config
98
+ end
99
+
100
+ # @api private
101
+ def log_x_deprecation_header(response)
102
+ if warning = response['x-deprecation']
103
+ Puppet.deprecation_warning "Deprecation from PuppetDB: #{warning}"
104
+ end
105
+ end
106
+ module_function :log_x_deprecation_header
107
+
108
+ end