openbolt 9.0.0.pre.rc1 → 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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +5 -5
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -2
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +2 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +1 -1
  8. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +1 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +1 -1
  12. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +27 -4
  13. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +2 -2
  14. data/bolt-modules/file/lib/puppet/functions/file/delete.rb +1 -1
  15. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -1
  16. data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -1
  17. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +1 -1
  18. data/bolt-modules/file/lib/puppet/functions/file/write.rb +1 -1
  19. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +1 -1
  20. data/bolt-modules/log/lib/puppet/functions/log/error.rb +1 -1
  21. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +1 -1
  22. data/bolt-modules/log/lib/puppet/functions/log/info.rb +1 -1
  23. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +1 -1
  24. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +1 -1
  25. data/guides/targets.yaml +2 -0
  26. data/lib/bolt/bolt_option_parser.rb +53 -53
  27. data/lib/bolt/cli.rb +21 -2
  28. data/lib/bolt/config/options.rb +18 -18
  29. data/lib/bolt/config/transport/choria.rb +3 -0
  30. data/lib/bolt/config/transport/options.rb +34 -11
  31. data/lib/bolt/executor.rb +4 -0
  32. data/lib/bolt/fiber_executor.rb +46 -2
  33. data/lib/bolt/inventory/options.rb +1 -1
  34. data/lib/bolt/plan_future.rb +5 -1
  35. data/lib/bolt/shell/bash.rb +5 -3
  36. data/lib/bolt/shell/powershell/snippets.rb +20 -1
  37. data/lib/bolt/shell/powershell.rb +2 -2
  38. data/lib/bolt/transport/base.rb +9 -4
  39. data/lib/bolt/transport/choria/agent_discovery.rb +32 -14
  40. data/lib/bolt/transport/choria/bolt_tasks.rb +64 -40
  41. data/lib/bolt/transport/choria/client.rb +66 -49
  42. data/lib/bolt/transport/choria/command_builders.rb +144 -24
  43. data/lib/bolt/transport/choria/file_transfer.rb +134 -0
  44. data/lib/bolt/transport/choria/helpers.rb +25 -20
  45. data/lib/bolt/transport/choria/shell.rb +255 -148
  46. data/lib/bolt/transport/choria.rb +96 -27
  47. data/lib/bolt/util.rb +13 -0
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_spec/plans/mock_executor.rb +4 -0
  50. data/libexec/apply_catalog.rb +3 -0
  51. metadata +5 -4
@@ -6,6 +6,8 @@ require 'digest/sha2'
6
6
  require 'json'
7
7
  require 'securerandom'
8
8
  require 'shellwords'
9
+ require_relative '../../bolt/node/errors'
10
+ require_relative '../../bolt/shell'
9
11
  require_relative '../../bolt/transport/base'
10
12
 
11
13
  module Bolt
@@ -21,15 +23,19 @@ module Bolt
21
23
  #
22
24
  # bolt_tasks agent only: Only run_task works, via the bolt_tasks agent
23
25
  # which downloads task files from an OpenVox/Puppet Server and executes
24
- # them via task_wrapper. All other operations fail with an actionable
25
- # error directing the user to install the shell agent.
26
+ # them via task_wrapper. All other operations fail with an error that
27
+ # names the agent they need.
26
28
  #
27
29
  # shell agent installed (>= 1.2.1): run_command, run_script, and
28
30
  # run_task work. run_task uses the bolt_tasks agent by default.
29
31
  # To run local tasks via the shell agent, set task-agent to 'shell'
30
32
  # in project config or specify --choria-task-agent shell.
31
33
  #
32
- # Upload, download, and plans are not yet supported.
34
+ # file_transfer agent installed (>= 1.0.0): upload and download work.
35
+ # The controller needs the agent module's client files, which carry
36
+ # the client library the transport uses.
37
+ #
38
+ # Plans are not yet supported.
33
39
  class Choria < Base
34
40
  def initialize
35
41
  super
@@ -45,10 +51,9 @@ module Bolt
45
51
  @default_collective = nil
46
52
  end
47
53
 
48
- # Advertise both shell and powershell so tasks with either requirement
49
- # can be selected. The per-target selection happens in
50
- # select_implementation below, which picks the right feature set based
51
- # on the target's detected OS.
54
+ # Task selection does not use this list, since select_implementation
55
+ # below overrides the Base method that reads it. The list is kept for
56
+ # apply_prep, which reads a transport's provided features.
52
57
  def provided_features
53
58
  %w[shell powershell]
54
59
  end
@@ -69,6 +74,29 @@ module Bolt
69
74
  impl
70
75
  end
71
76
 
77
+ # Groups the targets by the implementation each selects. A target
78
+ # without a suitable implementation gets its error result instead of
79
+ # failing the whole batch.
80
+ #
81
+ # @param targets [Array<Bolt::Target>] Targets to run the task on
82
+ # @param task [Bolt::Task] Task with the implementations to select from
83
+ # @param result_opts [Hash] Options passed through to emit_results (:action, :name, :position)
84
+ # @param callback [Proc] Called with :node_result events
85
+ # @return [Array] 2-element array:
86
+ # - [Hash{Hash => Array<Bolt::Target>}] Each implementation and the targets that selected it
87
+ # - [Array<Bolt::Result>] Error results for the targets without an implementation
88
+ def group_by_implementation(targets, task, result_opts, &callback)
89
+ selected = {}
90
+ failures = {}
91
+ targets.each do |target|
92
+ selected[target] = select_implementation(target, task)
93
+ rescue Bolt::NoImplementationError => e
94
+ logger.debug { "No suitable implementation of #{task.name} for #{target.safe_name}" }
95
+ failures[target] = error_output(e.message, e.kind)
96
+ end
97
+ [selected.keys.group_by { |target| selected[target] }, emit_results(failures, **result_opts, &callback)]
98
+ end
99
+
72
100
  # Group targets by collective so each batch uses a single RPC client
73
101
  # scope. MCollective RPC calls are published to a collective-specific
74
102
  # NATS subject, so targets in different collectives must be in separate
@@ -107,7 +135,6 @@ module Bolt
107
135
  logger.debug { "Task #{task.name} routing: agent: #{chosen_agent}, #{capable.size} capable / #{targets.size - capable.size} incapable" }
108
136
 
109
137
  unless capable.empty?
110
- capable.each { |target| callback&.call(type: :node_start, target: target) }
111
138
  arguments = unwrap_sensitive_args(arguments)
112
139
 
113
140
  results += case chosen_agent
@@ -148,12 +175,18 @@ module Bolt
148
175
  # @param callback [Proc] Called with :node_start and :node_result events
149
176
  # @return [Array<Bolt::Result>] Results for all targets
150
177
  def batch_task_with(targets, task, target_mapping, options = {}, position = [], &callback)
151
- # Pre-warm the agent cache so individual batch_task calls are cache hits
178
+ # Pre-warm the agent cache so individual batch_task calls are cache hits.
179
+ # A target that did not answer has no cache entry, and batch_task would
180
+ # discover it again and wait out the timeout again, so it gets its
181
+ # error here instead.
152
182
  configure_client(targets.first)
153
- discover_agents(targets)
183
+ discovery_failures = discover_agents(targets)
184
+ discovered, silent = targets.partition { |target| discovered?(target) }
154
185
 
155
- results = []
156
- targets.each do |target|
186
+ silent_errors = silent.to_h { |target| [target, discovery_failures[target] || undiscovered_error(target)] }
187
+ silent.each { |target| callback&.call(type: :node_start, target: target) }
188
+ results = emit_results(silent_errors, action: 'task', name: task.name, position: position, &callback)
189
+ discovered.each do |target|
157
190
  results += batch_task([target], task, target_mapping[target], options, position, &callback)
158
191
  end
159
192
  results
@@ -169,29 +202,59 @@ module Bolt
169
202
  first_target = targets.first
170
203
  configure_client(first_target)
171
204
 
172
- response = rpc_request('rpcutil', targets, 'rpcutil.ping') do |client|
205
+ # A target that denies the caller still answers, so it counts as connected.
206
+ response = rpc_request('rpcutil', targets, 'rpcutil.ping', accept_aborted: true) do |client|
173
207
  client.ping
174
208
  end
175
209
  response[:responded].length == targets.length
176
- rescue StandardError => e
177
- raise if e.is_a?(Bolt::Error)
210
+ end
211
+
212
+ # The single-target methods of the transport API. The remote transport
213
+ # calls run_task for its run-on target.
214
+
215
+ def run_command(target, command, options = {}, position = [])
216
+ batch_command([target], command, options, position).first
217
+ end
178
218
 
179
- logger.warn { "Batch connectivity check failed: #{e.class}: #{e.message}" }
180
- false
219
+ def run_script(target, script, arguments, options = {}, position = [])
220
+ raise_file_error(batch_script([target], script, arguments, options, position).first)
181
221
  end
182
222
 
183
- def upload(_target, _source, _destination, _options = {}, _position = [])
184
- raise Bolt::Error.new(
185
- 'The Choria transport does not yet support upload.',
186
- 'bolt/choria-unsupported-operation'
187
- )
223
+ # Raises when no implementation of the task suits the target, like the
224
+ # other transports do. A batch reports that as the target's result.
225
+ def run_task(target, task, arguments, options = {}, position = [])
226
+ result = batch_task([target], task, arguments, options, position).first
227
+ # A result with an object is the task's own, whatever error the task
228
+ # printed. A failure the transport reports has none.
229
+ return result if result.object
230
+
231
+ raise Bolt::NoImplementationError.new(target, task) if result.error_hash&.dig('kind') == 'bolt/no-implementation'
232
+
233
+ raise_file_error(result)
234
+ end
235
+
236
+ def upload(target, source, destination, options = {})
237
+ batch_upload([target], source, destination, options).first
238
+ end
239
+
240
+ # Downloads into the destination itself. A batch gives each target a
241
+ # directory of its own under the destination.
242
+ def download(target, source, destination, _options = {})
243
+ download_into({ target => destination }, source, []).first
188
244
  end
189
245
 
190
- def download(_target, _source, _destination, _options = {}, _position = [])
191
- raise Bolt::Error.new(
192
- 'The Choria transport does not yet support download.',
193
- 'bolt/choria-unsupported-operation'
194
- )
246
+ def connected?(target)
247
+ batch_connected?([target])
248
+ end
249
+
250
+ # Raises when a preparation step failed on the node, like the other
251
+ # transports do, and returns the result otherwise. A batch reports
252
+ # that failure as the target's result.
253
+ def raise_file_error(result)
254
+ error = result.error_hash
255
+ raise Bolt::Node::FileError, error['msg'] if error && error['kind'] == 'puppetlabs.tasks/task_file_error'
256
+
257
+ result
195
258
  end
196
259
 
197
260
  # Returns the Choria node identity for a target. Uses the transport
@@ -201,6 +264,11 @@ module Bolt
201
264
  target.options['host'] || target.host
202
265
  end
203
266
 
267
+ # The distinct identities behind the targets, in target order.
268
+ def identities_of(targets)
269
+ targets.map { |target| choria_identity(target) }.uniq
270
+ end
271
+
204
272
  # Returns the collective for a target, used by batches() to group
205
273
  # targets. Falls back to the default collective from the loaded config.
206
274
  def collective_for(target)
@@ -214,5 +282,6 @@ require_relative 'choria/agent_discovery'
214
282
  require_relative 'choria/bolt_tasks'
215
283
  require_relative 'choria/client'
216
284
  require_relative 'choria/command_builders'
285
+ require_relative 'choria/file_transfer'
217
286
  require_relative 'choria/helpers'
218
287
  require_relative 'choria/shell'
data/lib/bolt/util.rb CHANGED
@@ -422,6 +422,19 @@ module Bolt
422
422
  path.split(%r{[/\\]}).last
423
423
  end
424
424
 
425
+ # Escapes a string for use inside a PowerShell single-quoted string by
426
+ # doubling every character PowerShell reads as a single quote, which is
427
+ # the apostrophe and the curly single quotes U+2018 to U+201B. The
428
+ # string is converted to UTF-8 first, so a quote in another encoding,
429
+ # such as 0x92 in Windows-1252, is doubled too. A binary string, such
430
+ # as a command-line argument under the C locale, can't be converted, so
431
+ # its bytes are read as UTF-8 and the result stays binary.
432
+ def escape_powershell_single_quotes(str)
433
+ return str.gsub(/'|\xE2\x80[\x98-\x9B]/n) { |quote| quote * 2 } if str.encoding == Encoding::BINARY
434
+
435
+ str.encode('UTF-8').gsub(/['\u{2018}-\u{201B}]/) { |quote| quote * 2 }
436
+ end
437
+
425
438
  # Prompts yes or no, returning true for yes and false for no.
426
439
  #
427
440
  def prompt_yes_no(prompt, outputter)
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '9.0.0-rc1'
4
+ VERSION = '9.0.0'
5
5
  end
@@ -279,6 +279,10 @@ module BoltSpec
279
279
  yield
280
280
  end
281
281
 
282
+ def run_in_new_fiber
283
+ yield
284
+ end
285
+
282
286
  def in_parallel?
283
287
  false
284
288
  end
@@ -106,6 +106,9 @@ begin
106
106
 
107
107
  Puppet.override(overrides) do
108
108
  catalog = Puppet::Resource::Catalog.from_data_hash(args['catalog'])
109
+ # The catalog is named after the Bolt target, and OpenVox 9 logs an error when that
110
+ # name differs from the node name the agent reports under
111
+ Puppet[:node_name_value] = catalog.name
109
112
  catalog.environment = env.name.to_s
110
113
  catalog.environment_instance = env
111
114
  if defined?(Puppet::Pops::Evaluator::DeferredResolver)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openbolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.0.pre.rc1
4
+ version: 9.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenVox Project
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '2.26'
74
+ version: '2.28'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '2.26'
81
+ version: '2.28'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: concurrent-ruby
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -685,6 +685,7 @@ files:
685
685
  - lib/bolt/transport/choria/bolt_tasks.rb
686
686
  - lib/bolt/transport/choria/client.rb
687
687
  - lib/bolt/transport/choria/command_builders.rb
688
+ - lib/bolt/transport/choria/file_transfer.rb
688
689
  - lib/bolt/transport/choria/helpers.rb
689
690
  - lib/bolt/transport/choria/shell.rb
690
691
  - lib/bolt/transport/docker.rb
@@ -749,7 +750,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
749
750
  requirements:
750
751
  - - ">="
751
752
  - !ruby/object:Gem::Version
752
- version: '3.2'
753
+ version: '3.4'
753
754
  required_rubygems_version: !ruby/object:Gem::Requirement
754
755
  requirements:
755
756
  - - ">="