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)


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

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:



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

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



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

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:



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

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



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

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:



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

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:



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

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:



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

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



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

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



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

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



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

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:



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

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



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

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)


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

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



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

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)


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

def has_ca?
  File.exist?(ca_path)
end

#has_client_private_key?Boolean

Determines if the client_private_key exist

Returns:

  • (Boolean)


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

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)


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

def has_client_public_cert?
  File.exist?(client_public_cert)
end

#has_csr?Boolean

Determines if the CSR exist

Returns:

  • (Boolean)


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

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)


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

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)


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

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)


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

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)


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

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)


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

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:



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

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


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

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:



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

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



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

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)


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

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



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

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)


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

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



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

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:



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

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



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

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



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

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



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

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)


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

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)


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

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



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

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



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

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)


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

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



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

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 choria.srv_domain and defaults to the domain as reported by facter

Returns:



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

def srv_domain
  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:



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

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



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

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:



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

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


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

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:



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

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



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

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



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

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



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

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