openbolt 5.6.0 → 9.0.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 +4 -4
- data/Puppetfile +35 -35
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/background.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +27 -4
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +14 -22
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +14 -26
- data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +2 -2
- data/bolt-modules/file/lib/puppet/functions/file/delete.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/write.rb +1 -1
- data/bolt-modules/log/lib/puppet/functions/log/debug.rb +1 -1
- data/bolt-modules/log/lib/puppet/functions/log/error.rb +1 -1
- data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +1 -1
- data/bolt-modules/log/lib/puppet/functions/log/info.rb +1 -1
- data/bolt-modules/log/lib/puppet/functions/log/trace.rb +1 -1
- data/bolt-modules/log/lib/puppet/functions/log/warn.rb +1 -1
- data/guides/debugging.yaml +1 -1
- data/guides/inventory.yaml +2 -2
- data/guides/links.yaml +18 -9
- data/guides/logging.yaml +1 -1
- data/guides/module.yaml +1 -1
- data/guides/modulepath.yaml +1 -1
- data/guides/project.yaml +2 -2
- data/guides/targets.yaml +3 -1
- data/guides/transports.yaml +4 -3
- data/lib/bolt/analytics.rb +2 -10
- data/lib/bolt/bolt_option_parser.rb +66 -66
- data/lib/bolt/cli.rb +21 -2
- data/lib/bolt/config/options.rb +18 -18
- data/lib/bolt/config/transport/choria.rb +3 -0
- data/lib/bolt/config/transport/options.rb +34 -45
- data/lib/bolt/executor.rb +4 -0
- data/lib/bolt/fiber_executor.rb +46 -2
- data/lib/bolt/inventory/options.rb +1 -1
- data/lib/bolt/inventory/target.rb +1 -1
- data/lib/bolt/module_installer/puppetfile.rb +1 -1
- data/lib/bolt/module_installer/specs.rb +1 -1
- data/lib/bolt/outputter/human.rb +1 -2
- data/lib/bolt/plan_creator.rb +4 -4
- data/lib/bolt/plan_future.rb +5 -1
- data/lib/bolt/project_manager/inventory_migrator.rb +1 -1
- data/lib/bolt/project_manager/module_migrator.rb +1 -1
- data/lib/bolt/project_manager.rb +1 -1
- data/lib/bolt/shell/bash.rb +7 -4
- data/lib/bolt/shell/powershell/snippets.rb +20 -1
- data/lib/bolt/shell/powershell.rb +2 -2
- data/lib/bolt/transport/base.rb +9 -4
- data/lib/bolt/transport/choria/agent_discovery.rb +32 -14
- data/lib/bolt/transport/choria/bolt_tasks.rb +64 -40
- data/lib/bolt/transport/choria/client.rb +66 -49
- data/lib/bolt/transport/choria/command_builders.rb +144 -24
- data/lib/bolt/transport/choria/file_transfer.rb +134 -0
- data/lib/bolt/transport/choria/helpers.rb +25 -20
- data/lib/bolt/transport/choria/shell.rb +255 -148
- data/lib/bolt/transport/choria.rb +96 -27
- data/lib/bolt/transport/ssh/connection.rb +2 -1
- data/lib/bolt/util.rb +13 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/plans/mock_executor.rb +4 -0
- data/libexec/apply_catalog.rb +3 -0
- metadata +22 -7
|
@@ -15,13 +15,13 @@ module Bolt
|
|
|
15
15
|
# @return [Array<Bolt::Result>] Results for all targets
|
|
16
16
|
def run_task_via_bolt_tasks(targets, task, arguments, result_opts, &callback)
|
|
17
17
|
logger.debug { "Running task #{task.name} via bolt_tasks agent on #{target_count(targets)}" }
|
|
18
|
-
results =
|
|
18
|
+
groups, results = group_by_implementation(targets, task, result_opts, &callback)
|
|
19
19
|
|
|
20
20
|
# Start all implementation groups. Each gets its own download +
|
|
21
21
|
# run_no_wait sequence. Tasks begin executing on nodes as soon as
|
|
22
22
|
# run_no_wait returns.
|
|
23
23
|
started_groups = []
|
|
24
|
-
|
|
24
|
+
groups.each do |implementation, impl_targets|
|
|
25
25
|
start_result = download_and_start_task(impl_targets, task, implementation,
|
|
26
26
|
arguments, result_opts, &callback)
|
|
27
27
|
results += start_result[:failed_results]
|
|
@@ -51,35 +51,35 @@ module Bolt
|
|
|
51
51
|
# @return [Hash] with keys:
|
|
52
52
|
# - :failed_results [Array<Bolt::Result>] Error results from setup phase
|
|
53
53
|
# - :targets [Array<Bolt::Target>] Targets that successfully started
|
|
54
|
-
# - :task_id [String
|
|
54
|
+
# - :task_id [String] Shared task ID for polling
|
|
55
|
+
# When nothing started, only :failed_results is set.
|
|
55
56
|
def download_and_start_task(targets, task, implementation, arguments, result_opts, &callback)
|
|
56
57
|
environment = targets.first.options['puppet-environment']
|
|
57
58
|
input_method = implementation['input_method']
|
|
58
59
|
impl_files = [{ 'name' => File.basename(implementation['name']), 'path' => implementation['path'] }] +
|
|
59
|
-
|
|
60
|
+
implementation['files']
|
|
60
61
|
file_specs_json = impl_files.map { |file| task_file_spec(file, task.module_name, environment) }.to_json
|
|
61
62
|
|
|
62
63
|
# The failed_results reference will get updated and if we ever end up without
|
|
63
64
|
# any targets left to act on, we can return it immediately.
|
|
64
65
|
failed_results = []
|
|
65
|
-
none_started_result = { failed_results: failed_results
|
|
66
|
+
none_started_result = { failed_results: failed_results }
|
|
66
67
|
|
|
67
68
|
# Download task files
|
|
68
69
|
logger.debug { "Downloading task #{task.name} files via bolt_tasks to #{target_count(targets)}" }
|
|
69
|
-
response = rpc_request('bolt_tasks', targets, 'bolt_tasks.download') do |client|
|
|
70
|
+
response = rpc_request('bolt_tasks', targets, 'bolt_tasks.download', accept_aborted: true) do |client|
|
|
70
71
|
client.download(task: task.name, files: file_specs_json, environment: environment)
|
|
71
72
|
end
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
73
|
+
# A download that fails reaches the client as an RPC error with
|
|
74
|
+
# statuscode 5, since the agent's download raises after its retries,
|
|
75
|
+
# and rpc_request puts it under :errors. A statuscode 1 reply stays
|
|
76
|
+
# under :responded here, since the call accepts aborted replies, so it
|
|
77
|
+
# moves to the errors too, instead of letting run_no_wait fail with a
|
|
78
|
+
# confusing "task not available" error.
|
|
77
79
|
dl_errors = response[:errors]
|
|
78
|
-
response[:
|
|
79
|
-
next if code.zero? || dl_errors.key?(target)
|
|
80
|
-
|
|
80
|
+
response[:aborted].each do |target, statusmsg|
|
|
81
81
|
dl_errors[target] = error_output(
|
|
82
|
-
"bolt_tasks.download on #{target.safe_name} failed to download task files",
|
|
82
|
+
"bolt_tasks.download on #{target.safe_name} failed to download task files: #{statusmsg}",
|
|
83
83
|
'bolt/choria-download-failed'
|
|
84
84
|
)
|
|
85
85
|
end
|
|
@@ -90,18 +90,29 @@ module Bolt
|
|
|
90
90
|
|
|
91
91
|
# Start task execution
|
|
92
92
|
logger.debug { "Starting task #{task.name} on #{target_count(remaining)}" }
|
|
93
|
-
response = rpc_request('bolt_tasks', remaining, 'bolt_tasks.run_no_wait') do |client|
|
|
93
|
+
response = rpc_request('bolt_tasks', remaining, 'bolt_tasks.run_no_wait', accept_aborted: true) do |client|
|
|
94
94
|
client.run_no_wait(task: task.name, input_method: input_method,
|
|
95
95
|
files: file_specs_json, input: arguments.to_json)
|
|
96
96
|
end
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
# A reply with statuscode 1 lands in :responded like a download
|
|
98
|
+
# failure does, so it is moved to the errors the same way. Otherwise
|
|
99
|
+
# the target would be polled for a task that never ran, or its reply
|
|
100
|
+
# without a task_id would stand in for the whole group.
|
|
101
|
+
start_errors = response[:errors]
|
|
102
|
+
response[:aborted].each do |target, statusmsg|
|
|
103
|
+
start_errors[target] = error_output(
|
|
104
|
+
"bolt_tasks.run_no_wait on #{target.safe_name} failed to start the task: #{statusmsg}",
|
|
105
|
+
'bolt/choria-task-start-failed'
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
failed_results.concat(emit_results(start_errors, **result_opts, &callback))
|
|
109
|
+
started = response[:responded].except(*start_errors.keys)
|
|
99
110
|
|
|
100
111
|
# Extract the shared task_id (all targets get the same one from
|
|
101
112
|
# the single run_no_wait call that fanned out to all of them)
|
|
102
|
-
task_id =
|
|
113
|
+
task_id = started.values.first&.dig(:task_id)
|
|
103
114
|
unless task_id
|
|
104
|
-
no_id_errors =
|
|
115
|
+
no_id_errors = started.each_with_object({}) do |(target, _), errors|
|
|
105
116
|
errors[target] = error_output(
|
|
106
117
|
"bolt_tasks.run_no_wait on #{target.safe_name} succeeded but returned no task_id",
|
|
107
118
|
'bolt/choria-missing-task-id'
|
|
@@ -111,8 +122,8 @@ module Bolt
|
|
|
111
122
|
return none_started_result
|
|
112
123
|
end
|
|
113
124
|
|
|
114
|
-
logger.debug { "Started task #{task.name} on #{target_count(
|
|
115
|
-
{ failed_results: failed_results, targets:
|
|
125
|
+
logger.debug { "Started task #{task.name} on #{target_count(started)}, task_id: #{task_id}" }
|
|
126
|
+
{ failed_results: failed_results, targets: started.keys, task_id: task_id }
|
|
116
127
|
end
|
|
117
128
|
|
|
118
129
|
# Poll bolt_tasks.task_status until all targets complete or timeout.
|
|
@@ -125,12 +136,14 @@ module Bolt
|
|
|
125
136
|
timeout = targets.first.options['task-timeout']
|
|
126
137
|
|
|
127
138
|
poll_result = poll_with_retries(targets, timeout, 'bolt_tasks.task_status') do |remaining|
|
|
128
|
-
|
|
139
|
+
# The agent answers statuscode 1 for a task that ran and failed,
|
|
140
|
+
# with the exit code and output in the data.
|
|
141
|
+
response = rpc_request('bolt_tasks', remaining, 'bolt_tasks.task_status', accept_aborted: true) do |client|
|
|
129
142
|
client.task_status(task_id: task_id)
|
|
130
143
|
end
|
|
131
|
-
next { rpc_failed: true
|
|
144
|
+
next { rpc_failed: true } if response[:rpc_failed]
|
|
132
145
|
|
|
133
|
-
done = response[:errors]
|
|
146
|
+
done = response[:errors]
|
|
134
147
|
|
|
135
148
|
response[:responded].each do |target, data|
|
|
136
149
|
if data.nil?
|
|
@@ -149,14 +162,8 @@ module Bolt
|
|
|
149
162
|
end
|
|
150
163
|
|
|
151
164
|
remaining_errors = poll_result[:remaining].each_with_object({}) do |target, errors|
|
|
152
|
-
errors[target] =
|
|
153
|
-
|
|
154
|
-
error_output("RPC requests to poll task status on #{target.safe_name} failed persistently",
|
|
155
|
-
'bolt/choria-poll-failed')
|
|
156
|
-
else
|
|
157
|
-
error_output("Task #{task.name} timed out after #{timeout} seconds on #{target.safe_name}",
|
|
158
|
-
'bolt/choria-task-timeout')
|
|
159
|
-
end
|
|
165
|
+
errors[target] = error_output("Task #{task.name} timed out after #{timeout} seconds on #{target.safe_name}",
|
|
166
|
+
'bolt/choria-task-timeout')
|
|
160
167
|
end
|
|
161
168
|
|
|
162
169
|
poll_result[:completed].merge(remaining_errors)
|
|
@@ -168,13 +175,14 @@ module Bolt
|
|
|
168
175
|
# @param target [Bolt::Target] Target for logging and stdout unwrapping context
|
|
169
176
|
# @return [Hash] Output hash from output() or error_output()
|
|
170
177
|
def extract_task_output(data, target)
|
|
171
|
-
exitcode = exitcode_from(data, target, 'task')
|
|
178
|
+
exitcode = exitcode_from(data[:exitcode], target, 'task')
|
|
172
179
|
output(stdout: unwrap_bolt_tasks_stdout(data[:stdout]),
|
|
173
180
|
stderr: data[:stderr], exitcode: exitcode)
|
|
174
181
|
end
|
|
175
182
|
|
|
176
183
|
# Build a file spec hash for the bolt_tasks download action. Computes
|
|
177
|
-
# the
|
|
184
|
+
# the server's file_content URI from the file's module-relative path,
|
|
185
|
+
# the way the server does it for the task metadata it serves.
|
|
178
186
|
#
|
|
179
187
|
# @param file [Hash] With 'name' (module-relative path) and 'path' (local absolute path)
|
|
180
188
|
# @param module_name [String] Task's module name (used for simple task files)
|
|
@@ -192,7 +200,10 @@ module Bolt
|
|
|
192
200
|
when 'files'
|
|
193
201
|
"/puppet/v3/file_content/modules/#{mod}/#{rest}"
|
|
194
202
|
when 'lib'
|
|
195
|
-
|
|
203
|
+
# The plugins mount serves the lib directories of every module as one tree
|
|
204
|
+
"/puppet/v3/file_content/plugins/#{rest}"
|
|
205
|
+
when 'scripts'
|
|
206
|
+
"/puppet/v3/file_content/scripts/#{mod}/#{rest}"
|
|
196
207
|
else
|
|
197
208
|
"/puppet/v3/file_content/tasks/#{mod}/#{rest}"
|
|
198
209
|
end
|
|
@@ -211,7 +222,8 @@ module Bolt
|
|
|
211
222
|
}
|
|
212
223
|
end
|
|
213
224
|
|
|
214
|
-
#
|
|
225
|
+
# Turn the stdout of a bolt_tasks task_status reply into what the task
|
|
226
|
+
# printed, for Result.for_task.
|
|
215
227
|
#
|
|
216
228
|
# Normally, create_task_stdout returns a Hash and reply_task_status
|
|
217
229
|
# calls .to_json on it, producing a single JSON string like:
|
|
@@ -228,8 +240,13 @@ module Bolt
|
|
|
228
240
|
# case, it produces a String (the inner JSON), which we return so
|
|
229
241
|
# Result.for_task can parse it.
|
|
230
242
|
#
|
|
243
|
+
# For a task that exits non-zero without an _error of its own,
|
|
244
|
+
# create_task_stdout adds one of kind choria.tasks/task-error. We take
|
|
245
|
+
# it out and return what the task printed, so Result.for_task reports
|
|
246
|
+
# the failure the way it does for the other transports.
|
|
247
|
+
#
|
|
231
248
|
# @param agent_stdout [String, nil] JSON-encoded stdout from the bolt_tasks agent
|
|
232
|
-
# @return [String, nil] JSON
|
|
249
|
+
# @return [String, nil] The text or JSON the task printed, for Result.for_task
|
|
233
250
|
def unwrap_bolt_tasks_stdout(agent_stdout)
|
|
234
251
|
return agent_stdout unless agent_stdout.is_a?(String)
|
|
235
252
|
|
|
@@ -239,9 +256,16 @@ module Bolt
|
|
|
239
256
|
return agent_stdout
|
|
240
257
|
end
|
|
241
258
|
|
|
242
|
-
# Normal case: parsed is a Hash, return the original JSON string.
|
|
243
259
|
# Double-encoded case: parsed is a String (the inner JSON), return it.
|
|
244
|
-
parsed.is_a?(String)
|
|
260
|
+
return parsed if parsed.is_a?(String)
|
|
261
|
+
|
|
262
|
+
# A Hash without the agent's error is the normal case and returns the original JSON string
|
|
263
|
+
agent_error = parsed.is_a?(Hash) && parsed['_error'].is_a?(Hash) && parsed['_error']['kind'] == 'choria.tasks/task-error'
|
|
264
|
+
return agent_stdout unless agent_error
|
|
265
|
+
|
|
266
|
+
# The agent puts output that isn't a JSON object under _output
|
|
267
|
+
printed = parsed.except('_error')
|
|
268
|
+
printed.keys == ['_output'] && printed['_output'].is_a?(String) ? printed['_output'] : printed.to_json
|
|
245
269
|
end
|
|
246
270
|
end
|
|
247
271
|
end
|
|
@@ -79,7 +79,7 @@ module Bolt
|
|
|
79
79
|
logger.debug { "Choria brokers overridden: #{brokers.join(', ')}" }
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
if opts['ssl-ca']
|
|
82
|
+
if opts['ssl-ca']
|
|
83
83
|
unreadable = %w[ssl-ca ssl-cert ssl-key].find { |key| !File.readable?(opts[key]) }
|
|
84
84
|
if unreadable
|
|
85
85
|
raise Bolt::Error.new(
|
|
@@ -101,9 +101,9 @@ module Bolt
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Create an MCollective::RPC::Client for one or more targets.
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
104
|
+
# Uses MCollective's direct addressing mode (client.discover(nodes:))
|
|
105
|
+
# to skip broadcast discovery and send requests directly to the
|
|
106
|
+
# specified nodes.
|
|
107
107
|
#
|
|
108
108
|
# Note that when the client is created, if the shell agent isn't already
|
|
109
109
|
# installed on the OpenBolt controller node, then the shell DDL that we
|
|
@@ -113,24 +113,16 @@ module Bolt
|
|
|
113
113
|
# DDL is already included in the choria-mcorpc-support gem.
|
|
114
114
|
#
|
|
115
115
|
# @param agent_name [String] MCollective agent name (e.g. 'shell', 'bolt_tasks')
|
|
116
|
-
# @param targets [
|
|
116
|
+
# @param targets [Array<Bolt::Target>] Targets to address
|
|
117
117
|
# @param timeout [Numeric] RPC call timeout in seconds
|
|
118
118
|
# @return [MCollective::RPC::Client] Configured client with direct addressing enabled
|
|
119
119
|
def create_rpc_client(agent_name, targets, timeout)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
options[:connection_timeout] = targets.first.options['broker-timeout']
|
|
120
|
+
client = MCollective::RPC::Client.new(agent_name, options: client_options(targets))
|
|
121
|
+
# Set after the build, which replaces a timeout of exactly 5, the
|
|
122
|
+
# gem's default, with the DDL timeout plus the discovery timeout.
|
|
123
|
+
client.timeout = timeout
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
options[:collective] = collective if collective
|
|
128
|
-
|
|
129
|
-
client = MCollective::RPC::Client.new(agent_name, options: options)
|
|
130
|
-
client.progress = false
|
|
131
|
-
|
|
132
|
-
identities = targets.map { |target| choria_identity(target) }.uniq
|
|
133
|
-
client.discover(nodes: identities)
|
|
125
|
+
client.discover(nodes: identities_of(targets))
|
|
134
126
|
|
|
135
127
|
client
|
|
136
128
|
end
|
|
@@ -140,13 +132,14 @@ module Bolt
|
|
|
140
132
|
#
|
|
141
133
|
# Results are split based on MCollective RPC statuscodes:
|
|
142
134
|
# - statuscode 0: action completed successfully (:responded)
|
|
143
|
-
# - statuscode 1 (RPCAborted): action
|
|
144
|
-
# problem (:
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
135
|
+
# - statuscode 1 (RPCAborted): the action could not run or reported a
|
|
136
|
+
# problem. An error (:errors) by default, since the shell and rpcutil
|
|
137
|
+
# agents never answer 1 with data the transport can use, and a denied
|
|
138
|
+
# action policy answers 1 as well. With accept_aborted set the reply
|
|
139
|
+
# stays under :responded with its data and its status message goes
|
|
140
|
+
# under :aborted, which bolt_tasks needs, since it answers 1 for a
|
|
141
|
+
# task that ran and failed with the data still valid. Such a caller
|
|
142
|
+
# must not assume :responded means success.
|
|
150
143
|
# - statuscode 2-5: RPC infrastructure error (:errors)
|
|
151
144
|
# - no response: target didn't reply (:errors)
|
|
152
145
|
# - exception: total RPC failure (rpc_failed: true)
|
|
@@ -159,27 +152,27 @@ module Bolt
|
|
|
159
152
|
# @param agent [String] MCollective agent name (e.g. 'shell', 'bolt_tasks', 'rpcutil')
|
|
160
153
|
# @param targets [Bolt::Target, Array<Bolt::Target>] One or more targets to address
|
|
161
154
|
# @param context [String] Human-readable label for logging (e.g. 'shell.start')
|
|
155
|
+
# @param accept_aborted [Boolean] Whether a statuscode 1 reply counts as a response, default is false
|
|
162
156
|
# @yield [MCollective::RPC::Client] The configured RPC client to invoke an action on
|
|
163
157
|
# @return [Hash] with keys:
|
|
164
|
-
# - :responded [Hash] Targets where the action completed (statuscode 0
|
|
165
|
-
# mapped to their response data
|
|
158
|
+
# - :responded [Hash] Targets where the action completed (statuscode 0, or 1 with
|
|
159
|
+
# accept_aborted), mapped to their response data
|
|
166
160
|
# - :errors [Hash] Targets with RPC errors or no response, mapped to error output hashes
|
|
167
161
|
# - :rpc_failed [Boolean] True when the entire RPC call failed
|
|
168
|
-
# - :
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
def rpc_request(agent, targets, context)
|
|
162
|
+
# - :aborted [Hash] Targets in :responded whose reply had statuscode 1,
|
|
163
|
+
# mapped to the agent's status message, so a caller can report the
|
|
164
|
+
# agent's reason
|
|
165
|
+
def rpc_request(agent, targets, context, accept_aborted: false)
|
|
172
166
|
targets = Array(targets)
|
|
173
167
|
rpc_results = @rpc_mutex.synchronize do
|
|
174
|
-
|
|
175
|
-
client = create_rpc_client(agent, targets, rpc_timeout)
|
|
168
|
+
client = create_rpc_client(agent, targets, targets.first.options['rpc-timeout'])
|
|
176
169
|
yield(client)
|
|
177
170
|
end
|
|
178
171
|
by_sender = index_results_by_sender(rpc_results, targets, context)
|
|
179
172
|
|
|
180
173
|
responded = {}
|
|
181
174
|
errors = {}
|
|
182
|
-
|
|
175
|
+
aborted = {}
|
|
183
176
|
targets.each do |target|
|
|
184
177
|
rpc_result = by_sender[choria_identity(target)]
|
|
185
178
|
if rpc_result.nil?
|
|
@@ -187,22 +180,21 @@ module Bolt
|
|
|
187
180
|
"No response from #{target.safe_name} for #{context}",
|
|
188
181
|
'bolt/choria-no-response'
|
|
189
182
|
)
|
|
190
|
-
elsif rpc_result[:statuscode] > 1
|
|
191
|
-
rpc_statuscodes[target] = rpc_result[:statuscode]
|
|
183
|
+
elsif rpc_result[:statuscode] > 1 || (rpc_result[:statuscode] == 1 && !accept_aborted)
|
|
192
184
|
errors[target] = error_output(
|
|
193
185
|
"#{context} on #{target.safe_name} returned RPC error: " \
|
|
194
186
|
"#{rpc_result[:statusmsg]} (code #{rpc_result[:statuscode]})",
|
|
195
187
|
'bolt/choria-rpc-error'
|
|
196
188
|
)
|
|
197
189
|
else
|
|
198
|
-
rpc_statuscodes[target] = rpc_result[:statuscode]
|
|
199
190
|
if rpc_result[:statuscode] == 1
|
|
200
191
|
logger.warn { "#{context} on #{target.safe_name} had RPC status code #{rpc_result[:statuscode]}: #{rpc_result[:statusmsg]}" }
|
|
192
|
+
aborted[target] = rpc_result[:statusmsg]
|
|
201
193
|
end
|
|
202
194
|
responded[target] = rpc_result[:data]
|
|
203
195
|
end
|
|
204
196
|
end
|
|
205
|
-
{ responded: responded, errors: errors, rpc_failed: false,
|
|
197
|
+
{ responded: responded, errors: errors, rpc_failed: false, aborted: aborted }
|
|
206
198
|
rescue StandardError => e
|
|
207
199
|
raise if e.is_a?(Bolt::Error)
|
|
208
200
|
|
|
@@ -211,11 +203,26 @@ module Bolt
|
|
|
211
203
|
errs[target] = error_output("#{context} failed on #{target.safe_name}: #{e.message}",
|
|
212
204
|
'bolt/choria-rpc-failed')
|
|
213
205
|
end
|
|
214
|
-
{ responded: {}, errors: errors, rpc_failed: true,
|
|
206
|
+
{ responded: {}, errors: errors, rpc_failed: true, aborted: {} }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The client options the targets' configuration gives: the broker
|
|
210
|
+
# timeout and the collective over the gem's defaults. The file
|
|
211
|
+
# transfer library builds its own clients from the same hash, under
|
|
212
|
+
# the same RPC mutex.
|
|
213
|
+
#
|
|
214
|
+
# @param targets [Array<Bolt::Target>] The targets of one batch
|
|
215
|
+
# @return [Hash]
|
|
216
|
+
def client_options(targets)
|
|
217
|
+
options = MCollective::Util.default_options
|
|
218
|
+
options[:connection_timeout] = targets.first.options['broker-timeout']
|
|
219
|
+
options[:collective] = collective_for(targets.first)
|
|
220
|
+
options
|
|
215
221
|
end
|
|
216
222
|
|
|
217
|
-
#
|
|
218
|
-
# availability, and emit errors for
|
|
223
|
+
# Fire node_start for every target, configure the client, discover
|
|
224
|
+
# agents, partition targets by agent availability, and emit errors for
|
|
225
|
+
# incapable targets.
|
|
219
226
|
#
|
|
220
227
|
# @param targets [Array<Bolt::Target>] Targets to prepare
|
|
221
228
|
# @param agent_name [String] Required agent name (e.g. 'shell', 'bolt_tasks')
|
|
@@ -225,24 +232,34 @@ module Bolt
|
|
|
225
232
|
# - [Array<Bolt::Target>] Targets that have the required agent
|
|
226
233
|
# - [Array<Bolt::Result>] Error results for targets that lack the agent
|
|
227
234
|
def prepare_targets(targets, agent_name, result_opts, &callback)
|
|
235
|
+
targets.each { |target| callback&.call(type: :node_start, target: target) }
|
|
228
236
|
configure_client(targets.first)
|
|
229
|
-
discover_agents(targets)
|
|
237
|
+
discovery_failures = discover_agents(targets)
|
|
230
238
|
|
|
231
239
|
capable, incapable = targets.partition { |target| has_agent?(target, agent_name) }
|
|
232
240
|
|
|
233
241
|
agent_errors = incapable.each_with_object({}) do |target, errors|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
errors[target] = error_output(msg, 'bolt/choria-agent-not-available')
|
|
242
|
+
errors[target] = if discovered?(target)
|
|
243
|
+
error_output("The '#{agent_name}' agent is not available on #{target.safe_name}.", 'bolt/choria-agent-not-available')
|
|
244
|
+
else
|
|
245
|
+
discovery_failures[target] || undiscovered_error(target)
|
|
246
|
+
end
|
|
240
247
|
end
|
|
241
|
-
incapable_results = emit_results(agent_errors,
|
|
248
|
+
incapable_results = emit_results(agent_errors, **result_opts, &callback)
|
|
242
249
|
|
|
243
250
|
[capable, incapable_results]
|
|
244
251
|
end
|
|
245
252
|
|
|
253
|
+
# The error output for a target that discovery has no agent list and no
|
|
254
|
+
# failure for, which happens when discovery raised before it finished.
|
|
255
|
+
#
|
|
256
|
+
# @param target [Bolt::Target]
|
|
257
|
+
# @return [Hash] An error output hash
|
|
258
|
+
def undiscovered_error(target)
|
|
259
|
+
error_output("No agent information available for #{target.safe_name} (agent discovery failed)",
|
|
260
|
+
'bolt/choria-agent-not-available')
|
|
261
|
+
end
|
|
262
|
+
|
|
246
263
|
# Index RPC results by sender, keeping only the first response per
|
|
247
264
|
# sender and only from the set of expected identities. Logs and discards
|
|
248
265
|
# responses from unexpected senders and duplicates.
|