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