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)


865
866
867
# File 'lib/mcollective/util/choria.rb', line 865

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:



856
857
858
859
860
# File 'lib/mcollective/util/choria.rb', line 856

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



460
461
462
# File 'lib/mcollective/util/choria.rb', line 460

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:



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

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



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

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:



840
841
842
843
844
# File 'lib/mcollective/util/choria.rb', line 840

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:



823
824
825
826
827
# File 'lib/mcollective/util/choria.rb', line 823

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:



879
880
881
882
883
# File 'lib/mcollective/util/choria.rb', line 879

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



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

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



950
951
952
# File 'lib/mcollective/util/choria.rb', line 950

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



813
814
815
816
817
# File 'lib/mcollective/util/choria.rb', line 813

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:



916
917
918
919
920
# File 'lib/mcollective/util/choria.rb', line 916

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:



124
125
126
127
128
# File 'lib/mcollective/util/choria.rb', line 124

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
106
107
# File 'lib/mcollective/util/choria.rb', line 99

def federation_collectives
  if !@config.federations.empty?
    @config.federations
  elsif (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



563
564
565
# File 'lib/mcollective/util/choria.rb', line 563

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)


797
798
799
# File 'lib/mcollective/util/choria.rb', line 797

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



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

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)


872
873
874
# File 'lib/mcollective/util/choria.rb', line 872

def has_ca?
  File.exist?(ca_path)
end

#has_client_private_key?Boolean

Determines if the client_private_key exist

Returns:

  • (Boolean)


849
850
851
# File 'lib/mcollective/util/choria.rb', line 849

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)


832
833
834
# File 'lib/mcollective/util/choria.rb', line 832

def has_client_public_cert?
  File.exist?(client_public_cert)
end

#has_csr?Boolean

Determines if the CSR exist

Returns:

  • (Boolean)


888
889
890
# File 'lib/mcollective/util/choria.rb', line 888

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)


946
947
948
# File 'lib/mcollective/util/choria.rb', line 946

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)


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

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)


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

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)


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

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)


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

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:



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

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>)


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

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:



327
328
329
# File 'lib/mcollective/util/choria.rb', line 327

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



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

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)


687
688
689
# File 'lib/mcollective/util/choria.rb', line 687

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



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

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)


802
803
804
# File 'lib/mcollective/util/choria.rb', line 802

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



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

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:



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

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



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

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



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

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



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
209
210
# File 'lib/mcollective/util/choria.rb', line 180

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)


570
571
572
# File 'lib/mcollective/util/choria.rb', line 570

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)


416
417
418
419
420
# File 'lib/mcollective/util/choria.rb', line 416

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)


113
114
115
# File 'lib/mcollective/util/choria.rb', line 113

def resolver
  Resolv::DNS.new
end

#security_providerObject

Determines the security provider



792
793
794
# File 'lib/mcollective/util/choria.rb', line 792

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



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

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)


167
168
169
# File 'lib/mcollective/util/choria.rb', line 167

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



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

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:



137
138
139
# File 'lib/mcollective/util/choria.rb', line 137

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:



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

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



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
768
769
# File 'lib/mcollective/util/choria.rb', line 738

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:



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

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>)


438
439
440
441
442
# File 'lib/mcollective/util/choria.rb', line 438

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:



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

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



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

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



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
410
411
# File 'lib/mcollective/util/choria.rb', line 378

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



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

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