choria-mcorpc-support 2.22.1 → 2.23.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mcollective.rb +1 -1
  3. data/lib/mcollective/agent/bolt_tasks.ddl +235 -0
  4. data/lib/mcollective/agent/bolt_tasks.json +347 -0
  5. data/lib/mcollective/agent/bolt_tasks.rb +176 -0
  6. data/lib/mcollective/agent/choria_util.ddl +152 -0
  7. data/lib/mcollective/agent/choria_util.json +244 -0
  8. data/lib/mcollective/agent/rpcutil.ddl +7 -3
  9. data/lib/mcollective/agent/rpcutil.json +333 -0
  10. data/lib/mcollective/agent/scout.ddl +169 -0
  11. data/lib/mcollective/agent/scout.json +224 -0
  12. data/lib/mcollective/agents.rb +7 -6
  13. data/lib/mcollective/aggregate.rb +4 -4
  14. data/lib/mcollective/aggregate/average.rb +2 -2
  15. data/lib/mcollective/aggregate/base.rb +2 -2
  16. data/lib/mcollective/aggregate/result.rb +3 -3
  17. data/lib/mcollective/aggregate/result/collection_result.rb +2 -2
  18. data/lib/mcollective/aggregate/result/numeric_result.rb +2 -2
  19. data/lib/mcollective/aggregate/sum.rb +2 -2
  20. data/lib/mcollective/aggregate/summary.rb +3 -4
  21. data/lib/mcollective/application.rb +57 -21
  22. data/lib/mcollective/application/choria.rb +249 -0
  23. data/lib/mcollective/application/completion.rb +6 -6
  24. data/lib/mcollective/application/describe_filter.rb +20 -20
  25. data/lib/mcollective/application/facts.rb +11 -11
  26. data/lib/mcollective/application/federation.rb +239 -0
  27. data/lib/mcollective/application/find.rb +4 -4
  28. data/lib/mcollective/application/help.rb +3 -3
  29. data/lib/mcollective/application/inventory.rb +3 -341
  30. data/lib/mcollective/application/ping.rb +3 -77
  31. data/lib/mcollective/application/playbook.rb +207 -0
  32. data/lib/mcollective/application/plugin.rb +106 -106
  33. data/lib/mcollective/application/rpc.rb +3 -108
  34. data/lib/mcollective/application/tasks.rb +416 -0
  35. data/lib/mcollective/applications.rb +11 -10
  36. data/lib/mcollective/audit/choria.rb +33 -0
  37. data/lib/mcollective/cache.rb +2 -4
  38. data/lib/mcollective/client.rb +11 -10
  39. data/lib/mcollective/config.rb +21 -26
  40. data/lib/mcollective/connector/base.rb +2 -1
  41. data/lib/mcollective/connector/nats.ddl +9 -0
  42. data/lib/mcollective/connector/nats.rb +450 -0
  43. data/lib/mcollective/data.rb +8 -3
  44. data/lib/mcollective/data/agent_data.rb +1 -1
  45. data/lib/mcollective/data/base.rb +6 -5
  46. data/lib/mcollective/data/bolt_task_data.ddl +90 -0
  47. data/lib/mcollective/data/bolt_task_data.rb +32 -0
  48. data/lib/mcollective/data/collective_data.rb +1 -1
  49. data/lib/mcollective/data/fact_data.rb +6 -6
  50. data/lib/mcollective/data/fstat_data.rb +2 -4
  51. data/lib/mcollective/data/result.rb +7 -2
  52. data/lib/mcollective/ddl/agentddl.rb +5 -17
  53. data/lib/mcollective/ddl/base.rb +10 -13
  54. data/lib/mcollective/discovery.rb +12 -26
  55. data/lib/mcollective/discovery/choria.ddl +11 -0
  56. data/lib/mcollective/discovery/choria.rb +223 -0
  57. data/lib/mcollective/discovery/flatfile.rb +7 -8
  58. data/lib/mcollective/discovery/mc.rb +2 -2
  59. data/lib/mcollective/discovery/stdin.rb +17 -18
  60. data/lib/mcollective/exceptions.rb +13 -0
  61. data/lib/mcollective/facts/base.rb +9 -9
  62. data/lib/mcollective/facts/yaml_facts.rb +12 -12
  63. data/lib/mcollective/generators.rb +3 -3
  64. data/lib/mcollective/generators/agent_generator.rb +3 -4
  65. data/lib/mcollective/generators/base.rb +14 -15
  66. data/lib/mcollective/generators/data_generator.rb +5 -6
  67. data/lib/mcollective/log.rb +2 -2
  68. data/lib/mcollective/logger/base.rb +3 -2
  69. data/lib/mcollective/logger/console_logger.rb +10 -10
  70. data/lib/mcollective/logger/file_logger.rb +7 -7
  71. data/lib/mcollective/logger/syslog_logger.rb +11 -15
  72. data/lib/mcollective/matcher.rb +14 -14
  73. data/lib/mcollective/matcher/parser.rb +31 -41
  74. data/lib/mcollective/matcher/scanner.rb +69 -74
  75. data/lib/mcollective/message.rb +10 -17
  76. data/lib/mcollective/monkey_patches.rb +2 -4
  77. data/lib/mcollective/optionparser.rb +1 -0
  78. data/lib/mcollective/pluginmanager.rb +3 -5
  79. data/lib/mcollective/pluginpackager.rb +1 -3
  80. data/lib/mcollective/pluginpackager/agent_definition.rb +3 -8
  81. data/lib/mcollective/pluginpackager/forge_packager.rb +7 -9
  82. data/lib/mcollective/pluginpackager/standard_definition.rb +1 -2
  83. data/lib/mcollective/registration/base.rb +18 -16
  84. data/lib/mcollective/rpc.rb +2 -4
  85. data/lib/mcollective/rpc/actionrunner.rb +16 -18
  86. data/lib/mcollective/rpc/agent.rb +26 -43
  87. data/lib/mcollective/rpc/audit.rb +1 -0
  88. data/lib/mcollective/rpc/client.rb +67 -85
  89. data/lib/mcollective/rpc/helpers.rb +55 -62
  90. data/lib/mcollective/rpc/progress.rb +2 -2
  91. data/lib/mcollective/rpc/reply.rb +17 -19
  92. data/lib/mcollective/rpc/request.rb +7 -5
  93. data/lib/mcollective/rpc/result.rb +6 -8
  94. data/lib/mcollective/rpc/stats.rb +49 -58
  95. data/lib/mcollective/security/base.rb +29 -36
  96. data/lib/mcollective/security/choria.rb +765 -0
  97. data/lib/mcollective/shell.rb +9 -4
  98. data/lib/mcollective/signer/base.rb +28 -0
  99. data/lib/mcollective/signer/choria.rb +185 -0
  100. data/lib/mcollective/ssl.rb +8 -6
  101. data/lib/mcollective/util.rb +52 -53
  102. data/lib/mcollective/util/bolt_support.rb +176 -0
  103. data/lib/mcollective/util/bolt_support/plan_runner.rb +167 -0
  104. data/lib/mcollective/util/bolt_support/task_result.rb +94 -0
  105. data/lib/mcollective/util/bolt_support/task_results.rb +128 -0
  106. data/lib/mcollective/util/choria.rb +1103 -0
  107. data/lib/mcollective/util/indifferent_hash.rb +12 -0
  108. data/lib/mcollective/util/natswrapper.rb +242 -0
  109. data/lib/mcollective/util/playbook.rb +435 -0
  110. data/lib/mcollective/util/playbook/data_stores.rb +201 -0
  111. data/lib/mcollective/util/playbook/data_stores/base.rb +99 -0
  112. data/lib/mcollective/util/playbook/data_stores/consul_data_store.rb +88 -0
  113. data/lib/mcollective/util/playbook/data_stores/environment_data_store.rb +33 -0
  114. data/lib/mcollective/util/playbook/data_stores/etcd_data_store.rb +42 -0
  115. data/lib/mcollective/util/playbook/data_stores/file_data_store.rb +106 -0
  116. data/lib/mcollective/util/playbook/data_stores/shell_data_store.rb +103 -0
  117. data/lib/mcollective/util/playbook/inputs.rb +265 -0
  118. data/lib/mcollective/util/playbook/nodes.rb +207 -0
  119. data/lib/mcollective/util/playbook/nodes/mcollective_nodes.rb +86 -0
  120. data/lib/mcollective/util/playbook/nodes/pql_nodes.rb +40 -0
  121. data/lib/mcollective/util/playbook/nodes/shell_nodes.rb +55 -0
  122. data/lib/mcollective/util/playbook/nodes/terraform_nodes.rb +65 -0
  123. data/lib/mcollective/util/playbook/nodes/yaml_nodes.rb +47 -0
  124. data/lib/mcollective/util/playbook/playbook_logger.rb +47 -0
  125. data/lib/mcollective/util/playbook/puppet_logger.rb +51 -0
  126. data/lib/mcollective/util/playbook/report.rb +152 -0
  127. data/lib/mcollective/util/playbook/task_result.rb +55 -0
  128. data/lib/mcollective/util/playbook/tasks.rb +196 -0
  129. data/lib/mcollective/util/playbook/tasks/base.rb +45 -0
  130. data/lib/mcollective/util/playbook/tasks/graphite_event_task.rb +64 -0
  131. data/lib/mcollective/util/playbook/tasks/mcollective_task.rb +356 -0
  132. data/lib/mcollective/util/playbook/tasks/shell_task.rb +93 -0
  133. data/lib/mcollective/util/playbook/tasks/slack_task.rb +105 -0
  134. data/lib/mcollective/util/playbook/tasks/webhook_task.rb +136 -0
  135. data/lib/mcollective/util/playbook/template_util.rb +98 -0
  136. data/lib/mcollective/util/playbook/uses.rb +169 -0
  137. data/lib/mcollective/util/tasks_support.rb +733 -0
  138. data/lib/mcollective/util/tasks_support/cli.rb +260 -0
  139. data/lib/mcollective/util/tasks_support/default_formatter.rb +138 -0
  140. data/lib/mcollective/util/tasks_support/json_formatter.rb +108 -0
  141. data/lib/mcollective/validator.rb +6 -1
  142. data/lib/mcollective/validator/bolt_task_name_validator.ddl +7 -0
  143. data/lib/mcollective/validator/bolt_task_name_validator.rb +11 -0
  144. data/lib/mcollective/validator/length_validator.rb +1 -3
  145. metadata +67 -4
@@ -0,0 +1,1103 @@
1
+ require "net/http"
2
+ require "resolv"
3
+
4
+ module MCollective
5
+ module Util
6
+ class Choria
7
+ class UserError < StandardError; end
8
+
9
+ class Abort < StandardError; end
10
+
11
+ unless defined?(Choria::VERSION) # rubocop:disable Style/IfUnlessModifier
12
+ VERSION = "0.19.0".freeze
13
+ end
14
+
15
+ attr_writer :ca
16
+
17
+ def initialize(check_ssl=true)
18
+ @config = Config.instance
19
+
20
+ check_ssl_setup if check_ssl
21
+ end
22
+
23
+ # Determines the configured path to the NATS credentials, empty when not set
24
+ #
25
+ # @return [String]
26
+ def credential_file
27
+ get_option("nats.credentials", "")
28
+ end
29
+
30
+ # Determines if a credential file is configured
31
+ #
32
+ # @return [Boolean]
33
+ def credential_file?
34
+ credential_file != ""
35
+ end
36
+
37
+ # Determines if we are connecting to NGS based on credentials and the nats.ngs setting
38
+ #
39
+ # @return [Boolean]
40
+ def ngs?
41
+ credential_file != "" && Util.str_to_bool(get_option("nats.ngs", "false"))
42
+ end
43
+
44
+ # Attempts to load the optional nkeys library
45
+ #
46
+ # @return [Boolean]
47
+ def nkeys?
48
+ require "nkeys"
49
+ true
50
+ rescue LoadError
51
+ false
52
+ end
53
+
54
+ # Creates a new TasksSupport instance with the configured cache dir
55
+ #
56
+ # @return [TasksSupport]
57
+ def tasks_support
58
+ require_relative "tasks_support"
59
+
60
+ Util::TasksSupport.new(self, tasks_cache_dir)
61
+ end
62
+
63
+ # Determines the Tasks Cache dir
64
+ #
65
+ # @return [String] path to the cache
66
+ def tasks_cache_dir
67
+ if Util.windows?
68
+ File.join(Util.windows_prefix, "tasks-cache")
69
+ elsif Process.uid == 0
70
+ "/opt/puppetlabs/mcollective/tasks-cache"
71
+ else
72
+ File.expand_path("~/.puppetlabs/mcollective/tasks-cache")
73
+ end
74
+ end
75
+
76
+ # Determines the Tasks Spool directory
77
+ #
78
+ # @return [String] path to the spool
79
+ def tasks_spool_dir
80
+ if Util.windows?
81
+ File.join(Util.windows_prefix, "tasks-spool")
82
+ elsif Process.uid == 0
83
+ "/opt/puppetlabs/mcollective/tasks-spool"
84
+ else
85
+ File.expand_path("~/.puppetlabs/mcollective/tasks-spool")
86
+ end
87
+ end
88
+
89
+ # Which port to provide stats over HTTP on
90
+ #
91
+ # @return [Integer,nil]
92
+ # @raise [StandardError] when not numeric
93
+ def stats_port
94
+ Integer(get_option("choria.stats_port", "")) if has_option?("choria.stats_port")
95
+ end
96
+
97
+ # Determines if there are any federations configured
98
+ #
99
+ # @return [Boolean]
100
+ def federated?
101
+ !federation_collectives.empty?
102
+ end
103
+
104
+ # List of active collectives that form the federation
105
+ #
106
+ # @return [Array<String>]
107
+ def federation_collectives
108
+ if override_networks = env_fetch("CHORIA_FED_COLLECTIVE", nil)
109
+ override_networks.split(",").map(&:strip).reject(&:empty?)
110
+ else
111
+ get_option("choria.federation.collectives", "").split(",").map(&:strip).reject(&:empty?)
112
+ end
113
+ end
114
+
115
+ # Retrieves a DNS resolver
116
+ #
117
+ # @note mainly used for testing
118
+ # @return [Resolv::DNS]
119
+ def resolver
120
+ Resolv::DNS.new
121
+ end
122
+
123
+ # Retrieves the domain from facter networking.domain if facter is found
124
+ #
125
+ # Potentially we could use the local facts in mcollective but that's a chicken
126
+ # and egg and sometimes its only set after initial connection if something like
127
+ # a cron job generates the yaml cache file
128
+ #
129
+ # @return [String,nil]
130
+ def facter_domain
131
+ if path = facter_cmd
132
+ `"#{path}" networking.domain`.chomp
133
+ end
134
+ end
135
+
136
+ # Determines the domain to do SRV lookups in
137
+ #
138
+ # This is settable using choria.srv_domain and defaults
139
+ # to the domain as reported by facter
140
+ #
141
+ # @return [String]
142
+ def srv_domain
143
+ get_option("choria.srv_domain", nil) || facter_domain
144
+ end
145
+
146
+ # Determines the SRV records to look up
147
+ #
148
+ # If an option choria.srv_domain is set that will be used else facter will be consulted,
149
+ # if neither of those provide a domain name a empty list is returned
150
+ #
151
+ # @param keys [Array<String>] list of keys to lookup
152
+ # @return [Array<String>] list of SRV records
153
+ def srv_records(keys)
154
+ domain = srv_domain
155
+
156
+ if domain.nil? || domain.empty?
157
+ Log.warn("Cannot look up SRV records, facter is not functional and choria.srv_domain was not supplied")
158
+ return []
159
+ end
160
+
161
+ keys.map do |key|
162
+ "%s.%s" % [key, domain]
163
+ end
164
+ end
165
+
166
+ # Determines if SRV records should be used
167
+ #
168
+ # Setting choria.use_srv_records to anything other than t, true, yes or 1 will disable
169
+ # SRV records
170
+ #
171
+ # @return [Boolean]
172
+ def should_use_srv?
173
+ ["t", "true", "yes", "1"].include?(get_option("choria.use_srv_records", "1").downcase)
174
+ end
175
+
176
+ # Query DNS for a series of records
177
+ #
178
+ # The given records will be passed through {#srv_records} to figure out the domain to query in.
179
+ #
180
+ # Querying of records can be bypassed by setting choria.use_srv_records to false
181
+ #
182
+ # @yield [Hash] each record for modification by the caller
183
+ # @param records [Array<String>] the records to query without their domain parts
184
+ # @return [Array<Hash>] with keys :port, :priority, :weight and :target
185
+ def query_srv_records(records)
186
+ unless should_use_srv?
187
+ Log.info("Skipping SRV record queries due to choria.query_srv_records setting")
188
+ return []
189
+ end
190
+
191
+ answers = Array(srv_records(records)).map do |record|
192
+ Log.debug("Attempting to resolve SRV record %s" % record)
193
+ answers = resolver.getresources(record, Resolv::DNS::Resource::IN::SRV)
194
+ Log.debug("Found %d SRV records for %s" % [answers.size, record])
195
+ answers
196
+ end.flatten
197
+
198
+ answers = answers.sort_by(&:priority).chunk(&:priority).sort
199
+ answers = sort_srv_answers(answers)
200
+
201
+ answers.map do |result|
202
+ Log.debug("Found %s:%s with priority %s and weight %s" % [result.target, result.port, result.priority, result.weight])
203
+
204
+ ans = {
205
+ :port => result.port,
206
+ :priority => result.priority,
207
+ :weight => result.weight,
208
+ :target => result.target
209
+ }
210
+
211
+ yield(ans) if block_given?
212
+
213
+ ans
214
+ end
215
+ end
216
+
217
+ # Sorts SRV records according to rfc2782
218
+ #
219
+ # @note this is probably still not correct :( so horrible
220
+ # @param answers [Array<Resolv::DNS::Resource::IN::SRV>]
221
+ # @return [Array<Resolv::DNS::Resource::IN::SRV>] sorted records
222
+ def sort_srv_answers(answers)
223
+ sorted_answers = []
224
+
225
+ # this is roughly based on the resolv-srv and supposedly mostly rfc2782 compliant
226
+ answers.each do |_, available|
227
+ total_weight = available.inject(0) {|a, e| a + e.weight + 1 }
228
+
229
+ until available.empty?
230
+ selector = Integer(rand * total_weight) + 1
231
+ selected_idx = available.find_index do |e|
232
+ selector -= e.weight + 1
233
+ selector <= 0
234
+ end
235
+ selected = available.delete_at(selected_idx)
236
+
237
+ total_weight -= selected.weight + 1
238
+
239
+ sorted_answers << selected
240
+ end
241
+ end
242
+
243
+ sorted_answers
244
+ end
245
+
246
+ # Create a Net::HTTP instance optionally set up with the Puppet certs
247
+ #
248
+ # If the client_private_key and client_public_cert both exist they will
249
+ # be used to validate the connection
250
+ #
251
+ # If the ca_path exist it will be used and full verification will be enabled
252
+ #
253
+ # @param server [Hash] as returned by {#try_srv}
254
+ # @param force_puppet_ssl [boolean] when true will call {#check_ssl_setup} and so force Puppet certs
255
+ # @return [Net::HTTP]
256
+ def https(server, force_puppet_ssl=false)
257
+ Log.debug("Creating new HTTPS connection to %s:%s" % [server[:target], server[:port]])
258
+
259
+ check_ssl_setup if force_puppet_ssl
260
+
261
+ http = Net::HTTP.new(server[:target], server[:port])
262
+
263
+ http.use_ssl = true
264
+
265
+ if has_client_private_key? && has_client_public_cert?
266
+ http.cert = OpenSSL::X509::Certificate.new(File.read(client_public_cert))
267
+ http.key = OpenSSL::PKey::RSA.new(File.read(client_private_key))
268
+ end
269
+
270
+ if has_ca?
271
+ http.ca_file = ca_path
272
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
273
+ else
274
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
275
+ end
276
+
277
+ http
278
+ end
279
+
280
+ # Creates a Net::HTTP::Get instance for a path that defaults to accepting JSON
281
+ #
282
+ # @param path [String]
283
+ # @return [Net::HTTP::Get]
284
+ def http_get(path, headers=nil)
285
+ headers ||= {}
286
+ headers = {
287
+ "Accept" => "application/json",
288
+ "User-Agent" => "Choria version %s http://choria.io" % VERSION
289
+ }.merge(headers)
290
+
291
+ Net::HTTP::Get.new(path, headers)
292
+ end
293
+
294
+ # Creates a Net::HTTP::Post instance for a path that defaults to accepting JSON
295
+ #
296
+ # @param path [String]
297
+ # @return [Net::HTTP::Post]
298
+ def http_post(path, headers=nil)
299
+ headers ||= {}
300
+ headers = {
301
+ "Accept" => "application/json",
302
+ "User-Agent" => "Choria version %s http://choria.io" % VERSION
303
+ }.merge(headers)
304
+
305
+ Net::HTTP::Post.new(path, headers)
306
+ end
307
+
308
+ # Does a proxied discovery request
309
+ #
310
+ # @param query [Hash] Discovery query as per pdbproxy standard
311
+ # @return [Array] JSON parsed result set
312
+ # @raise [StandardError] on any failures
313
+ def proxy_discovery_query(query)
314
+ transport = https(discovery_server, true)
315
+ request = http_get("/v1/discover")
316
+ request.body = query.to_json
317
+ request["Content-Type"] = "application/json"
318
+
319
+ resp, data = transport.request(request)
320
+
321
+ raise("Failed to make request to Discovery Proxy: %s: %s" % [resp.code, resp.body]) unless resp.code == "200"
322
+
323
+ result = JSON.parse(data || resp.body)
324
+
325
+ result["nodes"]
326
+ end
327
+
328
+ # Extract certnames from PQL results, deactivated nodes are ignored
329
+ #
330
+ # @param results [Array]
331
+ # @return [Array<String>] list of certnames
332
+ def pql_extract_certnames(results)
333
+ results.reject {|n| n["deactivated"]}.map {|n| n["certname"]}.compact
334
+ end
335
+
336
+ # Performs a PQL query against the configured PuppetDB
337
+ #
338
+ # @param query [String] PQL Query
339
+ # @param only_certnames [Boolean] extract certnames from the results
340
+ # @return [Array] JSON parsed result set
341
+ # @raise [StandardError] on any failures
342
+ def pql_query(query, only_certnames=false)
343
+ Log.debug("Performing PQL query: %s" % query)
344
+
345
+ path = "/pdb/query/v4?%s" % URI.encode_www_form("query" => query)
346
+
347
+ resp, data = https(puppetdb_server, true).request(http_get(path))
348
+
349
+ raise("Failed to make request to PuppetDB: %s: %s: %s" % [resp.code, resp.message, resp.body]) unless resp.code == "200"
350
+
351
+ result = JSON.parse(data || resp.body)
352
+
353
+ Log.debug("Found %d records for query %s" % [result.size, query])
354
+
355
+ only_certnames ? pql_extract_certnames(result) : result
356
+ end
357
+
358
+ # Checks if all the required SSL files exist
359
+ #
360
+ # @param log [Boolean] log warnings when true
361
+ # @return [Boolean]
362
+ def have_ssl_files?(log=true)
363
+ [client_public_cert, client_private_key, ca_path].map do |path|
364
+ Log.debug("Checking for SSL file %s" % path)
365
+
366
+ if File.exist?(path)
367
+ true
368
+ else
369
+ Log.warn("Cannot find SSL file %s" % path) if log
370
+ false
371
+ end
372
+ end.all?
373
+ end
374
+
375
+ # Validates a certificate against the CA
376
+ #
377
+ # @param pubcert [String] PEM encoded X509 public certificate
378
+ # @param name [String] name that should be present in the certificate
379
+ # @param log [Boolean] log warnings when true
380
+ # @return [String,false] when succesful, the certname else false
381
+ # @raise [StandardError] in case OpenSSL fails to open the various certificates
382
+ # @raise [OpenSSL::X509::CertificateError] if the CA is invalid
383
+ def valid_certificate?(pubcert, name, log=true)
384
+ return false unless name
385
+
386
+ raise("Cannot find or read the CA in %s, cannot verify public certificate" % ca_path) unless File.readable?(ca_path)
387
+
388
+ certs = parse_pubcert(pubcert, log)
389
+
390
+ return false if certs.empty?
391
+
392
+ incoming = certs.first
393
+
394
+ chain = certs[1..-1]
395
+
396
+ begin
397
+ ca = OpenSSL::X509::Store.new.add_file(ca_path)
398
+ rescue OpenSSL::X509::StoreError
399
+ Log.warn("Failed to load CA from %s: %s: %s" % [ca_path, $!.class, $!.to_s]) if log
400
+ raise
401
+ end
402
+
403
+ unless ca.verify(incoming, chain)
404
+ Log.warn("Failed to verify certificate %s against CA %s in %s" % [incoming.subject.to_s, incoming.issuer.to_s, ca_path]) if log
405
+
406
+ return false
407
+ end
408
+
409
+ Log.debug("Verified certificate %s against CA %s" % [incoming.subject.to_s, incoming.issuer.to_s]) if log
410
+
411
+ if !remote_signer_configured? && !OpenSSL::SSL.verify_certificate_identity(incoming, name)
412
+ raise("Could not parse certificate with subject %s as it has no CN part, or name %s invalid" % [incoming.subject.to_s, name])
413
+ end
414
+
415
+ name
416
+ end
417
+
418
+ # Determines if a remote signer is configured
419
+ #
420
+ # @return [Boolean]
421
+ def remote_signer_configured?
422
+ url = get_option("choria.security.request_signer.url", nil)
423
+
424
+ ![nil, ""].include?(url)
425
+ end
426
+
427
+ # Utility function to split a chained certificate String into an Array
428
+ #
429
+ # @param pemdata [String] PEM encoded certificate
430
+ # @return [Array<String,nil>]
431
+ def ssl_split_pem(pemdata)
432
+ # Chained certificates typically have the public certificate, along
433
+ # with every intermediate certificiate.
434
+ # OpenSSL will stop at the first certificate when using OpenSSL::X509::Certificate.new,
435
+ # so we need to separate them into a list
436
+ pemdata.scan(/-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m)
437
+ end
438
+
439
+ # Split a string containing chained certificates into an Array of OpenSSL::X509::Certificate.
440
+ #
441
+ # @param pemdata [String]
442
+ # @return [Array<OpenSSL::X509::Certificate,nil>]
443
+ def ssl_parse_chain(pemdata)
444
+ ssl_split_pem(pemdata).map do |cpem|
445
+ OpenSSL::X509::Certificate.new(cpem)
446
+ end
447
+ end
448
+
449
+ # Parses a public cert
450
+ #
451
+ # @param pubcert [String] PEM encoded public certificate
452
+ # @param log [Boolean] log warnings when true
453
+ # @return [Array<OpenSSL::X509::Certificate,nil>]
454
+ def parse_pubcert(pubcert, log=true)
455
+ ssl_parse_chain(pubcert)
456
+ rescue OpenSSL::X509::CertificateError
457
+ Log.warn("Received certificate is not a valid x509 certificate: %s: %s" % [$!.class, $!.to_s]) if log
458
+ nil
459
+ end
460
+
461
+ # The callerid for the current client
462
+ #
463
+ # @return [String]
464
+ # @raise [Exception] when remote JWT is invalid
465
+ def callerid
466
+ PluginManager["security_plugin"].callerid
467
+ end
468
+
469
+ # Checks all the required SSL files exist
470
+ #
471
+ # @param log [Boolean] log warnings when true
472
+ # @return [Boolean]
473
+ # @raise [StandardError] on failure
474
+ def check_ssl_setup(log=true)
475
+ return true if $choria_unsafe_disable_protocol_security # rubocop:disable Style/GlobalVars
476
+ return true if anon_tls?
477
+
478
+ raise(UserError, "The Choria client cannot be run as root") if Process.uid == 0 && PluginManager["security_plugin"].initiated_by == :client
479
+
480
+ raise(UserError, "Not all required SSL files exist") unless have_ssl_files?(log)
481
+
482
+ embedded_certname = nil
483
+
484
+ begin
485
+ embedded_certname = valid_certificate?(File.read(client_public_cert), certname)
486
+ rescue
487
+ raise(UserError, "The public certificate was not signed by the configured CA")
488
+ end
489
+
490
+ unless embedded_certname == certname
491
+ raise(UserError, "The certname %s found in %s does not match the configured certname of %s" % [embedded_certname, client_public_cert, certname])
492
+ end
493
+
494
+ true
495
+ end
496
+
497
+ # Resolves server lists based on config and SRV records
498
+ #
499
+ # Attempts to find server in the following order:
500
+ #
501
+ # * Configured hosts in `config_option`
502
+ # * SRV lookups of `srv_records`
503
+ # * Defaults
504
+ # * nil otherwise
505
+ #
506
+ # @param config_option [String] config to lookup
507
+ # @param srv_records [Array<String>] list of SRV records to query
508
+ # @param default_host [String] host to use when not found
509
+ # @param default_port [String] port to use when not found
510
+ # @return [Array, nil] groups of host and port pairs
511
+ def server_resolver(config_option, srv_records, default_host=nil, default_port=nil)
512
+ if servers = get_option(config_option, nil)
513
+ hosts = servers.split(",").map do |server|
514
+ server.split(":").map(&:strip)
515
+ end
516
+
517
+ return hosts
518
+ end
519
+
520
+ srv_answers = query_srv_records(srv_records)
521
+
522
+ unless srv_answers.empty?
523
+ hosts = srv_answers.map do |answer|
524
+ [answer[:target], answer[:port]]
525
+ end
526
+
527
+ return hosts
528
+ end
529
+
530
+ [[default_host, default_port]] if default_host && default_port
531
+ end
532
+
533
+ # Finds the middleware hosts in config or DNS
534
+ #
535
+ # Attempts to find servers in the following order:
536
+ #
537
+ # * connects.ngs.global if configured to be ngs and empty choria.middleware_hosts
538
+ # * Any federation servers if in a federation
539
+ # * Configured hosts in choria.middleware_hosts
540
+ # * SRV lookups in _mcollective-server._tcp and _x-puppet-mcollective._tcp
541
+ # * Supplied defaults
542
+ #
543
+ # Eventually it's intended that other middleware might be supported
544
+ # this would provide a single way to configure them all
545
+ #
546
+ # @param default_host [String] default hostname
547
+ # @param default_port [String] default port
548
+ # @return [Array<Array<String, String>>] groups of host and port
549
+ def middleware_servers(default_host="puppet", default_port="4222")
550
+ return [["connect.ngs.global", "4222"]] if ngs? && !has_option?("choria.middleware_hosts")
551
+
552
+ if federated? && federation = federation_middleware_servers
553
+ return federation
554
+ end
555
+
556
+ server_resolver("choria.middleware_hosts", ["_mcollective-server._tcp", "_x-puppet-mcollective._tcp"], default_host, default_port)
557
+ end
558
+
559
+ # Looks for federation middleware servers when federated
560
+ #
561
+ # Attempts to find servers in the following order:
562
+ #
563
+ # * Configured hosts in choria.federation_middleware_hosts
564
+ # * SRV lookups in _mcollective-federation_server._tcp and _x-puppet-mcollective_federation._tcp
565
+ #
566
+ # @note you'd still want to only get your middleware servers from {#middleware_servers}
567
+ # @return [Array,nil] groups of host and port, nil when not found
568
+ def federation_middleware_servers
569
+ server_resolver("choria.federation_middleware_hosts", ["_mcollective-federation_server._tcp", "_x-puppet-mcollective_federation._tcp"])
570
+ end
571
+
572
+ # Determines if servers should be randomized
573
+ #
574
+ # @return [Boolean]
575
+ def randomize_middleware_servers?
576
+ Util.str_to_bool(get_option("choria.randomize_middleware_hosts", "true"))
577
+ end
578
+
579
+ # Attempts to look up some SRV records falling back to defaults
580
+ #
581
+ # When given a array of multiple names it will try each name individually
582
+ # and check if it resolved to a answer, if it did it will use that one.
583
+ # Else it will move to the next. In this way you can prioritise one
584
+ # record over another like puppetdb over puppet and faill back to defaults.
585
+ #
586
+ # This is a pretty naive implementation that right now just returns
587
+ # the first result, the correct behaviour needs to be determined but
588
+ # for now this gets us going with easily iterable code.
589
+ #
590
+ # These names are mainly being used by {#https} so in theory it would
591
+ # be quite easy to support multiple results with fall back etc, but
592
+ # I am not really sure what would be the best behaviour here
593
+ #
594
+ # @param names [Array<String>, String] list of names to lookup without the domain
595
+ # @param default_target [String] default for the returned :target
596
+ # @param default_port [String] default for the returned :port
597
+ # @return [Hash] with :target and :port
598
+ def try_srv(names, default_target, default_port)
599
+ srv_answers = Array(names).map do |name|
600
+ answer = query_srv_records([name])
601
+
602
+ answer.empty? ? nil : answer
603
+ end.compact.flatten
604
+
605
+ if srv_answers.empty?
606
+ {:target => default_target, :port => default_port}
607
+ else
608
+ {:target => srv_answers[0][:target].to_s, :port => srv_answers[0][:port]}
609
+ end
610
+ end
611
+
612
+ # The Puppet server to connect to
613
+ #
614
+ # Will consult SRV records for _x-puppet._tcp.example.net first then
615
+ # configurable using choria.puppetserver_host and choria.puppetserver_port
616
+ # defaults to puppet:8140.
617
+ #
618
+ # @return [Hash] with :target and :port
619
+ def puppet_server
620
+ d_host = get_option("choria.puppetserver_host", "puppet")
621
+ d_port = get_option("choria.puppetserver_port", "8140")
622
+
623
+ try_srv(["_x-puppet._tcp"], d_host, d_port)
624
+ end
625
+
626
+ # The Puppet server to connect to
627
+ #
628
+ # Will consult _x-puppet-ca._tcp.example.net then _x-puppet._tcp.example.net
629
+ # then configurable using choria.puppetca_host, defaults to puppet:8140
630
+ #
631
+ # @return [Hash] with :target and :port
632
+ def puppetca_server
633
+ d_port = get_option("choria.puppetca_port", "8140")
634
+
635
+ if @ca
636
+ {:target => @ca, :port => d_port}
637
+ else
638
+ d_host = get_option("choria.puppetca_host", "puppet")
639
+ try_srv(["_x-puppet-ca._tcp", "_x-puppet._tcp"], d_host, d_port)
640
+ end
641
+ end
642
+
643
+ # The PuppetDB server to connect to
644
+ #
645
+ # Will consult _x-puppet-db._tcp.example.net then _x-puppet._tcp.example.net
646
+ # then configurable using choria.puppetdb_host and choria.puppetdb_port, defaults
647
+ # to puppet:8081
648
+ #
649
+ # @return [Hash] with :target and :port
650
+ def puppetdb_server
651
+ d_host = get_option("choria.puppetdb_host", "puppet")
652
+ d_port = get_option("choria.puppetdb_port", "8081")
653
+
654
+ answer = try_srv(["_x-puppet-db._tcp"], nil, nil)
655
+ return answer if answer[:target]
656
+
657
+ # In the case where we take _x-puppet._tcp SRV records we unfortunately have
658
+ # to force the port else it uses the one from Puppet which will 404
659
+ answer = try_srv(["_x-puppet._tcp"], d_host, d_port)
660
+ answer[:port] = d_port
661
+
662
+ answer
663
+ end
664
+
665
+ # Looks for discovery proxy servers
666
+ #
667
+ # Attempts to find servers in the following order:
668
+ #
669
+ # * If choria.discovery_proxy is set to false, returns nil
670
+ # * Configured hosts in choria.discovery_proxies
671
+ # * SRV lookups in _mcollective-discovery._tcp
672
+ #
673
+ # @return [Hash] with :target and :port
674
+ def discovery_server
675
+ return unless proxied_discovery?
676
+
677
+ d_host = get_option("choria.discovery_host", "puppet")
678
+ d_port = get_option("choria.discovery_port", "8085")
679
+
680
+ try_srv(["_mcollective-discovery._tcp"], d_host, d_port)
681
+ end
682
+
683
+ # Determines if this is using a discovery proxy
684
+ #
685
+ # @return [Boolean]
686
+ def proxied_discovery?
687
+ has_option?("choria.discovery_host") || has_option?("choria.discovery_port") || Util.str_to_bool(get_option("choria.discovery_proxy", "false"))
688
+ end
689
+
690
+ # The certname of the current context
691
+ #
692
+ # In the case of root that would be the configured `identity`
693
+ # for non root it would a string made up of the current username
694
+ # as determined by the USER environment variable or the configured
695
+ # `identity`
696
+ #
697
+ # At present windows clients are probably not supported automatically
698
+ # as they will default to the certificate based on identity. Same
699
+ # as root. Windows will have to rely on the environment override
700
+ # until we can figure out what the best behaviour is
701
+ #
702
+ # In all cases the certname can be overridden using the `MCOLLECTIVE_CERTNAME`
703
+ # environment variable
704
+ #
705
+ # @return [String]
706
+ def certname
707
+ if Process.uid == 0 || Util.windows?
708
+ certname = @config.identity
709
+ else
710
+ certname = "%s.mcollective" % [env_fetch("USER", @config.identity)]
711
+ end
712
+
713
+ env_fetch("MCOLLECTIVE_CERTNAME", certname)
714
+ end
715
+
716
+ # Initialises Puppet if needed and retrieve a config setting
717
+ #
718
+ # @param setting [Symbol] a Puppet setting name
719
+ # @return [String]
720
+ def puppet_setting(setting)
721
+ require "puppet"
722
+
723
+ unless Puppet.settings.app_defaults_initialized?
724
+ Puppet.settings.preferred_run_mode = :agent
725
+
726
+ Puppet.settings.initialize_global_settings([])
727
+ Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(Puppet.run_mode))
728
+ Puppet.push_context(Puppet.base_context(Puppet.settings))
729
+ end
730
+
731
+ Puppet.settings[setting]
732
+ end
733
+
734
+ # Creates a SSL Context which includes the AIO SSL files
735
+ #
736
+ # @return [OpenSSL::SSL::SSLContext]
737
+ def ssl_context
738
+ context = OpenSSL::SSL::SSLContext.new
739
+ context.ca_file = ca_path
740
+ context.ssl_version = :TLSv1_2 # rubocop:disable Naming/VariableNumber
741
+
742
+ if anon_tls?
743
+ context.verify_mode = OpenSSL::SSL::VERIFY_NONE
744
+ return context
745
+ end
746
+
747
+ public_cert = File.read(client_public_cert)
748
+ private_key = File.read(client_private_key)
749
+
750
+ cert_chain = ssl_parse_chain(public_cert)
751
+
752
+ cert = cert_chain.first
753
+ key = OpenSSL::PKey::RSA.new(private_key)
754
+
755
+ extra_chain_cert = cert_chain[1..-1]
756
+
757
+ if OpenSSL::SSL::SSLContext.method_defined?(:add_certificate)
758
+ context.add_certificate(cert, key, extra_chain_cert)
759
+ else
760
+ context.cert = OpenSSL::X509::Certificate.new(File.read(client_public_cert))
761
+ context.key = OpenSSL::PKey::RSA.new(File.read(client_private_key))
762
+ context.extra_chain_cert = extra_chain_cert
763
+ end
764
+
765
+ context.verify_mode = OpenSSL::SSL::VERIFY_PEER
766
+
767
+ context
768
+ end
769
+
770
+ # The directory where SSL related files live
771
+ #
772
+ # This is configurable using choria.ssldir which should be a
773
+ # path expandable using File.expand_path
774
+ #
775
+ # On Windows or when running as root Puppet settings will be consulted
776
+ # but when running as a normal user it will default to the AIO path
777
+ # when not configured
778
+ #
779
+ # @return [String]
780
+ def ssl_dir
781
+ @_ssl_dir ||= if has_option?("choria.ssldir")
782
+ File.expand_path(get_option("choria.ssldir"))
783
+ elsif Util.windows? || Process.uid == 0
784
+ puppet_setting(:ssldir)
785
+ else
786
+ File.expand_path("~/.puppetlabs/etc/puppet/ssl")
787
+ end
788
+ end
789
+
790
+ # Determines the security provider
791
+ def security_provider
792
+ get_option("security.provider", "puppet")
793
+ end
794
+
795
+ # Determines if the file security provider is enabled
796
+ def file_security?
797
+ security_provider == "file"
798
+ end
799
+
800
+ # Determines if the puppet security provider is enabled
801
+ def puppet_security?
802
+ security_provider == "puppet"
803
+ end
804
+
805
+ # Expands full paths with special handling for empty string
806
+ #
807
+ # File.expand_path will expand `""` to cwd, this is not good for
808
+ # what we need in many cases so this returns `""` in that case
809
+ #
810
+ # @param path [String] the unexpanded path
811
+ # @return [String] `""` when empty string was given
812
+ def expand_path(path)
813
+ return "" if path == ""
814
+
815
+ File.expand_path(path)
816
+ end
817
+
818
+ # The path to a client public certificate
819
+ #
820
+ # @note paths determined by Puppet AIO packages
821
+ # @return [String]
822
+ def client_public_cert
823
+ return expand_path(get_option("security.file.certificate", "")) if file_security?
824
+
825
+ File.join(ssl_dir, "certs", "%s.pem" % certname)
826
+ end
827
+
828
+ # Determines if teh client_public_cert exist
829
+ #
830
+ # @return [Boolean]
831
+ def has_client_public_cert?
832
+ File.exist?(client_public_cert)
833
+ end
834
+
835
+ # The path to a client private key
836
+ #
837
+ # @note paths determined by Puppet AIO packages
838
+ # @return [String]
839
+ def client_private_key
840
+ return expand_path(get_option("security.file.key", "")) if file_security?
841
+
842
+ File.join(ssl_dir, "private_keys", "%s.pem" % certname)
843
+ end
844
+
845
+ # Determines if the client_private_key exist
846
+ #
847
+ # @return [Boolean]
848
+ def has_client_private_key?
849
+ File.exist?(client_private_key)
850
+ end
851
+
852
+ # The path to the CA
853
+ #
854
+ # @return [String]
855
+ def ca_path
856
+ return expand_path(get_option("security.file.ca", "")) if file_security?
857
+
858
+ File.join(ssl_dir, "certs", "ca.pem")
859
+ end
860
+
861
+ # Determines if Choria is configured for anonymous TLS mode
862
+ #
863
+ # @return [Boolean]
864
+ def anon_tls?
865
+ remote_signer_configured? && Util.str_to_bool(get_option("security.client_anon_tls", "false"))
866
+ end
867
+
868
+ # Determines if the CA exist
869
+ #
870
+ # @return [Boolean]
871
+ def has_ca?
872
+ File.exist?(ca_path)
873
+ end
874
+
875
+ # The path to a CSR for this user
876
+ #
877
+ # @return [String]
878
+ def csr_path
879
+ return "" if file_security?
880
+
881
+ File.join(ssl_dir, "certificate_requests", "%s.pem" % certname)
882
+ end
883
+
884
+ # Determines if the CSR exist
885
+ #
886
+ # @return [Boolean]
887
+ def has_csr?
888
+ File.exist?(csr_path)
889
+ end
890
+
891
+ # The formatted string representation of the CSR fingerprint
892
+ #
893
+ # @return [String]
894
+ def csr_fingerprint
895
+ require "puppet"
896
+ csr = OpenSSL::X509::Request.new(File.read(csr_path))
897
+ Puppet::SSL::Digest.new(nil, csr.to_der)
898
+ end
899
+
900
+ # Searches the PATH for an executable command
901
+ #
902
+ # @param command [String] a command to search for
903
+ # @return [String,nil] the path to the command or nil
904
+ def which(command)
905
+ exts = Array(env_fetch("PATHEXT", "").split(";"))
906
+ exts << "" if exts.empty?
907
+
908
+ env_fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
909
+ exts.each do |ext|
910
+ exe = File.join(path, "%s%s" % [command, ext])
911
+ return exe if File.executable?(exe) && !File.directory?(exe)
912
+ end
913
+ end
914
+
915
+ nil
916
+ end
917
+
918
+ # Searches the machine for a working facter
919
+ #
920
+ # It checks AIO path first and then attempts to find it in PATH and supports both
921
+ # unix and windows
922
+ #
923
+ # @return [String,nil]
924
+ def facter_cmd
925
+ return "/opt/puppetlabs/bin/facter" if File.executable?("/opt/puppetlabs/bin/facter")
926
+
927
+ which("facter")
928
+ end
929
+
930
+ # Creates any missing SSL directories
931
+ #
932
+ # This prepares a Puppet like SSL tree in case Puppet
933
+ # has not been initialized yet
934
+ #
935
+ # @return [void]
936
+ def make_ssl_dirs
937
+ return if file_security?
938
+
939
+ FileUtils.mkdir_p(ssl_dir, :mode => 0o0771)
940
+
941
+ ["certificate_requests", "certs", "public_keys"].each do |dir|
942
+ FileUtils.mkdir_p(File.join(ssl_dir, dir), :mode => 0o0755)
943
+ end
944
+
945
+ ["private_keys", "private"].each do |dir|
946
+ FileUtils.mkdir_p(File.join(ssl_dir, dir), :mode => 0o0750)
947
+ end
948
+ end
949
+
950
+ # Creates a RSA key of a certain strenth
951
+ #
952
+ # @return [OpenSSL::PKey::RSA]
953
+ def create_rsa_key(bits)
954
+ OpenSSL::PKey::RSA.new(bits)
955
+ end
956
+
957
+ # Writes a new 4096 bit key in the puppet default locatioj
958
+ #
959
+ # @return [OpenSSL::PKey::RSA]
960
+ # @raise [StandardError] when the key already exist
961
+ def write_key
962
+ raise("Refusing to overwrite existing key in %s" % client_private_key) if has_client_private_key?
963
+
964
+ key = create_rsa_key(4096)
965
+ File.open(client_private_key, "w", 0o0640) {|f| f.write(key.to_pem)}
966
+
967
+ key
968
+ end
969
+
970
+ # Creates a basic CSR
971
+ #
972
+ # @return [OpenSSL::X509::Request] signed CSR
973
+ def create_csr(comonname, orgunit, key)
974
+ csr = OpenSSL::X509::Request.new
975
+ csr.version = 0
976
+ csr.public_key = key.public_key
977
+ csr.subject = OpenSSL::X509::Name.new(
978
+ [
979
+ ["CN", comonname, OpenSSL::ASN1::UTF8STRING],
980
+ ["OU", orgunit, OpenSSL::ASN1::UTF8STRING]
981
+ ]
982
+ )
983
+ csr.sign(key, OpenSSL::Digest.new("SHA1"))
984
+
985
+ csr
986
+ end
987
+
988
+ # Creates a new CSR signed by the given key
989
+ #
990
+ # @param key [OpenSSL::PKey::RSA]
991
+ # @return [String] PEM encoded CSR
992
+ def write_csr(key)
993
+ raise("Refusing to overwrite existing CSR in %s" % csr_path) if has_csr?
994
+
995
+ csr = create_csr(certname, "mcollective", key)
996
+
997
+ File.open(csr_path, "w", 0o0644) {|f| f.write(csr.to_pem)}
998
+
999
+ csr.to_pem
1000
+ end
1001
+
1002
+ # Fetch and save the CA from Puppet
1003
+ #
1004
+ # @return [Boolean]
1005
+ def fetch_ca
1006
+ return true if has_ca?
1007
+
1008
+ server = puppetca_server
1009
+
1010
+ req = http_get("/puppet-ca/v1/certificate/ca?environment=production", "Accept" => "text/plain")
1011
+ resp, _ = https(server).request(req)
1012
+
1013
+ if resp.code == "200"
1014
+ File.open(ca_path, "w", 0o0644) {|f| f.write(resp.body)}
1015
+ else
1016
+ raise(UserError, "Failed to fetch CA from %s:%s: %s: %s" % [server[:target], server[:port], resp.code, resp.message])
1017
+ end
1018
+
1019
+ has_ca?
1020
+ end
1021
+
1022
+ # Requests a certificate from the Puppet CA
1023
+ #
1024
+ # This will attempt to create a new key, write a CSR and
1025
+ # then sends it to the CA for signing
1026
+ #
1027
+ # @return [Boolean]
1028
+ # @raise [UserError] when requesting the cert fails
1029
+ def request_cert
1030
+ key = write_key
1031
+ csr = write_csr(key)
1032
+
1033
+ server = puppetca_server
1034
+
1035
+ req = Net::HTTP::Put.new("/puppet-ca/v1/certificate_request/%s?environment=production" % certname, "Content-Type" => "text/plain")
1036
+ req.body = csr
1037
+ resp, _ = https(server).request(req)
1038
+
1039
+ if resp.code == "200"
1040
+ true
1041
+ else
1042
+ raise(UserError, "Failed to request certificate from %s:%s: %s: %s: %s" % [server[:target], server[:port], resp.code, resp.message, resp.body])
1043
+ end
1044
+ end
1045
+
1046
+ # Attempts to fetch a cert from the CA
1047
+ #
1048
+ # @return [Boolean]
1049
+ def attempt_fetch_cert
1050
+ return true if has_client_public_cert?
1051
+
1052
+ req = http_get("/puppet-ca/v1/certificate/%s?environment=production" % certname, "Accept" => "text/plain")
1053
+ resp, _ = https(puppetca_server).request(req)
1054
+
1055
+ if resp.code == "200"
1056
+ File.open(client_public_cert, "w", 0o0644) {|f| f.write(resp.body)}
1057
+ true
1058
+ else
1059
+ false
1060
+ end
1061
+ end
1062
+
1063
+ # Determines if a CSR has been sent but not yet retrieved
1064
+ #
1065
+ # @return [Boolean]
1066
+ def waiting_for_cert?
1067
+ !has_client_public_cert? && has_client_private_key?
1068
+ end
1069
+
1070
+ # Gets a config option
1071
+ #
1072
+ # @param opt [String] config option to look up
1073
+ # @param default [Object] default to return when not found
1074
+ # @return [Object, Proc] the found data or default. When it's a proc the proc will be called only when needed
1075
+ # @raise [StandardError] when no default is given and option is not found
1076
+ def get_option(opt, default=:_unset)
1077
+ return @config.pluginconf[opt] if has_option?(opt)
1078
+
1079
+ unless default == :_unset
1080
+ if default.is_a?(Proc)
1081
+ return default.call
1082
+ else
1083
+ return default
1084
+ end
1085
+ end
1086
+
1087
+ raise(UserError, "No plugin.%s configuration option given" % opt)
1088
+ end
1089
+
1090
+ # Determines if a config option is set
1091
+ #
1092
+ # @param opt [String] config option to look up
1093
+ # @return [Boolean]
1094
+ def has_option?(opt)
1095
+ @config.pluginconf.include?(opt)
1096
+ end
1097
+
1098
+ def env_fetch(key, default=nil)
1099
+ ENV.fetch(key, default)
1100
+ end
1101
+ end
1102
+ end
1103
+ end