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,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,22 @@
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
+ # Run initial checks
9
+ def initialize
10
+ Puppet::Util::Puppetdb::GlobalCheck.run
11
+ end
12
+
13
+ def find(request)
14
+ end
15
+
16
+ def save(request)
17
+ end
18
+
19
+ def destroy(request)
20
+ submit_command(request.key, request.key, CommandDeactivateNode, 2)
21
+ end
22
+ end
@@ -0,0 +1,107 @@
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
+ # Run initial checks
10
+ def initialize
11
+ Puppet::Util::Puppetdb::GlobalCheck.run
12
+ end
13
+
14
+ def search(request)
15
+ profile "resource#search" do
16
+ type = request.key
17
+ host = request.options[:host]
18
+ filter = request.options[:filter]
19
+ scope = request.options[:scope]
20
+
21
+ # At minimum, we want to filter to the right type of exported resources.
22
+ expr = ['and',
23
+ ['=', 'type', type],
24
+ ['=', 'exported', true],
25
+ ['not',
26
+ ['=', 'certname', host]]]
27
+
28
+ filter_expr = build_expression(filter)
29
+ expr << filter_expr if filter_expr
30
+
31
+ query_param = CGI.escape(expr.to_json)
32
+
33
+ begin
34
+ url = "/v3/resources?query=#{query_param}"
35
+ response = profile "Resources query: #{URI.unescape(url)}" do
36
+ http_get(request, url, headers)
37
+ end
38
+ log_x_deprecation_header(response)
39
+
40
+ unless response.is_a? Net::HTTPSuccess
41
+ # Newline characters cause an HTTP error, so strip them
42
+ raise "[#{response.code} #{response.message}] #{response.body.gsub(/[\r\n]/, '')}"
43
+ end
44
+ rescue => e
45
+ raise Puppet::Error, "Could not retrieve resources from the PuppetDB at #{self.class.server}:#{self.class.port}: #{e}"
46
+ end
47
+
48
+ resources = profile "Parse resource query response (size: #{response.body.size})" do
49
+ JSON.load(response.body)
50
+ end
51
+
52
+ profile "Build up collected resource objects (count: #{resources.count})" do
53
+ resources.map do |res|
54
+ params = res['parameters'] || {}
55
+ params = params.map do |name,value|
56
+ Puppet::Parser::Resource::Param.new(:name => name, :value => value)
57
+ end
58
+ attrs = {:parameters => params, :scope => scope}
59
+ result = Puppet::Parser::Resource.new(res['type'], res['title'], attrs)
60
+ result.collector_id = "#{res['certname']}|#{res['type']}|#{res['title']}"
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def build_expression(filter)
68
+ return nil unless filter
69
+
70
+ lhs, op, rhs = filter
71
+
72
+ case op
73
+ when '==', '!='
74
+ build_predicate(op, lhs, rhs)
75
+ when 'and', 'or'
76
+ build_join(op, lhs, rhs)
77
+ else
78
+ raise Puppet::Error, "Operator #{op} in #{filter.inspect} not supported"
79
+ end
80
+ end
81
+
82
+ def build_predicate(op, field, value)
83
+ # Title and tag aren't parameters, so we have to special-case them.
84
+ expr = case field
85
+ when "tag"
86
+ # Tag queries are case-insensitive, so downcase them
87
+ ["=", "tag", value.downcase]
88
+ when "title"
89
+ ["=", "title", value]
90
+ else
91
+ ["=", ['parameter', field], value]
92
+ end
93
+
94
+ op == '!=' ? ['not', expr] : expr
95
+ end
96
+
97
+ def build_join(op, lhs, rhs)
98
+ lhs = build_expression(lhs)
99
+ rhs = build_expression(rhs)
100
+
101
+ [op, lhs, rhs]
102
+ end
103
+
104
+ def headers
105
+ {'Accept' => 'application/json'}
106
+ end
107
+ end
@@ -0,0 +1,186 @@
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
+ Puppet::Util::Puppetdb::GlobalCheck.run
9
+
10
+ CommandStoreReport = Puppet::Util::Puppetdb::CommandNames::CommandStoreReport
11
+
12
+ desc <<-DESC
13
+ Send report information to PuppetDB via the REST API. Reports are serialized to
14
+ JSON format, and then submitted to puppetdb using the '#{CommandStoreReport}'
15
+ command.
16
+ DESC
17
+
18
+
19
+ def process
20
+ profile "report#process" do
21
+ submit_command(self.host, report_to_hash, CommandStoreReport, 3)
22
+ end
23
+ end
24
+
25
+ # TODO: It seems unfortunate that we have to access puppet_version and
26
+ # report_format directly as instance variables. I've filed the following
27
+ # ticket / pull req against puppet to expose them via accessors, which
28
+ # seems more consistent and safer for the long-term. However, for reasons
29
+ # relating to backwards compatibility we won't be able to switch over to
30
+ # the accessors until version 3.x of puppet is our oldest supported version.
31
+ #
32
+ # This was resolved in puppet 3.x via ticket #16139 (puppet pull request #1073).
33
+
34
+ # @api private
35
+ def report_format
36
+ @report_format
37
+ end
38
+
39
+ # @api private
40
+ def puppet_version
41
+ @puppet_version
42
+ end
43
+
44
+ # Convert `self` (an instance of `Puppet::Transaction::Report`) to a hash
45
+ # suitable for sending over the wire to PuppetDB
46
+ #
47
+ # @api private
48
+ def report_to_hash
49
+ profile "Convert report to wire format hash" do
50
+ add_v4_fields_to_report(
51
+ {
52
+ "certname" => host,
53
+ "puppet-version" => puppet_version,
54
+ "report-format" => report_format,
55
+ "configuration-version" => configuration_version.to_s,
56
+ "start-time" => Puppet::Util::Puppetdb.to_wire_time(time),
57
+ "end-time" => Puppet::Util::Puppetdb.to_wire_time(time + run_duration),
58
+ "resource-events" => build_events_list,
59
+ "environment" => environment,
60
+ })
61
+ end
62
+ end
63
+
64
+ # @api private
65
+ def build_events_list
66
+ profile "Build events list (count: #{resource_statuses.count})" do
67
+ filter_events(resource_statuses.inject([]) do |events, status_entry|
68
+ _, status = *status_entry
69
+ if ! (status.events.empty?)
70
+ events.concat(status.events.map { |event| event_to_hash(status, event) })
71
+ elsif status.skipped
72
+ events.concat([fabricate_event(status, "skipped")])
73
+ elsif status.failed
74
+ # PP-254:
75
+ # We have to fabricate resource events here due to a bug/s in report providers
76
+ # that causes them not to include events on a resource status that has failed.
77
+ # When PuppetDB is able to make a hard break from older version of Puppet that
78
+ # have this bug, we can remove this behavior.
79
+ events.concat([fabricate_event(status, "failure")])
80
+ end
81
+ events
82
+ end)
83
+ end
84
+ end
85
+
86
+ # @api private
87
+ def run_duration
88
+ # TODO: this is wrong in puppet. I am consistently seeing reports where
89
+ # start-time + this value is less than the timestamp on the individual
90
+ # resource events. Not sure what the best short-term fix is yet; the long
91
+ # term fix is obviously to make the correct data available in puppet.
92
+ # I've filed a ticket against puppet here:
93
+ # http://projects.puppetlabs.com/issues/16480
94
+ #
95
+ # NOTE: failed reports have an empty metrics hash. Just send 0 for run time,
96
+ # since we don't have access to any better information.
97
+ if metrics["time"] and metrics["time"]["total"]
98
+ metrics["time"]["total"]
99
+ else
100
+ raise Puppet::Error, "Report from #{host} contained no metrics, which is often caused by a failed catalog compilation. Unable to process."
101
+ end
102
+ end
103
+
104
+ # Convert an instance of `Puppet::Transaction::Event` to a hash
105
+ # suitable for sending over the wire to PuppetDB
106
+ #
107
+ # @api private
108
+ def event_to_hash(resource_status, event)
109
+ add_v4_fields_to_event(resource_status,
110
+ {
111
+ "status" => event.status,
112
+ "timestamp" => Puppet::Util::Puppetdb.to_wire_time(event.time),
113
+ "resource-type" => resource_status.resource_type,
114
+ "resource-title" => resource_status.title,
115
+ "property" => event.property,
116
+ "new-value" => event.desired_value,
117
+ "old-value" => event.previous_value,
118
+ "message" => event.message,
119
+ "file" => resource_status.file,
120
+ "line" => resource_status.line
121
+ })
122
+ end
123
+
124
+ # Given an instance of `Puppet::Resource::Status` and a status string,
125
+ # this method fabricates a PuppetDB event object with the provided
126
+ # `"status"`.
127
+ #
128
+ # @api private
129
+ def fabricate_event(resource_status, event_status)
130
+ add_v4_fields_to_event(resource_status,
131
+ {
132
+ "status" => event_status,
133
+ "timestamp" => Puppet::Util::Puppetdb.to_wire_time(resource_status.time),
134
+ "resource-type" => resource_status.resource_type,
135
+ "resource-title" => resource_status.title,
136
+ "property" => nil,
137
+ "new-value" => nil,
138
+ "old-value" => nil,
139
+ "message" => nil,
140
+ "file" => resource_status.file,
141
+ "line" => resource_status.line
142
+ })
143
+ end
144
+
145
+ # Backwards compatibility with versions of Puppet prior to report format 4
146
+ #
147
+ # @api private
148
+ def add_v4_fields_to_report(report_hash)
149
+ if report_format >= 4
150
+ report_hash.merge("transaction-uuid" => transaction_uuid)
151
+ else
152
+ report_hash.merge("transaction-uuid" => nil)
153
+ end
154
+ end
155
+
156
+ # Backwards compatibility with versions of Puppet prior to report format 4
157
+ #
158
+ # @api private
159
+ def add_v4_fields_to_event(resource_status, event_hash)
160
+ if report_format >= 4
161
+ event_hash.merge("containment-path" => resource_status.containment_path)
162
+ else
163
+ event_hash.merge("containment-path" => nil)
164
+ end
165
+ end
166
+
167
+ # Filter out blacklisted events, if we're configured to do so
168
+ #
169
+ # @api private
170
+ def filter_events(events)
171
+ if config.ignore_blacklisted_events?
172
+ profile "Filter blacklisted events" do
173
+ events.select { |e| ! config.is_event_blacklisted?(e) }
174
+ end
175
+ else
176
+ events
177
+ end
178
+ end
179
+
180
+ # Helper method for accessing the puppetdb configuration
181
+ #
182
+ # @api private
183
+ def config
184
+ Puppet::Util::Puppetdb.config
185
+ end
186
+ end
@@ -0,0 +1,108 @@
1
+ require 'puppet/util'
2
+ require 'puppet/util/logging'
3
+ require 'puppet/util/profiler'
4
+ require 'puppet/util/puppetdb/global_check'
5
+ require 'puppet/util/puppetdb/command_names'
6
+ require 'puppet/util/puppetdb/command'
7
+ require 'puppet/util/puppetdb/config'
8
+ require 'digest/sha1'
9
+ require 'time'
10
+ require 'fileutils'
11
+
12
+ module Puppet::Util::Puppetdb
13
+
14
+ def self.server
15
+ config.server
16
+ end
17
+
18
+ def self.port
19
+ config.port
20
+ end
21
+
22
+ def self.config
23
+ @config ||= Puppet::Util::Puppetdb::Config.load
24
+ @config
25
+ end
26
+
27
+ # This magical stuff is needed so that the indirector termini will make requests to
28
+ # the correct host/port, because this module gets mixed in to our indirector
29
+ # termini.
30
+ module ClassMethods
31
+ def server
32
+ Puppet::Util::Puppetdb.server
33
+ end
34
+
35
+ def port
36
+ Puppet::Util::Puppetdb.port
37
+ end
38
+ end
39
+
40
+ def self.included(child)
41
+ child.extend ClassMethods
42
+ end
43
+
44
+ # Given an instance of ruby's Time class, this method converts it to a String
45
+ # that conforms to PuppetDB's wire format for representing a date/time.
46
+ def self.to_wire_time(time)
47
+ # The current implementation simply calls iso8601, but having this method
48
+ # allows us to change that in the future if needed w/o being forced to
49
+ # update all of the date objects elsewhere in the code.
50
+ time.iso8601(9)
51
+ end
52
+
53
+ # Convert a value (usually a string) to a boolean
54
+ def self.to_bool(value)
55
+ case value
56
+ when true, "true"; return true
57
+ when false, "false"; return false
58
+ else
59
+ raise ArgumentError.new("invalid value for Boolean: \"#{val}\"")
60
+ end
61
+ end
62
+
63
+ # @!group Public instance methods
64
+
65
+ # Submit a command to PuppetDB.
66
+ #
67
+ # @param certname [String] hostname name of puppetdb instance
68
+ # @param payload [String] payload
69
+ # @param command_name [String] name of command
70
+ # @param version [Number] version number of command
71
+ def submit_command(certname, payload, command_name, version)
72
+ profile "Submitted command '#{command_name}' version '#{version}'" do
73
+ command = Puppet::Util::Puppetdb::Command.new(command_name, version, certname, payload)
74
+ command.submit
75
+ end
76
+ end
77
+
78
+ # Profile a block of code and log the time it took to execute.
79
+ #
80
+ # This outputs logs entries to the Puppet masters logging destination
81
+ # providing the time it took, a message describing the profiled code
82
+ # and a leaf location marking where the profile method was called
83
+ # in the profiled hierachy.
84
+ #
85
+ # @param message [String] A description of the profiled event
86
+ # @param block [Block] The segment of code to profile
87
+ # @api public
88
+ def profile(message, &block)
89
+ message = "PuppetDB: " + message
90
+ Puppet::Util::Profiler.profile(message, &block)
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