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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +202 -0
- data/README.md +29 -0
- data/Rakefile +16 -0
- data/lib/md-puppetdb-terminus.rb +1 -0
- data/lib/puppet/application/storeconfigs.rb +4 -0
- data/lib/puppet/face/node/deactivate.rb +38 -0
- data/lib/puppet/face/node/status.rb +83 -0
- data/lib/puppet/face/storeconfigs.rb +179 -0
- data/lib/puppet/indirector/catalog/puppetdb.rb +350 -0
- data/lib/puppet/indirector/facts/puppetdb.rb +134 -0
- data/lib/puppet/indirector/facts/puppetdb_apply.rb +25 -0
- data/lib/puppet/indirector/node/puppetdb.rb +22 -0
- data/lib/puppet/indirector/resource/puppetdb.rb +107 -0
- data/lib/puppet/reports/puppetdb.rb +186 -0
- data/lib/puppet/util/puppetdb.rb +108 -0
- data/lib/puppet/util/puppetdb/blacklist.rb +35 -0
- data/lib/puppet/util/puppetdb/char_encoding.rb +212 -0
- data/lib/puppet/util/puppetdb/command.rb +113 -0
- data/lib/puppet/util/puppetdb/command_names.rb +8 -0
- data/lib/puppet/util/puppetdb/config.rb +112 -0
- data/lib/puppet/util/puppetdb/global_check.rb +31 -0
- data/lib/puppetdb-terminus.rb +1 -0
- data/lib/puppetdb/terminus.rb +6 -0
- data/lib/puppetdb/terminus/version.rb +5 -0
- data/puppetdb-terminus.gemspec +23 -0
- metadata +99 -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,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
|