Class: MCollective::Util::Choria

Inherits:
Object
  • Object
show all
Defined in:
lib/mcollective/util/choria.rb

Defined Under Namespace

Classes: Abort, UserError

Constant Summary collapse

VERSION =
"0.19.0".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(check_ssl = true) ⇒ Choria

Returns a new instance of Choria.



17
18
19
20
21
# File 'lib/mcollective/util/choria.rb', line 17

def initialize(check_ssl=true)
  @config = Config.instance

  check_ssl_setup if check_ssl
end

Instance Attribute Details

#ca=(value) ⇒ Object (writeonly)

Sets the attribute ca

Parameters:

  • value

    the value to set the attribute ca to.



15
16
17
# File 'lib/mcollective/util/choria.rb', line 15

def ca=(value)
  @ca = value
end

Instance Method Details

#anon_tls?Boolean

Determines if Choria is configured for anonymous TLS mode

Returns:

  • (Boolean)


863
864
865
# File 'lib/mcollective/util/choria.rb', line 863

def anon_tls?
  remote_signer_configured? && Util.str_to_bool(get_option("security.client_anon_tls", "false"))
end

#ca_pathString

The path to the CA

Returns:



854
855
856
857
858
# File 'lib/mcollective/util/choria.rb', line 854

def ca_path
  return expand_path(get_option("security.file.ca", "")) if file_security?

  File.join(ssl_dir, "certs", "ca.pem")
end

#calleridString

The callerid for the current client

Returns:

Raises:

  • (Exception)

    when remote JWT is invalid



458
459
460
# File 'lib/mcollective/util/choria.rb', line 458

def callerid
  PluginManager["security_plugin"].callerid
end

#certnameString

The certname of the current context

In the case of root that would be the configured ‘identity` for non root it would a string made up of the current username as determined by the USER environment variable or the configured `identity`

At present windows clients are probably not supported automatically as they will default to the certificate based on identity. Same as root. Windows will have to rely on the environment override until we can figure out what the best behaviour is

In all cases the certname can be overridden using the ‘MCOLLECTIVE_CERTNAME` environment variable

Returns:



705
706
707
708
709
710
711
712
713
# File 'lib/mcollective/util/choria.rb', line 705

def certname
  if Process.uid == 0 || Util.windows?
    certname = @config.identity
  else
    certname = "%s.mcollective" % [env_fetch("USER", @config.identity)]
  end

  env_fetch("MCOLLECTIVE_CERTNAME", certname)
end

#check_ssl_setup(log = true) ⇒ Boolean

Checks all the required SSL files exist

Parameters:

  • log (Boolean) (defaults to: true)

    log warnings when true

Returns:

  • (Boolean)

Raises:

  • (StandardError)

    on failure



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/mcollective/util/choria.rb', line 467

def check_ssl_setup(log=true)
  return true if $choria_unsafe_disable_protocol_security # rubocop:disable Style/GlobalVars
  return true if anon_tls?

  raise(UserError, "The Choria client cannot be run as root") if Process.uid == 0 && PluginManager["security_plugin"].initiated_by == :client

  raise(UserError, "Not all required SSL files exist") unless have_ssl_files?(log)

  embedded_certname = nil

  begin
    embedded_certname = valid_certificate?(File.read(client_public_cert), certname)
  rescue
    raise(UserError, "The public certificate was not signed by the configured CA")
  end

  unless embedded_certname == certname
    raise(UserError, "The certname %s found in %s does not match the configured certname of %s" % [embedded_certname, client_public_cert, certname])
  end

  true
end

#client_private_keyString

Note:

paths determined by Puppet AIO packages

The path to a client private key

Returns:



838
839
840
841
842
# File 'lib/mcollective/util/choria.rb', line 838

def client_private_key
  return expand_path(get_option("security.file.key", "")) if file_security?

  File.join(ssl_dir, "private_keys", "%s.pem" % certname)
end

#client_public_certString

Note:

paths determined by Puppet AIO packages

The path to a client public certificate

Returns:



821
822
823
824
825
# File 'lib/mcollective/util/choria.rb', line 821

def client_public_cert
  return expand_path(get_option("security.file.certificate", "")) if file_security?

  File.join(ssl_dir, "certs", "%s.pem" % certname)
end

#credential_fileString

Determines the configured path to the NATS credentials, empty when not set

Returns:



26
27
28
# File 'lib/mcollective/util/choria.rb', line 26

def credential_file
  get_option("nats.credentials", "")
end

#credential_file?Boolean

Determines if a credential file is configured

Returns:

  • (Boolean)


33
34
35
# File 'lib/mcollective/util/choria.rb', line 33

def credential_file?
  credential_file != ""
end

#csr_pathString

The path to a CSR for this user

Returns:



877
878
879
880
881
# File 'lib/mcollective/util/choria.rb', line 877

def csr_path
  return "" if file_security?

  File.join(ssl_dir, "certificate_requests", "%s.pem" % certname)
end

#discovery_serverHash

Looks for discovery proxy servers

Attempts to find servers in the following order:

* If choria.discovery_proxy is set to false, returns nil
* Configured hosts in choria.discovery_proxies
* SRV lookups in _mcollective-discovery._tcp

Returns:

  • (Hash)

    with :target and :port



673
674
675
676
677
678
679
680
# File 'lib/mcollective/util/choria.rb', line 673

def discovery_server
  return unless proxied_discovery?

  d_host = get_option("choria.discovery_host", "puppet")
  d_port = get_option("choria.discovery_port", "8085")

  try_srv(["_mcollective-discovery._tcp"], d_host, d_port)
end

#env_fetch(key, default = nil) ⇒ Object



948
949
950
# File 'lib/mcollective/util/choria.rb', line 948

def env_fetch(key, default=nil)
  ENV.fetch(key, default)
end

#expand_path(path) ⇒ String

Expands full paths with special handling for empty string

File.expand_path will expand ‘“”` to cwd, this is not good for what we need in many cases so this returns `“”` in that case

Parameters:

  • path (String)

    the unexpanded path

Returns:

  • (String)

    ‘“”` when empty string was given



811
812
813
814
815
# File 'lib/mcollective/util/choria.rb', line 811

def expand_path(path)
  return "" if path == ""

  File.expand_path(path)
end

#facter_cmdString?

Searches the machine for a working facter

It checks AIO path first and then attempts to find it in PATH and supports both unix and windows

Returns:



914
915
916
917
918
# File 'lib/mcollective/util/choria.rb', line 914

def facter_cmd
  return "/opt/puppetlabs/bin/facter" if File.executable?("/opt/puppetlabs/bin/facter")

  which("facter")
end

#facter_domainString?

Retrieves the domain from facter networking.domain if facter is found

Potentially we could use the local facts in mcollective but that’s a chicken and egg and sometimes its only set after initial connection if something like a cron job generates the yaml cache file

Returns:



122
123
124
125
126
# File 'lib/mcollective/util/choria.rb', line 122

def facter_domain
  if path = facter_cmd
    `"#{path}" networking.domain`.chomp
  end
end

#federated?Boolean

Determines if there are any federations configured

Returns:

  • (Boolean)


92
93
94
# File 'lib/mcollective/util/choria.rb', line 92

def federated?
  !federation_collectives.empty?
end

#federation_collectivesArray<String>

List of active collectives that form the federation

Returns:



99
100
101
102
103
104
105
# File 'lib/mcollective/util/choria.rb', line 99

def federation_collectives
  if override_networks = env_fetch("CHORIA_FED_COLLECTIVE", nil)
    override_networks.split(",").map(&:strip).reject(&:empty?)
  else
    get_option("choria.federation.collectives", "").split(",").map(&:strip).reject(&:empty?)
  end
end

#federation_middleware_serversArray?

Note:

you’d still want to only get your middleware servers from #middleware_servers

Looks for federation middleware servers when federated

Attempts to find servers in the following order:

* Configured hosts in choria.federation_middleware_hosts
* SRV lookups in _mcollective-federation_server._tcp and _x-puppet-mcollective_federation._tcp

Returns:

  • (Array, nil)

    groups of host and port, nil when not found



561
562
563
# File 'lib/mcollective/util/choria.rb', line 561

def federation_middleware_servers
  server_resolver("choria.federation_middleware_hosts", ["_mcollective-federation_server._tcp", "_x-puppet-mcollective_federation._tcp"])
end

#file_security?Boolean

Determines if the file security provider is enabled

Returns:

  • (Boolean)


795
796
797
# File 'lib/mcollective/util/choria.rb', line 795

def file_security?
  security_provider == "file"
end

#get_option(opt, default = :_unset) ⇒ Object, Proc

Gets a config option

Parameters:

  • opt (String)

    config option to look up

  • default (Object) (defaults to: :_unset)

    default to return when not found

Returns:

  • (Object, Proc)

    the found data or default. When it’s a proc the proc will be called only when needed

Raises:

  • (StandardError)

    when no default is given and option is not found



926
927
928
929
930
931
932
933
934
935
936
937
938
# File 'lib/mcollective/util/choria.rb', line 926

def get_option(opt, default=:_unset)
  return @config.pluginconf[opt] if has_option?(opt)

  unless default == :_unset
    if default.is_a?(Proc)
      return default.call
    else
      return default
    end
  end

  raise(UserError, "No plugin.%s configuration option given" % opt)
end

#has_ca?Boolean

Determines if the CA exist

Returns:

  • (Boolean)


870
871
872
# File 'lib/mcollective/util/choria.rb', line 870

def has_ca?
  File.exist?(ca_path)
end

#has_client_private_key?Boolean

Determines if the client_private_key exist

Returns:

  • (Boolean)


847
848
849
# File 'lib/mcollective/util/choria.rb', line 847

def has_client_private_key?
  File.exist?(client_private_key)
end

#has_client_public_cert?Boolean

Determines if teh client_public_cert exist

Returns:

  • (Boolean)


830
831
832
# File 'lib/mcollective/util/choria.rb', line 830

def has_client_public_cert?
  File.exist?(client_public_cert)
end

#has_csr?Boolean

Determines if the CSR exist

Returns:

  • (Boolean)


886
887
888
# File 'lib/mcollective/util/choria.rb', line 886

def has_csr?
  File.exist?(csr_path)
end

#has_option?(opt) ⇒ Boolean

Determines if a config option is set

Parameters:

  • opt (String)

    config option to look up

Returns:

  • (Boolean)


944
945
946
# File 'lib/mcollective/util/choria.rb', line 944

def has_option?(opt)
  @config.pluginconf.include?(opt)
end

#have_ssl_files?(log = true) ⇒ Boolean

Checks if all the required SSL files exist

Parameters:

  • log (Boolean) (defaults to: true)

    log warnings when true

Returns:

  • (Boolean)


355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/mcollective/util/choria.rb', line 355

def have_ssl_files?(log=true)
  [client_public_cert, client_private_key, ca_path].map do |path|
    Log.debug("Checking for SSL file %s" % path)

    if File.exist?(path)
      true
    else
      Log.warn("Cannot find SSL file %s" % path) if log
      false
    end
  end.all?
end

#http_get(path, headers = nil) ⇒ Net::HTTP::Get

Creates a Net::HTTP::Get instance for a path that defaults to accepting JSON

Parameters:

Returns:

  • (Net::HTTP::Get)


277
278
279
280
281
282
283
284
285
# File 'lib/mcollective/util/choria.rb', line 277

def http_get(path, headers=nil)
  headers ||= {}
  headers = {
    "Accept" => "application/json",
    "User-Agent" => "Choria version %s http://choria.io" % VERSION
  }.merge(headers)

  Net::HTTP::Get.new(path, headers)
end

#http_post(path, headers = nil) ⇒ Net::HTTP::Post

Creates a Net::HTTP::Post instance for a path that defaults to accepting JSON

Parameters:

Returns:

  • (Net::HTTP::Post)


291
292
293
294
295
296
297
298
299
# File 'lib/mcollective/util/choria.rb', line 291

def http_post(path, headers=nil)
  headers ||= {}
  headers = {
    "Accept" => "application/json",
    "User-Agent" => "Choria version %s http://choria.io" % VERSION
  }.merge(headers)

  Net::HTTP::Post.new(path, headers)
end

#https(server, force_puppet_ssl = false) ⇒ Net::HTTP

Create a Net::HTTP instance optionally set up with the Puppet certs

If the client_private_key and client_public_cert both exist they will be used to validate the connection

If the ca_path exist it will be used and full verification will be enabled

Parameters:

  • server (Hash)

    as returned by #try_srv

  • force_puppet_ssl (boolean) (defaults to: false)

    when true will call #check_ssl_setup and so force Puppet certs

Returns:

  • (Net::HTTP)


249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/mcollective/util/choria.rb', line 249

def https(server, force_puppet_ssl=false)
  Log.debug("Creating new HTTPS connection to %s:%s" % [server[:target], server[:port]])

  check_ssl_setup if force_puppet_ssl

  http = Net::HTTP.new(server[:target], server[:port])

  http.use_ssl = true

  if has_client_private_key? && has_client_public_cert?
    http.cert = OpenSSL::X509::Certificate.new(File.read(client_public_cert))
    http.key = OpenSSL::PKey::RSA.new(File.read(client_private_key))
  end

  if has_ca?
    http.ca_file = ca_path
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  else
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end

  http
end

#middleware_servers(default_host = "puppet", default_port = "4222") ⇒ Array<Array<String, String>>

Finds the middleware hosts in config or DNS

Attempts to find servers in the following order:

* connects.ngs.global if configured to be ngs and empty choria.middleware_hosts
* Any federation servers if in a federation
* Configured hosts in choria.middleware_hosts
* SRV lookups in _mcollective-server._tcp and _x-puppet-mcollective._tcp
* Supplied defaults

Eventually it’s intended that other middleware might be supported this would provide a single way to configure them all

Parameters:

  • default_host (String) (defaults to: "puppet")

    default hostname

  • default_port (String) (defaults to: "4222")

    default port

Returns:



542
543
544
545
546
547
548
549
550
# File 'lib/mcollective/util/choria.rb', line 542

def middleware_servers(default_host="puppet", default_port="4222")
  return [["connect.ngs.global", "4222"]] if ngs? && !has_option?("choria.middleware_hosts")

  if federated? && federation = federation_middleware_servers
    return federation
  end

  server_resolver("choria.middleware_hosts", ["_mcollective-server._tcp", "_x-puppet-mcollective._tcp"], default_host, default_port)
end

#ngs?Boolean

Determines if we are connecting to NGS based on credentials and the nats.ngs setting

Returns:

  • (Boolean)


40
41
42
# File 'lib/mcollective/util/choria.rb', line 40

def ngs?
  credential_file != "" && Util.str_to_bool(get_option("nats.ngs", "false"))
end

#nkeys?Boolean

Attempts to load the optional nkeys library

Returns:

  • (Boolean)


47
48
49
50
51
52
# File 'lib/mcollective/util/choria.rb', line 47

def nkeys?
  require "nkeys"
  true
rescue LoadError
  false
end

#parse_pubcert(pubcert, log = true) ⇒ Array<OpenSSL::X509::Certificate,nil>

Parses a public cert

Parameters:

  • pubcert (String)

    PEM encoded public certificate

  • log (Boolean) (defaults to: true)

    log warnings when true

Returns:

  • (Array<OpenSSL::X509::Certificate,nil>)


447
448
449
450
451
452
# File 'lib/mcollective/util/choria.rb', line 447

def parse_pubcert(pubcert, log=true)
  ssl_parse_chain(pubcert)
rescue OpenSSL::X509::CertificateError
  Log.warn("Received certificate is not a valid x509 certificate: %s: %s" % [$!.class, $!.to_s]) if log
  nil
end

#pql_extract_certnames(results) ⇒ Array<String>

Extract certnames from PQL results, deactivated nodes are ignored

Parameters:

Returns:



325
326
327
# File 'lib/mcollective/util/choria.rb', line 325

def pql_extract_certnames(results)
  results.reject {|n| n["deactivated"]}.map {|n| n["certname"]}.compact
end

#pql_query(query, only_certnames = false) ⇒ Array

Performs a PQL query against the configured PuppetDB

Parameters:

  • query (String)

    PQL Query

  • only_certnames (Boolean) (defaults to: false)

    extract certnames from the results

Returns:

  • (Array)

    JSON parsed result set

Raises:

  • (StandardError)

    on any failures



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/mcollective/util/choria.rb', line 335

def pql_query(query, only_certnames=false)
  Log.debug("Performing PQL query: %s" % query)

  path = "/pdb/query/v4?%s" % URI.encode_www_form("query" => query)

  resp, data = https(puppetdb_server, true).request(http_get(path))

  raise("Failed to make request to PuppetDB: %s: %s: %s" % [resp.code, resp.message, resp.body]) unless resp.code == "200"

  result = JSON.parse(data || resp.body)

  Log.debug("Found %d records for query %s" % [result.size, query])

  only_certnames ? pql_extract_certnames(result) : result
end

#proxied_discovery?Boolean

Determines if this is using a discovery proxy

Returns:

  • (Boolean)


685
686
687
# File 'lib/mcollective/util/choria.rb', line 685

def proxied_discovery?
  has_option?("choria.discovery_host") || has_option?("choria.discovery_port") || Util.str_to_bool(get_option("choria.discovery_proxy", "false"))
end

#proxy_discovery_query(query) ⇒ Array

Does a proxied discovery request

Parameters:

  • query (Hash)

    Discovery query as per pdbproxy standard

Returns:

  • (Array)

    JSON parsed result set

Raises:

  • (StandardError)

    on any failures



306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/mcollective/util/choria.rb', line 306

def proxy_discovery_query(query)
  transport = https(discovery_server, true)
  request = http_get("/v1/discover")
  request.body = query.to_json
  request["Content-Type"] = "application/json"

  resp, data = transport.request(request)

  raise("Failed to make request to Discovery Proxy: %s: %s" % [resp.code, resp.body]) unless resp.code == "200"

  result = JSON.parse(data || resp.body)

  result["nodes"]
end

#puppet_security?Boolean

Determines if the puppet security provider is enabled

Returns:

  • (Boolean)


800
801
802
# File 'lib/mcollective/util/choria.rb', line 800

def puppet_security?
  security_provider == "puppet"
end

#puppet_serverHash

The Puppet server to connect to

Will consult SRV records for _x-puppet._tcp.example.net first then configurable using choria.puppetserver_host and choria.puppetserver_port defaults to puppet:8140.

Returns:

  • (Hash)

    with :target and :port



612
613
614
615
616
617
# File 'lib/mcollective/util/choria.rb', line 612

def puppet_server
  d_host = get_option("choria.puppetserver_host", "puppet")
  d_port = get_option("choria.puppetserver_port", "8140")

  try_srv(["_x-puppet._tcp"], d_host, d_port)
end

#puppet_setting(setting) ⇒ String

Initialises Puppet if needed and retrieve a config setting

Parameters:

  • setting (Symbol)

    a Puppet setting name

Returns:



719
720
721
722
723
724
725
726
727
728
729
730
731
# File 'lib/mcollective/util/choria.rb', line 719

def puppet_setting(setting)
  require "puppet"

  unless Puppet.settings.app_defaults_initialized?
    Puppet.settings.preferred_run_mode = :agent

    Puppet.settings.initialize_global_settings([])
    Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(Puppet.run_mode))
    Puppet.push_context(Puppet.base_context(Puppet.settings))
  end

  Puppet.settings[setting]
end

#puppetca_serverHash

The Puppet server to connect to

Will consult _x-puppet-ca._tcp.example.net then _x-puppet._tcp.example.net then configurable using choria.puppetca_host, defaults to puppet:8140

Returns:

  • (Hash)

    with :target and :port



625
626
627
628
629
630
631
632
633
634
# File 'lib/mcollective/util/choria.rb', line 625

def puppetca_server
  d_port = get_option("choria.puppetca_port", "8140")

  if @ca
    {:target => @ca, :port => d_port}
  else
    d_host = get_option("choria.puppetca_host", "puppet")
    try_srv(["_x-puppet-ca._tcp", "_x-puppet._tcp"], d_host, d_port)
  end
end

#puppetdb_serverHash

The PuppetDB server to connect to

Use choria.puppetdb_host if set, otherwise query _x-puppet-db._tcp.example.net then _x-puppet._tcp.example.net if SRV lookup is enabled, and fallback to puppet:8081 if nothing else worked.

Returns:

  • (Hash)

    with :target and :port



643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
# File 'lib/mcollective/util/choria.rb', line 643

def puppetdb_server
  d_port = get_option("choria.puppetdb_port", "8081")

  answer = {
    :target => get_option("choria.puppetdb_host", nil),
    :port => d_port
  }

  return answer if answer[:target]

  answer = try_srv(["_x-puppet-db._tcp"], nil, nil)
  return answer if answer[:target]

  # In the case where we take _x-puppet._tcp SRV records we unfortunately have
  # to force the port else it uses the one from Puppet which will 404
  answer = try_srv(["_x-puppet._tcp"], "puppet", d_port)
  answer[:port] = d_port

  answer
end

#query_srv_records(records) {|Hash| ... } ⇒ Array<Hash>

Query DNS for a series of records

The given records will be passed through #srv_records to figure out the domain to query in.

Querying of records can be bypassed by setting choria.use_srv to false

Parameters:

  • records (Array<String>)

    the records to query without their domain parts

Yields:

  • (Hash)

    each record for modification by the caller

Returns:

  • (Array<Hash>)

    with keys :port, :priority, :weight and :target



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/mcollective/util/choria.rb', line 178

def query_srv_records(records)
  unless should_use_srv?
    Log.info("Skipping SRV record queries due to choria.query_srv_records setting")
    return []
  end

  answers = Array(srv_records(records)).map do |record|
    Log.debug("Attempting to resolve SRV record %s" % record)
    answers = resolver.getresources(record, Resolv::DNS::Resource::IN::SRV)
    Log.debug("Found %d SRV records for %s" % [answers.size, record])
    answers
  end.flatten

  answers = answers.sort_by(&:priority).chunk(&:priority).sort
  answers = sort_srv_answers(answers)

  answers.map do |result|
    Log.debug("Found %s:%s with priority %s and weight %s" % [result.target, result.port, result.priority, result.weight])

    ans = {
      :port => result.port,
      :priority => result.priority,
      :weight => result.weight,
      :target => result.target
    }

    yield(ans) if block_given?

    ans
  end
end

#randomize_middleware_servers?Boolean

Determines if servers should be randomized

Returns:

  • (Boolean)


568
569
570
# File 'lib/mcollective/util/choria.rb', line 568

def randomize_middleware_servers?
  Util.str_to_bool(get_option("choria.randomize_middleware_hosts", "true"))
end

#remote_signer_configured?Boolean

Determines if a remote signer is configured

Returns:

  • (Boolean)


414
415
416
417
418
# File 'lib/mcollective/util/choria.rb', line 414

def remote_signer_configured?
  url = get_option("choria.security.request_signer.url", nil)

  ![nil, ""].include?(url)
end

#resolverResolv::DNS

Note:

mainly used for testing

Retrieves a DNS resolver

Returns:

  • (Resolv::DNS)


111
112
113
# File 'lib/mcollective/util/choria.rb', line 111

def resolver
  Resolv::DNS.new
end

#security_providerObject

Determines the security provider



790
791
792
# File 'lib/mcollective/util/choria.rb', line 790

def security_provider
  get_option("security.provider", "puppet")
end

#server_resolver(config_option, srv_records, default_host = nil, default_port = nil) ⇒ Array?

Resolves server lists based on config and SRV records

Attempts to find server in the following order:

* Configured hosts in `config_option`
* SRV lookups of `srv_records`
* Defaults
* nil otherwise

Parameters:

  • config_option (String)

    config to lookup

  • srv_records (Array<String>)

    list of SRV records to query

  • default_host (String) (defaults to: nil)

    host to use when not found

  • default_port (String) (defaults to: nil)

    port to use when not found

Returns:

  • (Array, nil)

    groups of host and port pairs



504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/mcollective/util/choria.rb', line 504

def server_resolver(config_option, srv_records, default_host=nil, default_port=nil)
  if servers = get_option(config_option, nil)
    hosts = servers.split(",").map do |server|
      server.split(":").map(&:strip)
    end

    return hosts
  end

  srv_answers = query_srv_records(srv_records)

  unless srv_answers.empty?
    hosts = srv_answers.map do |answer|
      [answer[:target], answer[:port]]
    end

    return hosts
  end

  [[default_host, default_port]] if default_host && default_port
end

#should_use_srv?Boolean

Determines if SRV records should be used

Setting choria.use_srv to anything other than t, true, yes or 1 will disable SRV records

Returns:

  • (Boolean)


165
166
167
# File 'lib/mcollective/util/choria.rb', line 165

def should_use_srv?
  ["t", "true", "yes", "1"].include?(get_option("choria.use_srv", "1").downcase)
end

#sort_srv_answers(answers) ⇒ Array<Resolv::DNS::Resource::IN::SRV>

Note:

this is probably still not correct :( so horrible

Sorts SRV records according to rfc2782

Parameters:

  • answers (Array<Resolv::DNS::Resource::IN::SRV>)

Returns:

  • (Array<Resolv::DNS::Resource::IN::SRV>)

    sorted records



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/mcollective/util/choria.rb', line 215

def sort_srv_answers(answers)
  sorted_answers = []

  # this is roughly based on the resolv-srv and supposedly mostly rfc2782 compliant
  answers.each do |_, available|
    total_weight = available.inject(0) {|a, e| a + e.weight + 1 }

    until available.empty?
      selector = Integer(rand * total_weight) + 1
      selected_idx = available.find_index do |e|
        selector -= e.weight + 1
        selector <= 0
      end
      selected = available.delete_at(selected_idx)

      total_weight -= selected.weight + 1

      sorted_answers << selected
    end
  end

  sorted_answers
end

#srv_domainString

Determines the domain to do SRV lookups in

This is settable using the environment variable CHORIA_SRV_DOMAIN or choria.srv_domain and defaults to the domain as reported by facter

Returns:



135
136
137
# File 'lib/mcollective/util/choria.rb', line 135

def srv_domain
  env_fetch("CHORIA_SRV_DOMAIN", nil) || get_option("choria.srv_domain", nil) || facter_domain
end

#srv_records(keys) ⇒ Array<String>

Determines the SRV records to look up

If an option choria.srv_domain is set that will be used else facter will be consulted, if neither of those provide a domain name a empty list is returned

Parameters:

Returns:



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/mcollective/util/choria.rb', line 146

def srv_records(keys)
  domain = srv_domain

  if domain.nil? || domain.empty?
    Log.warn("Cannot look up SRV records, facter is not functional and choria.srv_domain was not supplied")
    return []
  end

  keys.map do |key|
    "%s.%s" % [key, domain]
  end
end

#ssl_contextOpenSSL::SSL::SSLContext

Creates a SSL Context which includes the AIO SSL files



736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/mcollective/util/choria.rb', line 736

def ssl_context
  context = OpenSSL::SSL::SSLContext.new
  context.ca_file = ca_path
  context.ssl_version = :TLSv1_2 # rubocop:disable Naming/VariableNumber

  if anon_tls?
    context.verify_mode = OpenSSL::SSL::VERIFY_NONE
    return context
  end

  public_cert = File.read(client_public_cert)
  private_key = File.read(client_private_key)

  cert_chain = ssl_parse_chain(public_cert)

  cert = cert_chain.first
  key = OpenSSL::PKey::RSA.new(private_key)

  extra_chain_cert = cert_chain[1..-1]

  if OpenSSL::SSL::SSLContext.method_defined?(:add_certificate)
    context.add_certificate(cert, key, extra_chain_cert)
  else
    context.cert = OpenSSL::X509::Certificate.new(File.read(client_public_cert))
    context.key = OpenSSL::PKey::RSA.new(File.read(client_private_key))
    context.extra_chain_cert = extra_chain_cert
  end

  context.verify_mode = OpenSSL::SSL::VERIFY_PEER

  context
end

#ssl_dirString

The directory where SSL related files live

This is configurable using choria.ssldir which should be a path expandable using File.expand_path

On Windows or when running as root Puppet settings will be consulted but when running as a normal user it will default to the AIO path when not configured

Returns:



779
780
781
782
783
784
785
786
787
# File 'lib/mcollective/util/choria.rb', line 779

def ssl_dir
  @_ssl_dir ||= if has_option?("choria.ssldir")
                  File.expand_path(get_option("choria.ssldir"))
                elsif Util.windows? || Process.uid == 0
                  puppet_setting(:ssldir)
                else
                  File.expand_path("~/.puppetlabs/etc/puppet/ssl")
                end
end

#ssl_parse_chain(pemdata) ⇒ Array<OpenSSL::X509::Certificate,nil>

Split a string containing chained certificates into an Array of OpenSSL::X509::Certificate.

Parameters:

Returns:

  • (Array<OpenSSL::X509::Certificate,nil>)


436
437
438
439
440
# File 'lib/mcollective/util/choria.rb', line 436

def ssl_parse_chain(pemdata)
  ssl_split_pem(pemdata).map do |cpem|
    OpenSSL::X509::Certificate.new(cpem)
  end
end

#ssl_split_pem(pemdata) ⇒ Array<String,nil>

Utility function to split a chained certificate String into an Array

Parameters:

  • pemdata (String)

    PEM encoded certificate

Returns:



424
425
426
427
428
429
430
# File 'lib/mcollective/util/choria.rb', line 424

def ssl_split_pem(pemdata)
  # Chained certificates typically have the public certificate, along
  # with every intermediate certificiate.
  # OpenSSL will stop at the first certificate when using OpenSSL::X509::Certificate.new,
  # so we need to separate them into a list
  pemdata.scan(/-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m)
end

#tasks_cache_dirString

Determines the Tasks Cache dir

Returns:

  • (String)

    path to the cache



66
67
68
69
70
71
72
73
74
# File 'lib/mcollective/util/choria.rb', line 66

def tasks_cache_dir
  if Util.windows?
    File.join(Util.windows_prefix, "tasks-cache")
  elsif Process.uid == 0
    "/opt/puppetlabs/mcollective/tasks-cache"
  else
    File.expand_path("~/.puppetlabs/mcollective/tasks-cache")
  end
end

#tasks_spool_dirString

Determines the Tasks Spool directory

Returns:

  • (String)

    path to the spool



79
80
81
82
83
84
85
86
87
# File 'lib/mcollective/util/choria.rb', line 79

def tasks_spool_dir
  if Util.windows?
    File.join(Util.windows_prefix, "tasks-spool")
  elsif Process.uid == 0
    "/opt/puppetlabs/mcollective/tasks-spool"
  else
    File.expand_path("~/.puppetlabs/mcollective/tasks-spool")
  end
end

#tasks_supportTasksSupport

Creates a new TasksSupport instance with the configured cache dir

Returns:



57
58
59
60
61
# File 'lib/mcollective/util/choria.rb', line 57

def tasks_support
  require_relative "tasks_support"

  Util::TasksSupport.new(self, tasks_cache_dir)
end

#try_srv(names, default_target, default_port) ⇒ Hash

Attempts to look up some SRV records falling back to defaults

When given a array of multiple names it will try each name individually and check if it resolved to a answer, if it did it will use that one. Else it will move to the next. In this way you can prioritise one record over another like puppetdb over puppet and faill back to defaults.

This is a pretty naive implementation that right now just returns the first result, the correct behaviour needs to be determined but for now this gets us going with easily iterable code.

These names are mainly being used by #https so in theory it would be quite easy to support multiple results with fall back etc, but I am not really sure what would be the best behaviour here

Parameters:

  • names (Array<String>, String)

    list of names to lookup without the domain

  • default_target (String)

    default for the returned :target

  • default_port (String)

    default for the returned :port

Returns:

  • (Hash)

    with :target and :port



591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'lib/mcollective/util/choria.rb', line 591

def try_srv(names, default_target, default_port)
  srv_answers = Array(names).map do |name|
    answer = query_srv_records([name])

    answer.empty? ? nil : answer
  end.compact.flatten

  if srv_answers.empty?
    {:target => default_target, :port => default_port}
  else
    {:target => srv_answers[0][:target].to_s, :port => srv_answers[0][:port]}
  end
end

#valid_certificate?(pubcert, name, log = true) ⇒ String, false

Validates a certificate against the CA

Parameters:

  • pubcert (String)

    PEM encoded X509 public certificate

  • name (String)

    name that should be present in the certificate

  • log (Boolean) (defaults to: true)

    log warnings when true

Returns:

  • (String, false)

    when succesful, the certname else false

Raises:

  • (StandardError)

    in case OpenSSL fails to open the various certificates

  • (OpenSSL::X509::CertificateError)

    if the CA is invalid



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/mcollective/util/choria.rb', line 376

def valid_certificate?(pubcert, name, log=true)
  return false unless name

  raise("Cannot find or read the CA in %s, cannot verify public certificate" % ca_path) unless File.readable?(ca_path)

  certs = parse_pubcert(pubcert, log)

  return false if certs.empty?

  incoming = certs.first

  chain = certs[1..-1]

  begin
    ca = OpenSSL::X509::Store.new.add_file(ca_path)
  rescue OpenSSL::X509::StoreError
    Log.warn("Failed to load CA from %s: %s: %s" % [ca_path, $!.class, $!.to_s]) if log
    raise
  end

  unless ca.verify(incoming, chain)
    Log.warn("Failed to verify certificate %s against CA %s in %s" % [incoming.subject.to_s, incoming.issuer.to_s, ca_path]) if log

    return false
  end

  Log.debug("Verified certificate %s against CA %s" % [incoming.subject.to_s, incoming.issuer.to_s]) if log

  if !remote_signer_configured? && !OpenSSL::SSL.verify_certificate_identity(incoming, name)
    raise("Could not parse certificate with subject %s as it has no CN part, or name %s invalid" % [incoming.subject.to_s, name])
  end

  name
end

#which(command) ⇒ String?

Searches the PATH for an executable command

Parameters:

  • command (String)

    a command to search for

Returns:

  • (String, nil)

    the path to the command or nil



894
895
896
897
898
899
900
901
902
903
904
905
906
# File 'lib/mcollective/util/choria.rb', line 894

def which(command)
  exts = Array(env_fetch("PATHEXT", "").split(";"))
  exts << "" if exts.empty?

  env_fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
    exts.each do |ext|
      exe = File.join(path, "%s%s" % [command, ext])
      return exe if File.executable?(exe) && !File.directory?(exe)
    end
  end

  nil
end