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.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/NOTICE.txt +17 -0
- data/README.md +22 -0
- data/puppet/lib/puppet/application/storeconfigs.rb +4 -0
- data/puppet/lib/puppet/face/node/deactivate.rb +37 -0
- data/puppet/lib/puppet/face/node/status.rb +80 -0
- data/puppet/lib/puppet/face/storeconfigs.rb +193 -0
- data/puppet/lib/puppet/indirector/catalog/puppetdb.rb +400 -0
- data/puppet/lib/puppet/indirector/facts/puppetdb.rb +152 -0
- data/puppet/lib/puppet/indirector/facts/puppetdb_apply.rb +25 -0
- data/puppet/lib/puppet/indirector/node/puppetdb.rb +19 -0
- data/puppet/lib/puppet/indirector/resource/puppetdb.rb +108 -0
- data/puppet/lib/puppet/reports/puppetdb.rb +188 -0
- data/puppet/lib/puppet/util/puppetdb.rb +108 -0
- data/puppet/lib/puppet/util/puppetdb/char_encoding.rb +316 -0
- data/puppet/lib/puppet/util/puppetdb/command.rb +116 -0
- data/puppet/lib/puppet/util/puppetdb/command_names.rb +8 -0
- data/puppet/lib/puppet/util/puppetdb/config.rb +148 -0
- data/puppet/lib/puppet/util/puppetdb/http.rb +121 -0
- data/puppet/spec/README.markdown +8 -0
- data/puppet/spec/spec.opts +6 -0
- data/puppet/spec/spec_helper.rb +38 -0
- data/puppet/spec/unit/face/node/deactivate_spec.rb +28 -0
- data/puppet/spec/unit/face/node/status_spec.rb +43 -0
- data/puppet/spec/unit/face/storeconfigs_spec.rb +199 -0
- data/puppet/spec/unit/indirector/catalog/puppetdb_spec.rb +703 -0
- data/puppet/spec/unit/indirector/facts/puppetdb_apply_spec.rb +27 -0
- data/puppet/spec/unit/indirector/facts/puppetdb_spec.rb +347 -0
- data/puppet/spec/unit/indirector/node/puppetdb_spec.rb +61 -0
- data/puppet/spec/unit/indirector/resource/puppetdb_spec.rb +199 -0
- data/puppet/spec/unit/reports/puppetdb_spec.rb +249 -0
- data/puppet/spec/unit/util/puppetdb/char_encoding_spec.rb +212 -0
- data/puppet/spec/unit/util/puppetdb/command_spec.rb +98 -0
- data/puppet/spec/unit/util/puppetdb/config_spec.rb +227 -0
- data/puppet/spec/unit/util/puppetdb/http_spec.rb +138 -0
- data/puppet/spec/unit/util/puppetdb_spec.rb +33 -0
- 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
|