openbolt 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9b0100076081c5483e86b98cea67dc2c9ed0a46ceb7b86d3883257f34b68ee8
4
- data.tar.gz: c85b0fd26588f95757ebdf25b86ed39a3f2c16dc530b0517c4d4c6848e16440a
3
+ metadata.gz: af114a7662dadfeb0b7a3b93bade87bd679b4d555c718ef9de2782327fb72f94
4
+ data.tar.gz: 212b616948e0e548f821576db50971c21ad9f9e5e9a84d9e676542078ea623d3
5
5
  SHA512:
6
- metadata.gz: 89b8e8179220b89ca34d44682c9a2e83723dfc7d06ffd09ed7bbaff010cb6e4635d837451f9131eea95917151b81c74e6f65912fd77c8cfc8d694e1748f236f9
7
- data.tar.gz: 6158a6c97dad745b1ad1a0ec9a745bd44ce59f1a8ad0e636c3df2f39cd07f962b28a1a19d6109dc94d68c3938e18cb5acd498ceea12a3fff094327390ab776a8
6
+ metadata.gz: 9a85395239d2b5de8adc4d538bf0860139ee0585c3880d8ea6ccb46a840fa0ee6371e58ce17feb2cc171b5fc47cddeabd284adedcbb3abd60675951b1b5e0169
7
+ data.tar.gz: f42f2da1b4ea78080c3790e2165be94105bb7a9ad9c5a16b2702f5511a8f6e9f8893c781f9fe2c63e65b3987819576ccd2e4e1b89b13e13bb7386e6d5708ed94
data/Puppetfile CHANGED
@@ -6,27 +6,27 @@ moduledir File.join(File.dirname(__FILE__), 'modules')
6
6
 
7
7
  # Core modules used by 'apply'
8
8
  mod 'puppetlabs-service', '3.1.0'
9
- mod 'puppet-openvox_bootstrap', '1.2.0'
9
+ mod 'puppet-openvox_bootstrap', '1.4.0'
10
10
  mod 'puppetlabs-facts', '1.7.0'
11
11
 
12
12
  # Other core Puppet modules
13
- mod 'puppetlabs-inifile', '6.2.0'
14
- mod 'puppetlabs-apt', '11.1.0'
13
+ mod 'puppetlabs-inifile', '6.3.1'
14
+ mod 'puppetlabs-apt', '11.2.0'
15
15
  mod 'puppetlabs-stdlib', '9.7.0'
16
16
  mod 'puppetlabs-powershell', '6.1.0'
17
- mod 'puppetlabs-pwshlib', '2.0.0'
17
+ mod 'puppetlabs-pwshlib', '2.0.1'
18
18
 
19
19
  # Core types and providers for Puppet 6
20
20
  mod 'puppetlabs-augeas_core', '2.0.1'
21
21
  mod 'puppetlabs-host_core', '2.0.1'
22
22
  mod 'puppetlabs-scheduled_task', '4.0.3'
23
23
  mod 'puppetlabs-sshkeys_core', '3.0.1'
24
- mod 'puppetlabs-zfs_core', '1.6.1'
24
+ mod 'puppetlabs-zfs_core', '2.0.1'
25
25
  mod 'puppetlabs-cron_core', '2.0.2'
26
26
  mod 'puppetlabs-mount_core', '2.0.1'
27
27
  mod 'puppetlabs-selinux_core', '2.0.1'
28
28
  mod 'puppetlabs-yumrepo_core', '3.0.1'
29
- mod 'puppetlabs-zone_core', '2.0.1'
29
+ mod 'puppetlabs-zone_core', '2.0.2'
30
30
 
31
31
  # Useful additional modules
32
32
  mod 'puppetlabs-package', '3.1.0'
@@ -55,4 +55,3 @@ mod 'puppetlabs-yaml', '0.2.0'
55
55
  mod 'canary', local: true
56
56
  mod 'aggregate', local: true
57
57
  mod 'puppetdb_fact', local: true
58
- mod 'puppet_connect', local: true
@@ -13,6 +13,11 @@ module Bolt
13
13
  run_context: %w[concurrency inventoryfile save-rerun cleanup puppetdb],
14
14
  global_config_setters: PROJECT_PATHS + %w[modulepath],
15
15
  transports: %w[transport connect-timeout tty native-ssh ssh-command copy-command],
16
+ choria: %w[choria-config-file choria-mcollective-certname
17
+ choria-ssl-ca choria-ssl-cert choria-ssl-key
18
+ choria-collective choria-puppet-environment choria-rpc-timeout
19
+ choria-task-timeout choria-command-timeout choria-brokers
20
+ choria-broker-timeout],
16
21
  display: %w[format color verbose trace stream],
17
22
  global: %w[help version log-level clear-cache] }.freeze
18
23
 
@@ -168,7 +173,7 @@ module Bolt
168
173
  when 'task'
169
174
  case action
170
175
  when 'run'
171
- { flags: ACTION_OPTS + %w[params tmpdir noop],
176
+ { flags: ACTION_OPTS + %w[params tmpdir noop choria-task-agent],
172
177
  banner: TASK_RUN_HELP }
173
178
  when 'show'
174
179
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters] + %w[filter format],
@@ -1095,6 +1100,63 @@ module Bolt
1095
1100
  define('--tmpdir DIR', 'The directory to upload and execute temporary files on the target.') do |tmpdir|
1096
1101
  @options[:tmpdir] = tmpdir
1097
1102
  end
1103
+ define('--choria-task-agent AGENT', %w[bolt_tasks shell],
1104
+ "Which Choria agent to use for task execution (bolt_tasks, shell).",
1105
+ "Defaults to 'bolt_tasks'. Set to 'shell' for tasks not on the Puppet Server.") do |agent|
1106
+ @options[:'task-agent'] = agent
1107
+ end
1108
+ define('--choria-config-file PATH',
1109
+ 'Path to a Choria/MCollective client configuration file.') do |path|
1110
+ @options[:'config-file'] = path
1111
+ end
1112
+ define('--choria-mcollective-certname NAME',
1113
+ 'Override the MCollective certname for Choria client identity.',
1114
+ 'The choria-mcorpc-support library identifies non-root clients',
1115
+ "as '<username>.mcollective', which fails when authenticating",
1116
+ "with a certificate that has a different CN (e.g. the host's",
1117
+ 'Puppet cert). Set this to the CN of the certificate being used.') do |name|
1118
+ @options[:'mcollective-certname'] = name
1119
+ end
1120
+ define('--choria-ssl-ca PATH',
1121
+ 'CA certificate path for Choria TLS authentication.') do |path|
1122
+ @options[:'ssl-ca'] = path
1123
+ end
1124
+ define('--choria-ssl-cert PATH',
1125
+ 'Client certificate path for Choria TLS authentication.') do |path|
1126
+ @options[:'ssl-cert'] = path
1127
+ end
1128
+ define('--choria-ssl-key PATH',
1129
+ 'Client private key path for Choria TLS authentication.') do |path|
1130
+ @options[:'ssl-key'] = path
1131
+ end
1132
+ define('--choria-collective NAME',
1133
+ 'Choria collective to route messages through.') do |name|
1134
+ @options[:collective] = name
1135
+ end
1136
+ define('--choria-puppet-environment ENV',
1137
+ "Puppet environment for bolt_tasks file downloads (default: 'production').") do |env|
1138
+ @options[:'puppet-environment'] = env
1139
+ end
1140
+ define('--choria-rpc-timeout SECONDS', Integer,
1141
+ 'Seconds to wait for replies to individual Choria RPC calls (default: 30).') do |timeout|
1142
+ @options[:'rpc-timeout'] = timeout
1143
+ end
1144
+ define('--choria-task-timeout SECONDS', Integer,
1145
+ 'Seconds to wait for task execution to complete (default: 300).') do |timeout|
1146
+ @options[:'task-timeout'] = timeout
1147
+ end
1148
+ define('--choria-command-timeout SECONDS', Integer,
1149
+ 'Seconds to wait for commands and scripts to complete (default: 60).') do |timeout|
1150
+ @options[:'command-timeout'] = timeout
1151
+ end
1152
+ define('--choria-brokers BROKERS',
1153
+ 'Choria broker addresses in host or host:port format (comma-separated). Port defaults to 4222 if omitted.') do |brokers|
1154
+ @options[:brokers] = brokers.split(',')
1155
+ end
1156
+ define('--choria-broker-timeout SECONDS', Integer,
1157
+ 'Seconds to wait for the TCP connection to a Choria broker (default: 30).') do |timeout|
1158
+ @options[:'broker-timeout'] = timeout
1159
+ end
1098
1160
 
1099
1161
  separator "\n#{self.class.colorize(:cyan, 'Module options')}"
1100
1162
  define('--[no-]resolve',
data/lib/bolt/cli.rb CHANGED
@@ -750,7 +750,7 @@ module Bolt
750
750
  # built-in modules are installed.
751
751
  #
752
752
  private def incomplete_install?
753
- builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
753
+ builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars]
754
754
  (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
755
755
  end
756
756
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../bolt/config/transport/choria'
3
4
  require_relative '../../bolt/config/transport/docker'
4
5
  require_relative '../../bolt/config/transport/jail'
5
6
  require_relative '../../bolt/config/transport/local'
@@ -15,6 +16,7 @@ module Bolt
15
16
  # Transport config classes. Used to load default transport config which
16
17
  # gets passed along to the inventory.
17
18
  TRANSPORT_CONFIG = {
19
+ 'choria' => Bolt::Config::Transport::Choria,
18
20
  'docker' => Bolt::Config::Transport::Docker,
19
21
  'jail' => Bolt::Config::Transport::Jail,
20
22
  'local' => Bolt::Config::Transport::Local,
@@ -76,6 +78,12 @@ module Bolt
76
78
  _example: 120,
77
79
  _plugin: true
78
80
  },
81
+ "headers" => {
82
+ description: "A map of HTTP headers to add to PuppetDB requests.",
83
+ type: Hash,
84
+ _example: { "Authorization" => "Bearer <token>" },
85
+ _plugin: true
86
+ },
79
87
  "key" => {
80
88
  description: "The private key for the certificate.",
81
89
  type: String,
@@ -545,6 +553,12 @@ module Bolt
545
553
  _example: "winrm",
546
554
  _default: "ssh"
547
555
  },
556
+ "choria" => {
557
+ description: "A map of configuration options for the choria transport.",
558
+ type: Hash,
559
+ _plugin: true,
560
+ _example: { "config-file" => "/etc/choria/client.conf" }
561
+ },
548
562
  "docker" => {
549
563
  description: "A map of configuration options for the docker transport.",
550
564
  type: Hash,
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../bolt/error'
4
+ require_relative '../../../bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Choria < Base
10
+ OPTIONS = %w[
11
+ cleanup
12
+ collective
13
+ command-timeout
14
+ config-file
15
+ host
16
+ interpreters
17
+ mcollective-certname
18
+ broker-timeout
19
+ brokers
20
+ puppet-environment
21
+ rpc-timeout
22
+ ssl-ca
23
+ ssl-cert
24
+ ssl-key
25
+ task-agent
26
+ task-timeout
27
+ tmpdir
28
+ ].sort.freeze
29
+
30
+ DEFAULTS = {
31
+ 'cleanup' => true,
32
+ 'command-timeout' => 60,
33
+ 'broker-timeout' => 30,
34
+ 'puppet-environment' => 'production',
35
+ 'rpc-timeout' => 30,
36
+ 'task-timeout' => 300,
37
+ 'tmpdir' => '/tmp'
38
+ }.freeze
39
+
40
+ VALID_AGENTS = %w[bolt_tasks shell].freeze
41
+
42
+ private def validate
43
+ super
44
+
45
+ if @config['task-agent'] && !VALID_AGENTS.include?(@config['task-agent'])
46
+ raise Bolt::ValidationError,
47
+ "task-agent must be one of #{VALID_AGENTS.join(', ')}, got '#{@config['task-agent']}'"
48
+ end
49
+
50
+ if @config['tmpdir'] && !absolute_path?(@config['tmpdir'])
51
+ raise Bolt::ValidationError,
52
+ "Choria tmpdir must be an absolute path, got '#{@config['tmpdir']}'"
53
+ end
54
+
55
+ ssl_keys = %w[ssl-ca ssl-cert ssl-key]
56
+ provided_ssl = ssl_keys.select { |k| @config[k] }
57
+ if provided_ssl.any? && provided_ssl.length < ssl_keys.length
58
+ missing = ssl_keys - provided_ssl
59
+ raise Bolt::ValidationError,
60
+ "When overriding Choria SSL settings, all three options must be provided " \
61
+ "(ssl-ca, ssl-cert, ssl-key). Missing: #{missing.join(', ')}"
62
+ end
63
+
64
+ @config['interpreters'] = normalize_interpreters(@config['interpreters']) if @config['interpreters']
65
+ end
66
+
67
+ # Accept both POSIX absolute paths (/tmp) and Windows absolute paths (C:\temp).
68
+ def absolute_path?(path)
69
+ path.start_with?('/') || path.match?(Bolt::Transport::Choria::WINDOWS_PATH_REGEX)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -51,6 +51,56 @@ module Bolt
51
51
  _default: true,
52
52
  _example: false
53
53
  },
54
+ "task-agent" => {
55
+ type: String,
56
+ description: "Which Choria agent to use for task execution. Defaults to 'bolt_tasks' " \
57
+ "(downloads task files from a Puppet Server). Set to 'shell' for tasks " \
58
+ "not available on the Puppet Server.",
59
+ _plugin: true,
60
+ _example: "shell"
61
+ },
62
+ "collective" => {
63
+ type: String,
64
+ description: "The Choria collective to target. Overrides the main_collective from the Choria " \
65
+ "client configuration file.",
66
+ _plugin: true,
67
+ _example: "production"
68
+ },
69
+ "command-timeout" => {
70
+ type: Integer,
71
+ description: "How long to wait in seconds for commands and scripts to complete when using the " \
72
+ "Choria transport.",
73
+ minimum: 1,
74
+ _plugin: true,
75
+ _default: 60,
76
+ _example: 120
77
+ },
78
+ "config-file" => {
79
+ type: String,
80
+ description: "The path to the Choria or MCollective client configuration file.",
81
+ _plugin: true,
82
+ _example: "/etc/choria/client.conf"
83
+ },
84
+ "broker-timeout" => {
85
+ type: Integer,
86
+ description: "How long to wait in seconds for the initial TCP connection to a Choria broker. " \
87
+ "If the connection cannot be made within this time, the operation fails.",
88
+ minimum: 1,
89
+ _plugin: true,
90
+ _default: 30,
91
+ _example: 60
92
+ },
93
+ "rpc-timeout" => {
94
+ type: Integer,
95
+ description: "How long to wait in seconds for nodes to respond to an RPC request. " \
96
+ "Used for lightweight operations like agent discovery, shell.start, and " \
97
+ "shell.list polling. Distinct from command-timeout and task-timeout which " \
98
+ "govern the overall duration of commands and tasks.",
99
+ minimum: 1,
100
+ _plugin: true,
101
+ _default: 30,
102
+ _example: 60
103
+ },
54
104
  "connect-timeout" => {
55
105
  type: Integer,
56
106
  description: "How long to wait in seconds when establishing connections. Set this value higher if you " \
@@ -225,6 +275,27 @@ module Bolt
225
275
  _plugin: true,
226
276
  _example: %w[defaults hmac-md5]
227
277
  },
278
+ "mcollective-certname" => {
279
+ type: String,
280
+ description: "Override the MCollective certname used for Choria client identity. " \
281
+ "The choria-mcorpc-support library identifies non-root clients as " \
282
+ "'<username>.mcollective'. Set this when authenticating with a certificate " \
283
+ "whose CN differs from that default (e.g. the host's Puppet cert).",
284
+ _plugin: true,
285
+ _example: "primary.example.com"
286
+ },
287
+ "brokers" => {
288
+ type: [String, Array],
289
+ description: "One or more Choria broker addresses in host or host:port format. " \
290
+ "Port defaults to 4222 if omitted. Do not use the nats:// prefix. " \
291
+ "Overrides the middleware hosts from the Choria client configuration file. " \
292
+ "Can be a single string or an array.",
293
+ items: {
294
+ type: String
295
+ },
296
+ _plugin: true,
297
+ _example: ["broker1:4222", "broker2:4222"]
298
+ },
228
299
  "native-ssh" => {
229
300
  type: [TrueClass, FalseClass],
230
301
  description: "This enables the native SSH transport, which shells out to SSH instead of using the " \
@@ -267,6 +338,14 @@ module Bolt
267
338
  _plugin: true,
268
339
  _example: "jump.example.com"
269
340
  },
341
+ "puppet-environment" => {
342
+ type: String,
343
+ description: "The Puppet environment to use when constructing task file URIs for the Choria " \
344
+ "bolt_tasks agent.",
345
+ _plugin: true,
346
+ _default: "production",
347
+ _example: "staging"
348
+ },
270
349
  "read-timeout" => {
271
350
  type: Integer,
272
351
  description: "How long to wait in seconds when making requests to the Orchestrator.",
@@ -343,6 +422,27 @@ module Bolt
343
422
  _plugin: true,
344
423
  _example: 445
345
424
  },
425
+ "ssl-ca" => {
426
+ type: String,
427
+ description: "The path to the CA certificate for Choria TLS connections. Overrides the CA " \
428
+ "from the Choria client configuration file.",
429
+ _plugin: true,
430
+ _example: "/etc/choria/ssl/ca.pem"
431
+ },
432
+ "ssl-cert" => {
433
+ type: String,
434
+ description: "The path to the client certificate for Choria TLS connections. Overrides the " \
435
+ "certificate from the Choria client configuration file.",
436
+ _plugin: true,
437
+ _example: "/etc/choria/ssl/client.pem"
438
+ },
439
+ "ssl-key" => {
440
+ type: String,
441
+ description: "The path to the client private key for Choria TLS connections. Overrides the " \
442
+ "key from the Choria client configuration file.",
443
+ _plugin: true,
444
+ _example: "/etc/choria/ssl/client-key.pem"
445
+ },
346
446
  "ssh-command" => {
347
447
  type: [Array, String],
348
448
  description: "The command and options to use when SSHing. This option is used when you need support for " \
@@ -393,6 +493,14 @@ module Bolt
393
493
  _default: "production",
394
494
  _example: "development"
395
495
  },
496
+ "task-timeout" => {
497
+ type: Integer,
498
+ description: "How long to wait in seconds for tasks to complete when using the Choria transport.",
499
+ minimum: 1,
500
+ _plugin: true,
501
+ _default: 300,
502
+ _example: 300
503
+ },
396
504
  "tmpdir" => {
397
505
  type: String,
398
506
  description: "The directory to upload and execute temporary files on the target.",
data/lib/bolt/executor.rb CHANGED
@@ -13,6 +13,7 @@ require_relative '../bolt/puppetdb'
13
13
  require_relative '../bolt/result'
14
14
  require_relative '../bolt/result_set'
15
15
  # Load transports
16
+ require_relative '../bolt/transport/choria'
16
17
  require_relative '../bolt/transport/docker'
17
18
  require_relative '../bolt/transport/jail'
18
19
  require_relative '../bolt/transport/local'
@@ -24,6 +25,7 @@ require_relative '../bolt/transport/winrm'
24
25
 
25
26
  module Bolt
26
27
  TRANSPORTS = {
28
+ choria: Bolt::Transport::Choria,
27
29
  docker: Bolt::Transport::Docker,
28
30
  jail: Bolt::Transport::Jail,
29
31
  local: Bolt::Transport::Local,
@@ -22,7 +22,7 @@ module Bolt
22
22
 
23
23
  plan_object = parse_plan
24
24
  param_descriptions = plan_object.parameters.map do |param|
25
- str = String.new("# @param #{param.name}")
25
+ str = "# @param #{param.name}"
26
26
  str << " #{param.description}" if param.description
27
27
  str
28
28
  end.join("\n")
@@ -5,7 +5,7 @@ module Bolt
5
5
  class Puppetdb
6
6
  class FactLookupError < Bolt::Error
7
7
  def initialize(fact, err = nil)
8
- m = String.new("Fact lookup '#{fact}' contains an invalid factname")
8
+ m = "Fact lookup '#{fact}' contains an invalid factname"
9
9
  m << ": #{err}" unless err.nil?
10
10
  super(m, 'bolt.plugin/fact-lookup-error')
11
11
  end
data/lib/bolt/plugin.rb CHANGED
@@ -127,7 +127,7 @@ module Bolt
127
127
  end
128
128
  end
129
129
 
130
- RUBY_PLUGINS = %w[task prompt env_var puppetdb puppet_connect_data].freeze
130
+ RUBY_PLUGINS = %w[task prompt env_var puppetdb].freeze
131
131
  BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory
132
132
  yaml env_var gcloud_inventory].freeze
133
133
  DEFAULT_PLUGIN_HOOKS = { 'puppet_library' => { 'plugin' => 'openvox_bootstrap', 'stop_service' => true } }.freeze
@@ -255,9 +255,6 @@ module Bolt
255
255
  hooks = KNOWN_HOOKS.map { |hook| [hook, {}] }.to_h
256
256
 
257
257
  @plugins.sort.each do |name, plugin|
258
- # Don't show the Puppet Connect plugin for now.
259
- next if name == 'puppet_connect_data'
260
-
261
258
  case plugin
262
259
  when Bolt::Plugin::Module
263
260
  plugin.hook_map.each do |hook, spec|
@@ -149,6 +149,14 @@ module Bolt
149
149
  @settings['key']
150
150
  end
151
151
 
152
+ def headers
153
+ if @settings['headers'] && !@settings['headers'].is_a?(Hash)
154
+ raise Bolt::PuppetDBError, "headers must be a Hash"
155
+ end
156
+
157
+ @settings['headers']
158
+ end
159
+
152
160
  def validate_cert_and_key
153
161
  if (@settings['cert'] && !@settings['key']) ||
154
162
  (!@settings['cert'] && @settings['key'])
@@ -139,6 +139,7 @@ module Bolt
139
139
 
140
140
  def headers
141
141
  headers = { 'Content-Type' => 'application/json' }
142
+ headers.merge!(@config.headers) if @config.headers
142
143
  headers['X-Authentication'] = @config.token if @config.token
143
144
  headers
144
145
  end
@@ -27,7 +27,7 @@ module Bolt
27
27
 
28
28
  def iterator
29
29
  if Object.const_defined?(:Puppet) && Puppet.const_defined?(:Pops) &&
30
- self.class.included_modules.include?(Puppet::Pops::Types::Iterable)
30
+ self.class.include?(Puppet::Pops::Types::Iterable)
31
31
  Puppet::Pops::Types::Iterable.on(@results, Bolt::Result)
32
32
  else
33
33
  raise NotImplementedError, "iterator requires puppet code to be loaded."
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ module Transport
5
+ class Choria
6
+ SHELL_MIN_VERSION = '1.2.1'
7
+
8
+ AGENT_MIN_VERSIONS = {
9
+ 'shell' => SHELL_MIN_VERSION
10
+ }.freeze
11
+
12
+ # Discover agents and detect OS on targets via two batched RPC calls
13
+ # (agent_inventory for agents+versions, get_fact for os.family).
14
+ # Populates @agent_cache with { agents: [...], os: 'redhat' | 'windows' | ... }.
15
+ #
16
+ # @param targets [Array<Bolt::Target>] Targets to discover agents on
17
+ def discover_agents(targets)
18
+ uncached = targets.reject { |target| @agent_cache.key?(choria_identity(target)) }
19
+ return if uncached.empty?
20
+
21
+ logger.debug { "Discovering agents on #{target_count(uncached)}" }
22
+ discover_agent_list(uncached)
23
+ discover_os_family(uncached)
24
+
25
+ uncached.each do |target|
26
+ identity = choria_identity(target)
27
+ logger.warn { "No response from #{identity} during agent discovery" } unless @agent_cache.key?(identity)
28
+ end
29
+ end
30
+
31
+ def has_agent?(target, agent_name)
32
+ @agent_cache[choria_identity(target)]&.dig(:agents)&.include?(agent_name) || false
33
+ end
34
+
35
+ def windows_target?(target)
36
+ @agent_cache[choria_identity(target)]&.dig(:os) == 'windows'
37
+ end
38
+
39
+ # Discover available agents on targets via rpcutil.agent_inventory
40
+ # and populate @agent_cache with agent lists.
41
+ #
42
+ # @param targets [Array<Bolt::Target>] Targets to query for agent inventory
43
+ def discover_agent_list(targets)
44
+ response = rpc_request('rpcutil', targets, 'rpcutil.agent_inventory') do |client|
45
+ client.agent_inventory
46
+ end
47
+ response[:errors].each { |target, output| logger.debug { "agent_inventory failed for #{target.safe_name}: #{output[:error]}" } }
48
+
49
+ response[:responded].each do |target, data|
50
+ sender = choria_identity(target)
51
+ agents = filter_agents(sender, data[:agents])
52
+ unless agents
53
+ logger.warn { "Unexpected agent_inventory response from #{sender}. This target will be treated as unreachable." }
54
+ next
55
+ end
56
+ @agent_cache[sender] = { agents: agents }
57
+ logger.debug { "Discovered agents on #{sender}: #{agents.join(', ')}" }
58
+ end
59
+ rescue StandardError => e
60
+ raise if e.is_a?(Bolt::Error)
61
+
62
+ logger.warn { "Agent discovery failed: #{e.class}: #{e.message}" }
63
+ end
64
+
65
+ # Detect the OS family on targets via rpcutil.get_fact and update
66
+ # @agent_cache entries with the :os key.
67
+ #
68
+ # @param targets [Array<Bolt::Target>] Targets to detect OS on
69
+ def discover_os_family(targets)
70
+ # Only fetch OS for targets that responded to agent_inventory
71
+ responded = targets.select { |target| @agent_cache.key?(choria_identity(target)) }
72
+ return if responded.empty?
73
+
74
+ response = rpc_request('rpcutil', responded, 'rpcutil.get_fact') do |client|
75
+ client.get_fact(fact: 'os.family')
76
+ end
77
+ response[:errors].each { |target, output|
78
+ logger.warn {
79
+ "OS detection failed for #{target.safe_name}: #{output[:error]}. Defaulting to POSIX command syntax."
80
+ }
81
+ }
82
+
83
+ response[:responded].each do |target, data|
84
+ sender = choria_identity(target)
85
+ os_family = data[:value].to_s.downcase
86
+ if os_family.empty?
87
+ logger.warn { "os.family fact is empty on #{sender}. Defaulting to POSIX command syntax." }
88
+ next
89
+ end
90
+ @agent_cache[sender][:os] = os_family
91
+ logger.debug { "Detected OS on #{sender}: #{os_family}" }
92
+ end
93
+ rescue StandardError => e
94
+ raise if e.is_a?(Bolt::Error)
95
+
96
+ logger.warn { "OS detection failed: #{e.class}: #{e.message}. Defaulting to POSIX command syntax." }
97
+ end
98
+
99
+ # Filter out agents that don't meet minimum version requirements.
100
+ #
101
+ # @param sender [String] Choria node identity (for logging)
102
+ # @param agent_list [Array<Hash>, nil] Agent entries from agent_inventory, each with
103
+ # :agent (name) and :version keys
104
+ # @return [Array<String>, nil] Agent names that meet version requirements, or nil
105
+ # if agent_list is not an Array
106
+ def filter_agents(sender, agent_list)
107
+ return nil unless agent_list.is_a?(Array)
108
+
109
+ agent_list.filter_map do |entry|
110
+ name = entry['agent']
111
+ next unless name
112
+
113
+ version = entry['version']
114
+ min_version = AGENT_MIN_VERSIONS[name]
115
+ if min_version && !meets_min_version?(version, min_version)
116
+ logger.warn {
117
+ "The '#{name}' agent on #{sender} is version #{version || 'unknown'}, " \
118
+ "but #{min_version} or later is required. It will be treated as unavailable."
119
+ }
120
+ next
121
+ end
122
+
123
+ name
124
+ end
125
+ end
126
+
127
+ def meets_min_version?(version, min_version)
128
+ return false unless version
129
+
130
+ Gem::Version.new(version) >= Gem::Version.new(min_version)
131
+ rescue ArgumentError => e
132
+ logger.warn { "Could not parse version '#{version}': #{e.message}. Treating agent as unavailable." }
133
+ false
134
+ end
135
+ end
136
+ end
137
+ end