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
|
@@ -23,6 +23,29 @@ module Bolt
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Build the command that creates a tmpdir. On POSIX it is mkdir -m 700
|
|
27
|
+
# without -p, so it fails when the directory the tmpdir goes into
|
|
28
|
+
# doesn't exist, like it does with the SSH transport. The Windows
|
|
29
|
+
# command leaves the tmpdir open only to the account the agent runs as
|
|
30
|
+
# and to the Administrators group, which the directories and files in
|
|
31
|
+
# it inherit, and it fails when it cannot set that.
|
|
32
|
+
#
|
|
33
|
+
# @param target [Bolt::Target] Used for platform detection
|
|
34
|
+
# @param tmpdir [String] Absolute path to the tmpdir on the remote node
|
|
35
|
+
# @return [String] Shell command
|
|
36
|
+
def make_tmpdir_command(target, tmpdir)
|
|
37
|
+
return "mkdir -m 700 #{Shellwords.shellescape(tmpdir)}" unless windows_target?(target)
|
|
38
|
+
|
|
39
|
+
"$ErrorActionPreference = 'Stop'; $dir = '#{ps_escape(tmpdir)}'; " \
|
|
40
|
+
'$null = New-Item -ItemType Directory -Force -Path $dir; ' \
|
|
41
|
+
'$acl = Get-Acl -LiteralPath $dir; $acl.SetAccessRuleProtection($true, $false); ' \
|
|
42
|
+
'foreach ($sid in @([Security.Principal.WindowsIdentity]::GetCurrent().User, ' \
|
|
43
|
+
"(New-Object Security.Principal.SecurityIdentifier('S-1-5-32-544')))) { " \
|
|
44
|
+
'$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($sid, ' \
|
|
45
|
+
"'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))) }; " \
|
|
46
|
+
'Set-Acl -LiteralPath $dir -AclObject $acl'
|
|
47
|
+
end
|
|
48
|
+
|
|
26
49
|
# Build a chmod +x command. Returns nil on Windows (not needed).
|
|
27
50
|
#
|
|
28
51
|
# @param target [Bolt::Target] Used for platform detection
|
|
@@ -43,19 +66,88 @@ module Bolt
|
|
|
43
66
|
"rm -rf #{Shellwords.shellescape(path)}"
|
|
44
67
|
end
|
|
45
68
|
|
|
69
|
+
# Build a command that removes the shell agent's job directories for
|
|
70
|
+
# the given handles, under the agent's default state path. A handle
|
|
71
|
+
# without a directory on the target is skipped, so 1 command can
|
|
72
|
+
# contain the handles of every target it goes to. On Windows a failed
|
|
73
|
+
# removal stops the command, so it exits 1 like a failed rm does.
|
|
74
|
+
#
|
|
75
|
+
# @param target [Bolt::Target] Used for platform detection
|
|
76
|
+
# @param handles [Array<String>] Job handles, each checked to be a UUID
|
|
77
|
+
# @return [String] Shell command
|
|
78
|
+
def remove_job_dirs_command(target, handles)
|
|
79
|
+
if windows_target?(target)
|
|
80
|
+
paths = handles.map { |handle| "'C:/ProgramData/mcollective-shell/#{handle}'" }.join(', ')
|
|
81
|
+
"foreach ($path in @(#{paths})) { if (Test-Path -LiteralPath $path) { " \
|
|
82
|
+
'Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction Stop } }'
|
|
83
|
+
else
|
|
84
|
+
"rm -rf #{handles.map { |handle| "/var/run/mcollective-shell/#{handle}" }.join(' ')}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
46
88
|
# Build a command that writes base64-encoded content to a file
|
|
47
89
|
# after decoding the content. Requires base64 CLI on POSIX targets.
|
|
48
90
|
#
|
|
49
91
|
# @param target [Bolt::Target] Used for platform detection
|
|
50
92
|
# @param content_b64 [String] Base64-encoded file content
|
|
51
93
|
# @param dest [String] Absolute destination path on the remote node
|
|
94
|
+
# @param append [Boolean] Whether to add the content to the end of the file instead of replacing it
|
|
52
95
|
# @return [String] Shell command
|
|
53
|
-
def upload_file_command(target, content_b64, dest)
|
|
54
|
-
if windows_target?(target)
|
|
96
|
+
def upload_file_command(target, content_b64, dest, append: false)
|
|
97
|
+
if windows_target?(target) && append
|
|
98
|
+
"$bytes = [Convert]::FromBase64String('#{content_b64}'); " \
|
|
99
|
+
"$stream = [IO.File]::Open('#{ps_escape(dest)}', 'Append', 'Write'); " \
|
|
100
|
+
'try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Close() }'
|
|
101
|
+
elsif windows_target?(target)
|
|
55
102
|
"[IO.File]::WriteAllBytes('#{ps_escape(dest)}', " \
|
|
56
103
|
"[Convert]::FromBase64String('#{content_b64}'))"
|
|
57
104
|
else
|
|
58
|
-
"printf '%s' #{Shellwords.shellescape(content_b64)} | base64 -d > #{Shellwords.shellescape(dest)}"
|
|
105
|
+
"printf '%s' #{Shellwords.shellescape(content_b64)} | base64 -d #{append ? '>>' : '>'} #{Shellwords.shellescape(dest)}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The path of the run file in a tmpdir. A script or task command can
|
|
110
|
+
# contain arguments, env var values and task parameters, so it goes to
|
|
111
|
+
# the nodes in this file instead of as the shell agent's start command,
|
|
112
|
+
# which the agent keeps on disk and returns in shell.list replies.
|
|
113
|
+
#
|
|
114
|
+
# @param target [Bolt::Target] Used for platform detection
|
|
115
|
+
# @param tmpdir [String] Absolute path to the tmpdir on the remote node
|
|
116
|
+
# @return [String] Absolute path to the run file
|
|
117
|
+
def run_file_path(target, tmpdir)
|
|
118
|
+
join_path(target, tmpdir, windows_target?(target) ? 'bolt_run.ps1' : 'bolt_run.sh')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build the content of a run file. On POSIX it removes itself as soon
|
|
122
|
+
# as sh starts reading it, which keeps reading from the open file. A
|
|
123
|
+
# single command then replaces sh through exec, so shell.kill reaches
|
|
124
|
+
# it. A pipeline doesn't, since exec would run printf as its own
|
|
125
|
+
# process with the task input in its arguments. On Windows the content
|
|
126
|
+
# runs in a script block, which would hide a failing last command from
|
|
127
|
+
# powershell.exe, so it exits 1 then, as the command itself would.
|
|
128
|
+
#
|
|
129
|
+
# @param target [Bolt::Target] Used for platform detection
|
|
130
|
+
# @param command [String] The command the run file contains
|
|
131
|
+
# @param pipeline [Boolean] Whether the command is a pipeline, default is false
|
|
132
|
+
# @return [String] File content
|
|
133
|
+
def run_file_content(target, command, pipeline: false)
|
|
134
|
+
return "#{command}\nif (-not $?) { exit 1 }" if windows_target?(target)
|
|
135
|
+
|
|
136
|
+
"rm -f -- \"$0\"\n#{'exec ' unless pipeline}#{command}\n"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Build the start command that runs a run file. On Windows it reads the
|
|
140
|
+
# file and removes it before running what was in it.
|
|
141
|
+
#
|
|
142
|
+
# @param target [Bolt::Target] Used for platform detection
|
|
143
|
+
# @param path [String] Absolute path to the run file
|
|
144
|
+
# @return [String] Shell command
|
|
145
|
+
def run_file_command(target, path)
|
|
146
|
+
if windows_target?(target)
|
|
147
|
+
"$boltRunFile = '#{ps_escape(path)}'; $boltRunScript = [IO.File]::ReadAllText($boltRunFile); " \
|
|
148
|
+
'Remove-Item -LiteralPath $boltRunFile -Force; & ([ScriptBlock]::Create($boltRunScript))'
|
|
149
|
+
else
|
|
150
|
+
"sh #{Shellwords.shellescape(path)}"
|
|
59
151
|
end
|
|
60
152
|
end
|
|
61
153
|
|
|
@@ -64,46 +156,58 @@ module Bolt
|
|
|
64
156
|
#
|
|
65
157
|
# @param target [Bolt::Target] Used for platform detection
|
|
66
158
|
# @param command [String] The command to prepend env vars to
|
|
67
|
-
# @param env_vars [Hash{String =>
|
|
159
|
+
# @param env_vars [Hash{String => Object}, nil] Variable names to values, which become strings
|
|
68
160
|
# @param context [String] Description for error messages (e.g., 'task argument')
|
|
69
161
|
# @return [String] Command with env vars prepended
|
|
70
162
|
def prepend_env_vars(target, command, env_vars, context)
|
|
71
163
|
return command unless env_vars&.any?
|
|
72
164
|
|
|
73
|
-
env_vars.each_key { |key| validate_env_key!(key, context) }
|
|
74
|
-
|
|
75
165
|
if windows_target?(target)
|
|
76
|
-
|
|
77
|
-
"#{set_stmts.join('; ')}; & #{command}"
|
|
166
|
+
"#{powershell_env_statements(env_vars, context)}; #{command}"
|
|
78
167
|
else
|
|
168
|
+
env_vars.each_key { |key| validate_env_key!(key, context) }
|
|
79
169
|
env_str = env_vars.map { |key, val| "#{key}=#{Shellwords.shellescape(val)}" }.join(' ')
|
|
80
170
|
"/usr/bin/env #{env_str} #{command}"
|
|
81
171
|
end
|
|
82
172
|
end
|
|
83
173
|
|
|
174
|
+
# Build the PowerShell statements that set environment variables,
|
|
175
|
+
# after validating each name.
|
|
176
|
+
#
|
|
177
|
+
# @param env_vars [Hash{String => Object}] Variable names to values, which become strings
|
|
178
|
+
# @param context [String] Description for error messages (e.g., 'task argument')
|
|
179
|
+
# @return [String] The statements joined with '; '
|
|
180
|
+
def powershell_env_statements(env_vars, context)
|
|
181
|
+
env_vars.each_key { |key| validate_env_key!(key, context) }
|
|
182
|
+
env_vars.map { |key, val| "$env:#{key} = '#{ps_escape(val.to_s)}'" }.join('; ')
|
|
183
|
+
end
|
|
184
|
+
|
|
84
185
|
# Build a command that pipes data to another command via stdin.
|
|
85
186
|
#
|
|
86
187
|
# @param target [Bolt::Target] Used for platform detection
|
|
87
188
|
# @param data [String] Data to pipe (typically JSON task arguments)
|
|
88
|
-
# @param command [String] The command to receive stdin
|
|
189
|
+
# @param command [String] The command to receive stdin, which on Windows must
|
|
190
|
+
# be a PowerShell command such as one from executable_command
|
|
89
191
|
# @return [String] Shell command with stdin piping
|
|
90
192
|
def stdin_pipe_command(target, data, command)
|
|
91
193
|
if windows_target?(target)
|
|
92
194
|
# Use a here-string (@'...'@) to avoid escaping issues with
|
|
93
|
-
# large JSON payloads.
|
|
94
|
-
|
|
195
|
+
# large JSON payloads. Its content is literal as long as no line
|
|
196
|
+
# starts with a single quote character and @, which single-line
|
|
197
|
+
# JSON never does.
|
|
198
|
+
"@'\n#{data}\n'@ | #{command}"
|
|
95
199
|
else
|
|
96
200
|
"printf '%s' #{Shellwords.shellescape(data)} | #{command}"
|
|
97
201
|
end
|
|
98
202
|
end
|
|
99
203
|
|
|
100
|
-
# Escape a
|
|
204
|
+
# Escape a value for use as a shell argument on the target platform.
|
|
101
205
|
#
|
|
102
206
|
# @param target [Bolt::Target] Used for platform detection
|
|
103
|
-
# @param str [
|
|
207
|
+
# @param str [Object] The value to escape, turned into a string first
|
|
104
208
|
# @return [String] Escaped string (single-quoted on Windows, sh-escaped on POSIX)
|
|
105
209
|
def escape_arg(target, str)
|
|
106
|
-
windows_target?(target) ? "'#{ps_escape(str)}'" : Shellwords.shellescape(str)
|
|
210
|
+
windows_target?(target) ? "'#{ps_escape(str.to_s)}'" : Shellwords.shellescape(str)
|
|
107
211
|
end
|
|
108
212
|
|
|
109
213
|
# Join path segments using the target platform's separator.
|
|
@@ -132,9 +236,22 @@ module Bolt
|
|
|
132
236
|
# Escape single quotes for use inside PowerShell single-quoted strings.
|
|
133
237
|
#
|
|
134
238
|
# @param str [String] String to escape
|
|
135
|
-
# @return [String] String with single
|
|
239
|
+
# @return [String] String with every single quote character doubled
|
|
136
240
|
def ps_escape(str)
|
|
137
|
-
|
|
241
|
+
Bolt::Util.escape_powershell_single_quotes(str)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Build the command that runs an executable, through the interpreter
|
|
245
|
+
# when one is given. On Windows it starts with the call operator,
|
|
246
|
+
# since PowerShell treats a quoted path as a string.
|
|
247
|
+
#
|
|
248
|
+
# @param target [Bolt::Target] Used for platform detection
|
|
249
|
+
# @param path [String] Absolute path to the executable on the remote node
|
|
250
|
+
# @param interpreter [String, Array<String>, nil] Interpreter command, or nil to run the file directly
|
|
251
|
+
# @return [String] Shell command
|
|
252
|
+
def executable_command(target, path, interpreter)
|
|
253
|
+
command = (Array(interpreter) + [path]).map { |part| escape_arg(target, part) }.join(' ')
|
|
254
|
+
windows_target?(target) ? "& #{command}" : command
|
|
138
255
|
end
|
|
139
256
|
|
|
140
257
|
# Build the full command string for task execution via the shell agent,
|
|
@@ -144,14 +261,19 @@ module Bolt
|
|
|
144
261
|
# @param target [Bolt::Target] Target (used for platform detection)
|
|
145
262
|
# @param remote_task_path [String] Absolute path to the task executable on the remote node
|
|
146
263
|
# @param arguments [Hash] Task parameter names to values
|
|
147
|
-
# @param input_method [String] How to pass arguments: 'stdin', 'environment', or '
|
|
264
|
+
# @param input_method [String] How to pass arguments: 'stdin', 'environment', 'both', or 'powershell'
|
|
148
265
|
# @param interpreter_options [Hash{String => String}] File extension to interpreter path mapping
|
|
149
266
|
# @return [String] The fully constructed shell command
|
|
150
267
|
def build_task_command(target, remote_task_path, arguments, input_method, interpreter_options)
|
|
268
|
+
if input_method == 'powershell' && windows_target?(target) && File.extname(remote_task_path).casecmp?('.ps1')
|
|
269
|
+
# The arguments become the script's named parameters, like they
|
|
270
|
+
# do with the WinRM transport
|
|
271
|
+
return Bolt::Shell::Powershell::Snippets.json_functions +
|
|
272
|
+
Bolt::Shell::Powershell::Snippets.ps_task(remote_task_path, arguments)
|
|
273
|
+
end
|
|
274
|
+
|
|
151
275
|
interpreter = select_interpreter(remote_task_path, interpreter_options)
|
|
152
|
-
cmd = interpreter
|
|
153
|
-
"#{Array(interpreter).map { |part| escape_arg(target, part) }.join(' ')} #{escape_arg(target, remote_task_path)}" :
|
|
154
|
-
escape_arg(target, remote_task_path)
|
|
276
|
+
cmd = executable_command(target, remote_task_path, interpreter)
|
|
155
277
|
|
|
156
278
|
needs_env = Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
|
|
157
279
|
needs_stdin = Bolt::Task::STDIN_METHODS.include?(input_method)
|
|
@@ -161,11 +283,9 @@ module Bolt
|
|
|
161
283
|
# requires a script block. Pipeline data doesn't automatically
|
|
162
284
|
# flow through a script block to inner commands, so we
|
|
163
285
|
# explicitly forward $input via a pipe.
|
|
164
|
-
|
|
165
|
-
env_params.each_key { |key| validate_env_key!(key, 'task argument') }
|
|
166
|
-
set_stmts = env_params.map { |key, val| "$env:#{key} = '#{ps_escape(val)}'" }
|
|
286
|
+
env_statements = powershell_env_statements(envify_params(arguments), 'task argument')
|
|
167
287
|
cmd = stdin_pipe_command(target, arguments.to_json,
|
|
168
|
-
"{ #{
|
|
288
|
+
"& { #{env_statements}; $input | #{cmd} }")
|
|
169
289
|
else
|
|
170
290
|
if needs_env
|
|
171
291
|
cmd = prepend_env_vars(target, cmd, envify_params(arguments), 'task argument')
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bolt
|
|
4
|
+
module Transport
|
|
5
|
+
class Choria
|
|
6
|
+
# Upload and download through the file_transfer agent. The transfers
|
|
7
|
+
# themselves are done by the client library the agent's Puppet module
|
|
8
|
+
# installs on the controller, MCollective::Util::FileTransfer. This file
|
|
9
|
+
# maps targets, options and results onto it.
|
|
10
|
+
|
|
11
|
+
# The library's outcome kinds as this transport's error kinds. A kind
|
|
12
|
+
# a later library adds is reported as a transfer failure.
|
|
13
|
+
FILE_TRANSFER_ERROR_KINDS = {
|
|
14
|
+
no_response: 'bolt/choria-no-response',
|
|
15
|
+
rpc_error: 'bolt/choria-rpc-error',
|
|
16
|
+
rpc_failed: 'bolt/choria-rpc-failed',
|
|
17
|
+
transfer_failed: 'bolt/choria-file-transfer-failed',
|
|
18
|
+
payload_too_large: 'bolt/choria-payload-too-large',
|
|
19
|
+
checksum_mismatch: 'bolt/choria-checksum-mismatch'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Upload one file to the capable targets through the library.
|
|
23
|
+
def batch_upload(targets, source, destination, _options = {}, position = [], &callback)
|
|
24
|
+
result_opts = { action: 'upload', name: source, position: position }
|
|
25
|
+
capable, results = prepare_targets(targets, 'file_transfer', result_opts, &callback)
|
|
26
|
+
return results if capable.empty?
|
|
27
|
+
|
|
28
|
+
logger.debug { "Uploading #{source} to #{destination} on #{target_count(capable)}" }
|
|
29
|
+
results + transfer(capable, result_opts, callback) do |client|
|
|
30
|
+
outcomes = client.upload(source, destination, identities_of(capable))
|
|
31
|
+
file_transfer_results(capable, outcomes, result_opts, callback) do |target|
|
|
32
|
+
Bolt::Result.for_upload(target, source, destination)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Download a path from each capable target into its own directory.
|
|
38
|
+
def batch_download(targets, source, destination, _options = {}, position = [], &callback)
|
|
39
|
+
directories = targets.to_h { |target| [target, download_dir(destination, target)] }
|
|
40
|
+
download_into(directories, source, position, &callback)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Download a path from each capable target into the directory given
|
|
44
|
+
# for it.
|
|
45
|
+
#
|
|
46
|
+
# @param directories [Hash{Bolt::Target => String}] Each target and the local directory its download goes into
|
|
47
|
+
# @param source [String] The path to download from each target
|
|
48
|
+
# @param position [Array] Positional info for result tracking
|
|
49
|
+
# @param callback [Proc] Called with :node_start and :node_result events
|
|
50
|
+
# @return [Array<Bolt::Result>]
|
|
51
|
+
def download_into(directories, source, position, &callback)
|
|
52
|
+
result_opts = { action: 'download', name: source, position: position }
|
|
53
|
+
capable, results = prepare_targets(directories.keys, 'file_transfer', result_opts, &callback)
|
|
54
|
+
return results if capable.empty?
|
|
55
|
+
|
|
56
|
+
logger.debug { "Downloading #{source} from #{target_count(capable)}" }
|
|
57
|
+
# The library takes one directory per identity, so targets behind one
|
|
58
|
+
# identity share the last one's directory.
|
|
59
|
+
destination_dirs = capable.to_h { |target| [choria_identity(target), directories.fetch(target)] }
|
|
60
|
+
results + transfer(capable, result_opts, callback) do |client|
|
|
61
|
+
outcomes = client.download(source, destination_dirs)
|
|
62
|
+
file_transfer_results(capable, outcomes, result_opts, callback) do |target, outcome|
|
|
63
|
+
Bolt::Result.for_download(target, source, destination_dirs.fetch(choria_identity(target)), outcome.path)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Runs one transfer for the targets with a library client and returns a
|
|
69
|
+
# result per target. A library that isn't installed on this host, or an
|
|
70
|
+
# error raised for what the controller contains, such as a source that
|
|
71
|
+
# can't be read, is a result for each of the targets.
|
|
72
|
+
#
|
|
73
|
+
# @param targets [Array<Bolt::Target>] The capable targets of one batch
|
|
74
|
+
# @param result_opts [Hash] Options passed through to emit_results (:action, :name, :position)
|
|
75
|
+
# @param callback [Proc, nil] Called with :node_result events
|
|
76
|
+
# @yield [MCollective::Util::FileTransfer::Client] The client for the targets
|
|
77
|
+
# @return [Array<Bolt::Result>]
|
|
78
|
+
def transfer(targets, result_opts, callback)
|
|
79
|
+
yield file_transfer_client(targets)
|
|
80
|
+
rescue Bolt::Error => e
|
|
81
|
+
failures = targets.to_h { |target| [target, error_output(e.message, e.kind)] }
|
|
82
|
+
emit_results(failures, **result_opts, &callback)
|
|
83
|
+
rescue SystemCallError, ArgumentError => e
|
|
84
|
+
logger.warn { "The file transfer raised #{e.class} on the controller: #{e.message}" }
|
|
85
|
+
logger.debug { e.backtrace.join("\n") }
|
|
86
|
+
message = "#{e.message} (#{e.class} on the controller)"
|
|
87
|
+
failures = targets.to_h { |target| [target, error_output(message, FILE_TRANSFER_ERROR_KINDS[:transfer_failed])] }
|
|
88
|
+
emit_results(failures, **result_opts, &callback)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A library client for the targets, with the batch's options. The
|
|
92
|
+
# library builds its connection from the options this transport's own
|
|
93
|
+
# clients get and the RPC mutex every other call in the process takes.
|
|
94
|
+
# A timeout or size the batch does not set is left to the library's
|
|
95
|
+
# default.
|
|
96
|
+
def file_transfer_client(targets)
|
|
97
|
+
load_file_transfer_library
|
|
98
|
+
options = targets.first.options
|
|
99
|
+
settings = { rpc_timeout: options['file-transfer-timeout'], message_data_size: options['message-data-size'],
|
|
100
|
+
upload_batch_size: options['upload-batch-size'] }
|
|
101
|
+
MCollective::Util::FileTransfer::Client.new(client_options(targets), mutex: @rpc_mutex, logger: logger, **settings.compact)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The library is a client file of the agent's Puppet module under the
|
|
105
|
+
# libdir the Choria client config names, which loading that config in
|
|
106
|
+
# configure_client put on the load path.
|
|
107
|
+
def load_file_transfer_library
|
|
108
|
+
require 'mcollective/util/file_transfer'
|
|
109
|
+
rescue LoadError => e
|
|
110
|
+
raise Bolt::Error.new(
|
|
111
|
+
'The file_transfer client library is not installed on this host. Apply the mcollective_agent_file_transfer ' \
|
|
112
|
+
"module with client => true where OpenBolt runs, which installs it under the Choria libdir (#{e.class}: #{e.message}).",
|
|
113
|
+
'bolt/choria-file-transfer-library'
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A result per target from the outcomes by identity, firing
|
|
118
|
+
# node_result for each. The block builds the result of a success.
|
|
119
|
+
def file_transfer_results(targets, outcomes, result_opts, callback)
|
|
120
|
+
targets.map do |target|
|
|
121
|
+
outcome = outcomes.fetch(choria_identity(target))
|
|
122
|
+
result = if outcome.success?
|
|
123
|
+
yield(target, outcome)
|
|
124
|
+
else
|
|
125
|
+
kind = FILE_TRANSFER_ERROR_KINDS.fetch(outcome.error_kind, FILE_TRANSFER_ERROR_KINDS[:transfer_failed])
|
|
126
|
+
build_result(target, error_output(outcome.error_message, kind), **result_opts)
|
|
127
|
+
end
|
|
128
|
+
callback&.call(type: :node_result, result: result)
|
|
129
|
+
result
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -14,7 +14,7 @@ module Bolt
|
|
|
14
14
|
WINDOWS_PATH_REGEX = %r{\A[A-Za-z]:[\\/]}
|
|
15
15
|
|
|
16
16
|
def target_count(targets)
|
|
17
|
-
count = targets.
|
|
17
|
+
count = targets.size
|
|
18
18
|
"#{count} #{count == 1 ? 'target' : 'targets'}"
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -26,11 +26,11 @@ module Bolt
|
|
|
26
26
|
#
|
|
27
27
|
# @param targets [Array, Hash] Initial targets to poll (duped internally)
|
|
28
28
|
# @param timeout [Numeric] Maximum seconds before exiting
|
|
29
|
-
# @param context [String] Label for log messages
|
|
29
|
+
# @param context [String] Label for log and error messages
|
|
30
30
|
# @return [Hash] with keys:
|
|
31
|
-
# - :completed [Hash{Target => Hash}] All finished target outputs
|
|
32
|
-
#
|
|
33
|
-
# - :
|
|
31
|
+
# - :completed [Hash{Target => Hash}] All finished target outputs, plus a poll
|
|
32
|
+
# error for each pending target after RPC_FAILURE_RETRIES failed polls in a row
|
|
33
|
+
# - :remaining [Array, Hash] Targets still pending at the deadline
|
|
34
34
|
def poll_with_retries(targets, timeout, context)
|
|
35
35
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
36
36
|
remaining = targets.dup
|
|
@@ -62,8 +62,17 @@ module Bolt
|
|
|
62
62
|
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
if poll_failures >= RPC_FAILURE_RETRIES
|
|
66
|
+
(remaining.is_a?(Hash) ? remaining.keys : remaining).each do |target|
|
|
67
|
+
completed[target] = error_output(
|
|
68
|
+
"RPC requests to poll #{context} on #{target.safe_name} failed persistently",
|
|
69
|
+
'bolt/choria-poll-failed'
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
remaining.clear
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
{ completed: completed, remaining: remaining }
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
# Build a Bolt::Result from an output hash. Handles both success and
|
|
@@ -100,19 +109,16 @@ module Bolt
|
|
|
100
109
|
end
|
|
101
110
|
|
|
102
111
|
# Convert a hash of { target => output } into Results, fire callbacks,
|
|
103
|
-
# and return the Results array.
|
|
104
|
-
# :node_start callback before each :node_result.
|
|
112
|
+
# and return the Results array.
|
|
105
113
|
#
|
|
106
114
|
# @param target_outputs [Hash{Bolt::Target => Hash}] Map of targets to output hashes
|
|
107
115
|
# @param action [String] One of 'task', 'command', or 'script'
|
|
108
116
|
# @param name [String] Task/command/script name for result metadata
|
|
109
117
|
# @param position [Array] Positional info for result tracking
|
|
110
|
-
# @param
|
|
111
|
-
# @param callback [Proc] Called with :node_start and :node_result events
|
|
118
|
+
# @param callback [Proc] Called with :node_result events
|
|
112
119
|
# @return [Array<Bolt::Result>] Results for all targets in the hash
|
|
113
|
-
def emit_results(target_outputs, action:, name:, position:,
|
|
120
|
+
def emit_results(target_outputs, action:, name:, position:, &callback)
|
|
114
121
|
target_outputs.map do |target, data|
|
|
115
|
-
callback&.call(type: :node_start, target: target) if fire_node_start
|
|
116
122
|
result = build_result(target, data, action: action, name: name, position: position)
|
|
117
123
|
callback&.call(type: :node_result, result: result)
|
|
118
124
|
result
|
|
@@ -120,8 +126,8 @@ module Bolt
|
|
|
120
126
|
end
|
|
121
127
|
|
|
122
128
|
# Build an output hash from command/task output.
|
|
123
|
-
def output(stdout
|
|
124
|
-
{ stdout: stdout || '', stderr: stderr || '', exitcode: exitcode
|
|
129
|
+
def output(stdout:, stderr:, exitcode:)
|
|
130
|
+
{ stdout: stdout || '', stderr: stderr || '', exitcode: exitcode }
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
# Build an error output hash. When actual output is available (e.g.
|
|
@@ -131,15 +137,14 @@ module Bolt
|
|
|
131
137
|
.merge(error: message, error_kind: kind)
|
|
132
138
|
end
|
|
133
139
|
|
|
134
|
-
#
|
|
140
|
+
# Return the exit code an agent reported, defaulting to 1 with a
|
|
135
141
|
# warning if the agent returned nil.
|
|
136
142
|
#
|
|
137
|
-
# @param
|
|
143
|
+
# @param exitcode [Integer, nil] The exit code from the RPC response data
|
|
138
144
|
# @param target [Bolt::Target] Target for logging context
|
|
139
145
|
# @param context [String] Human-readable label for the log message
|
|
140
|
-
# @return [Integer] The exit code
|
|
141
|
-
def exitcode_from(
|
|
142
|
-
exitcode = data[:exitcode] || data['exitcode']
|
|
146
|
+
# @return [Integer] The exit code, or 1 if nil
|
|
147
|
+
def exitcode_from(exitcode, target, context)
|
|
143
148
|
if exitcode.nil?
|
|
144
149
|
logger.warn {
|
|
145
150
|
"Agent on #{target.safe_name} returned no exit code for #{context}. " \
|