md-puppetdb-terminus 2.0.0.3

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