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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +35 -35
  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/run_task.rb +14 -22
  14. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +14 -26
  15. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +2 -2
  16. data/bolt-modules/file/lib/puppet/functions/file/delete.rb +1 -1
  17. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -1
  18. data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -1
  19. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +1 -1
  20. data/bolt-modules/file/lib/puppet/functions/file/write.rb +1 -1
  21. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +1 -1
  22. data/bolt-modules/log/lib/puppet/functions/log/error.rb +1 -1
  23. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +1 -1
  24. data/bolt-modules/log/lib/puppet/functions/log/info.rb +1 -1
  25. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +1 -1
  26. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +1 -1
  27. data/guides/debugging.yaml +1 -1
  28. data/guides/inventory.yaml +2 -2
  29. data/guides/links.yaml +18 -9
  30. data/guides/logging.yaml +1 -1
  31. data/guides/module.yaml +1 -1
  32. data/guides/modulepath.yaml +1 -1
  33. data/guides/project.yaml +2 -2
  34. data/guides/targets.yaml +3 -1
  35. data/guides/transports.yaml +4 -3
  36. data/lib/bolt/analytics.rb +2 -10
  37. data/lib/bolt/bolt_option_parser.rb +66 -66
  38. data/lib/bolt/cli.rb +21 -2
  39. data/lib/bolt/config/options.rb +18 -18
  40. data/lib/bolt/config/transport/choria.rb +3 -0
  41. data/lib/bolt/config/transport/options.rb +34 -45
  42. data/lib/bolt/executor.rb +4 -0
  43. data/lib/bolt/fiber_executor.rb +46 -2
  44. data/lib/bolt/inventory/options.rb +1 -1
  45. data/lib/bolt/inventory/target.rb +1 -1
  46. data/lib/bolt/module_installer/puppetfile.rb +1 -1
  47. data/lib/bolt/module_installer/specs.rb +1 -1
  48. data/lib/bolt/outputter/human.rb +1 -2
  49. data/lib/bolt/plan_creator.rb +4 -4
  50. data/lib/bolt/plan_future.rb +5 -1
  51. data/lib/bolt/project_manager/inventory_migrator.rb +1 -1
  52. data/lib/bolt/project_manager/module_migrator.rb +1 -1
  53. data/lib/bolt/project_manager.rb +1 -1
  54. data/lib/bolt/shell/bash.rb +7 -4
  55. data/lib/bolt/shell/powershell/snippets.rb +20 -1
  56. data/lib/bolt/shell/powershell.rb +2 -2
  57. data/lib/bolt/transport/base.rb +9 -4
  58. data/lib/bolt/transport/choria/agent_discovery.rb +32 -14
  59. data/lib/bolt/transport/choria/bolt_tasks.rb +64 -40
  60. data/lib/bolt/transport/choria/client.rb +66 -49
  61. data/lib/bolt/transport/choria/command_builders.rb +144 -24
  62. data/lib/bolt/transport/choria/file_transfer.rb +134 -0
  63. data/lib/bolt/transport/choria/helpers.rb +25 -20
  64. data/lib/bolt/transport/choria/shell.rb +255 -148
  65. data/lib/bolt/transport/choria.rb +96 -27
  66. data/lib/bolt/transport/ssh/connection.rb +2 -1
  67. data/lib/bolt/util.rb +13 -0
  68. data/lib/bolt/version.rb +1 -1
  69. data/lib/bolt_spec/plans/mock_executor.rb +4 -0
  70. data/libexec/apply_catalog.rb +3 -0
  71. metadata +22 -7
@@ -12,9 +12,11 @@ module Bolt
12
12
  collective
13
13
  command-timeout
14
14
  config-file
15
+ file-transfer-timeout
15
16
  host
16
17
  interpreters
17
18
  mcollective-certname
19
+ message-data-size
18
20
  broker-timeout
19
21
  brokers
20
22
  puppet-environment
@@ -25,6 +27,7 @@ module Bolt
25
27
  task-agent
26
28
  task-timeout
27
29
  tmpdir
30
+ upload-batch-size
28
31
  ].sort.freeze
29
32
 
30
33
  DEFAULTS = {
@@ -19,8 +19,8 @@ module Bolt
19
19
  "batch-mode" => {
20
20
  type: [TrueClass, FalseClass],
21
21
  description: "Whether to disable password querying. When set to `false`, SSH will fall back to " \
22
- "prompting for a password if key authentication fails. This might cause Bolt to hang. " \
23
- "To prevent Bolt from hanging, you can configure `ssh-command` to use an SSH utility " \
22
+ "prompting for a password if key authentication fails. This might cause OpenBolt to hang. " \
23
+ "To prevent OpenBolt from hanging, you can configure `ssh-command` to use an SSH utility " \
24
24
  "such as sshpass that supports providing a password non-interactively. For more " \
25
25
  "information, see [Providing a password non-interactively using " \
26
26
  "`native-ssh`](troubleshooting.md#providing-a-password-non-interactively-using-native-ssh).",
@@ -29,7 +29,7 @@ module Bolt
29
29
  _example: false
30
30
  },
31
31
  "bundled-ruby" => {
32
- description: "Whether to use the Ruby bundled with Bolt packages for local targets.",
32
+ description: "Whether to use the Ruby bundled with OpenBolt packages for local targets.",
33
33
  type: [TrueClass, FalseClass],
34
34
  _plugin: false,
35
35
  _example: true,
@@ -44,7 +44,7 @@ module Bolt
44
44
  "cleanup" => {
45
45
  type: [TrueClass, FalseClass],
46
46
  description: "Whether to clean up temporary files created on targets. When running commands on a target, " \
47
- "Bolt might create temporary files. After completing the command, these files are " \
47
+ "OpenBolt might create temporary files. After completing the command, these files are " \
48
48
  "automatically deleted. This value can be set to 'false' if you wish to leave these " \
49
49
  "temporary files on the target.",
50
50
  _plugin: true,
@@ -101,10 +101,33 @@ module Bolt
101
101
  _default: 30,
102
102
  _example: 60
103
103
  },
104
+ "file-transfer-timeout" => {
105
+ type: Integer,
106
+ description: "How long to wait in seconds for targets to respond to a file transfer request. Default is " \
107
+ "300, and the wait is never shorter than the agent's DDL timeout of 120 seconds.",
108
+ minimum: 1,
109
+ _plugin: true,
110
+ _example: 600
111
+ },
112
+ "message-data-size" => {
113
+ type: Integer,
114
+ description: "The file content in one Choria RPC message, in kibibytes, for uploads, downloads and " \
115
+ "directory listings. Default is 512, half the broker's 1 MiB message limit.",
116
+ minimum: 1,
117
+ _plugin: true,
118
+ _example: 256
119
+ },
120
+ "upload-batch-size" => {
121
+ type: Integer,
122
+ description: "How many targets one chunk request is published to at a time. Default is 256.",
123
+ minimum: 1,
124
+ _plugin: true,
125
+ _example: 100
126
+ },
104
127
  "connect-timeout" => {
105
128
  type: Integer,
106
129
  description: "How long to wait in seconds when establishing connections. Set this value higher if you " \
107
- "frequently encounter connection timeout errors when running Bolt.",
130
+ "frequently encounter connection timeout errors when running OpenBolt.",
108
131
  minimum: 1,
109
132
  _plugin: true,
110
133
  _default: 10,
@@ -112,7 +135,7 @@ module Bolt
112
135
  },
113
136
  "copy-command" => {
114
137
  type: [Array, String],
115
- description: "The command to use when copying files using native SSH. Bolt runs `<copy-command> <src> " \
138
+ description: "The command to use when copying files using native SSH. OpenBolt runs `<copy-command> <src> " \
116
139
  "<dest>`. This option is used when you need support for features or algorithms that are not " \
117
140
  "supported by the net-ssh Ruby library. **This option is experimental.** You can read more " \
118
141
  "about this option in [Native SSH transport](experimental_features.md#native-ssh-transport).",
@@ -203,7 +226,7 @@ module Bolt
203
226
  "override the shebang defined in a task executable. The extension can optionally be " \
204
227
  "specified with the `.` character (`.py` and `py` both map to a task executable " \
205
228
  "`task.py`) and the extension is case sensitive. When a target's name is `localhost`, " \
206
- "Ruby tasks run with the Bolt Ruby interpreter by default.",
229
+ "Ruby tasks run with the OpenBolt Ruby interpreter by default.",
207
230
  additionalProperties: {
208
231
  type: [String, Array],
209
232
  _plugin: false
@@ -215,20 +238,6 @@ module Bolt
215
238
  _example: { "rb" => ["/usr/bin/ruby", "-r", "puppet"],
216
239
  ".py" => "/usr/bin/python3" }
217
240
  },
218
- "job-poll-interval" => {
219
- type: Integer,
220
- description: "The interval, in seconds, to poll orchestrator for job status.",
221
- minimum: 1,
222
- _plugin: true,
223
- _example: 2
224
- },
225
- "job-poll-timeout" => {
226
- type: Integer,
227
- description: "The time, in seconds, to wait for orchestrator job status.",
228
- minimum: 1,
229
- _plugin: true,
230
- _example: 2000
231
- },
232
241
  "kex-algorithms" => {
233
242
  type: Array,
234
243
  description: "A list of key exchange algorithms to use when establishing a connection " \
@@ -253,7 +262,7 @@ module Bolt
253
262
  },
254
263
  "login-shell" => {
255
264
  type: String,
256
- description: "Which login shell Bolt should expect on the target. Supported shells are " \
265
+ description: "Which login shell OpenBolt should expect on the target. Supported shells are " \
257
266
  "#{LOGIN_SHELLS.join(', ')}. **This option is experimental.**",
258
267
  enum: LOGIN_SHELLS,
259
268
  _plugin: true,
@@ -346,13 +355,6 @@ module Bolt
346
355
  _default: "production",
347
356
  _example: "staging"
348
357
  },
349
- "read-timeout" => {
350
- type: Integer,
351
- description: "How long to wait in seconds when making requests to the Orchestrator.",
352
- minimum: 1,
353
- _plugin: true,
354
- _example: 15
355
- },
356
358
  "realm" => {
357
359
  type: String,
358
360
  description: "The Kerberos realm (Active Directory domain) to authenticate against.",
@@ -375,9 +377,9 @@ module Bolt
375
377
  },
376
378
  "run-as-command" => {
377
379
  type: Array,
378
- description: "The command to elevate permissions. Bolt appends the user and command strings to the " \
379
- "configured `run-as-command` before running it on the target. This command must not require " \
380
- "aninteractive password prompt, and the `sudo-password` option is ignored when " \
380
+ description: "The command to elevate permissions. OpenBolt appends the user and command strings to the " \
381
+ "configured `run-as-command` before running it on the target. This command must not require " \
382
+ "an interactive password prompt, and the `sudo-password` option is ignored when " \
381
383
  "`run-as-command` is specified. The `run-as-command` must be specified as an array.",
382
384
  items: {
383
385
  type: String
@@ -486,13 +488,6 @@ module Bolt
486
488
  _plugin: true,
487
489
  _example: "p@$$w0rd!"
488
490
  },
489
- "task-environment" => {
490
- type: String,
491
- description: "The environment the orchestrator loads task code from.",
492
- _plugin: true,
493
- _default: "production",
494
- _example: "development"
495
- },
496
491
  "task-timeout" => {
497
492
  type: Integer,
498
493
  description: "How long to wait in seconds for tasks to complete when using the Choria transport.",
@@ -507,12 +502,6 @@ module Bolt
507
502
  _plugin: true,
508
503
  _example: "/tmp/bolt"
509
504
  },
510
- "token-file" => {
511
- type: String,
512
- description: "The path to the token file.",
513
- _plugin: true,
514
- _example: "~/.puppetlabs/puppet/token.pem"
515
- },
516
505
  "tty" => {
517
506
  type: [TrueClass, FalseClass],
518
507
  description: "Whether to enable tty on exec commands.",
data/lib/bolt/executor.rb CHANGED
@@ -401,6 +401,10 @@ module Bolt
401
401
  @fiber_executor.get_current_plan_id(fiber: fiber)
402
402
  end
403
403
 
404
+ def run_in_new_fiber(&block)
405
+ @fiber_executor.run_in_new_fiber(&block)
406
+ end
407
+
404
408
  def plan_complete?
405
409
  @fiber_executor.plan_complete?
406
410
  end
@@ -5,6 +5,11 @@ require_relative '../bolt/plan_future'
5
5
 
6
6
  module Bolt
7
7
  class FiberExecutor
8
+ # Limits how many plans one future can nest, so a plan that runs itself
9
+ # with no condition that stops it fails instead of starting Fibers until
10
+ # memory runs out.
11
+ MAX_NESTED_PLANS = 1000
12
+
8
13
  attr_reader :active_futures, :finished_futures
9
14
 
10
15
  def initialize
@@ -99,6 +104,7 @@ module Bolt
99
104
  # `wait` call, Bolt should pause for long enough that something can
100
105
  # execute before checking again. This mitigates CPU
101
106
  # thrashing.
107
+ return if active_futures.empty?
102
108
  return unless active_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
103
109
 
104
110
  @logger.trace("Nothing can be resumed. Rechecking in 0.5 seconds.")
@@ -121,7 +127,45 @@ module Bolt
121
127
  # Get the PlanFuture object that is currently executing
122
128
  #
123
129
  def get_current_future(fiber:)
124
- all_futures.select { |f| f.fiber == fiber }.first
130
+ all_futures.find { |f| f.fiber == fiber || f.nested_fibers.include?(fiber) }
131
+ end
132
+
133
+ # Run the block on a new Fiber, so it starts with an empty stack instead
134
+ # of adding to the current one. Every plan a plan runs adds dozens of
135
+ # frames, and a Fiber's stack is small and fixed. Values the block yields
136
+ # while it waits are yielded on to the scheduler, and an error raised into
137
+ # the current Fiber, like a wait() timeout, is raised inside the block.
138
+ #
139
+ def run_in_new_fiber(&block)
140
+ future = get_current_future(fiber: Fiber.current)
141
+ return yield unless future
142
+
143
+ if future.nested_fibers.length >= MAX_NESTED_PLANS
144
+ msg = "Plans cannot be nested more than #{MAX_NESTED_PLANS} levels deep. Check for a " \
145
+ "plan that runs itself without a condition that stops it."
146
+ # A plain error, not a Bolt::Error, so plans can't catch it with
147
+ # _catch_errors or catch_errors() and keep a runaway plan going.
148
+ raise msg
149
+ end
150
+
151
+ fiber = Fiber.new(&block)
152
+ future.nested_fibers.push(fiber)
153
+ @logger.trace("Running a nested plan on a new Fiber in future '#{future.name}'")
154
+ begin
155
+ value = fiber.resume
156
+ while fiber.alive?
157
+ begin
158
+ resumed_with = Fiber.yield(value)
159
+ rescue StandardError => e
160
+ value = fiber.raise(e)
161
+ next
162
+ end
163
+ value = fiber.resume(resumed_with)
164
+ end
165
+ value
166
+ ensure
167
+ future.nested_fibers.delete(fiber)
168
+ end
125
169
  end
126
170
 
127
171
  # Get the plan invocation ID for the PlanFuture that is currently executing
@@ -146,7 +190,7 @@ module Bolt
146
190
  # finished. This ensures that we include futures created inside of
147
191
  # futures being waited on.
148
192
  until (futures = get_futures_for_plan(plan_id: plan_id)).map(&:alive?).none?
149
- if futures.map(&:fiber).include?(Fiber.current)
193
+ if futures.include?(get_current_future(fiber: Fiber.current))
150
194
  msg = "The wait() function cannot be called with no arguments inside a " \
151
195
  "background block in the same plan."
152
196
  raise Bolt::Error.new(msg, 'bolt/infinite-wait')
@@ -84,7 +84,7 @@ module Bolt
84
84
  "plugin_hooks" => {
85
85
  description: "Configuration for the Puppet library plugin used to install the " \
86
86
  "Puppet agent on the target. For more information, see " \
87
- "https://pup.pt/bolt-plugin-hooks",
87
+ "[plugin hooks](writing_plugins.md#plugin-hooks).",
88
88
  type: Hash,
89
89
  properties: {
90
90
  "puppet_library" => {
@@ -179,7 +179,7 @@ module Bolt
179
179
  'dotted_fact_name',
180
180
  "Target '#{safe_name}' includes dotted fact names: '#{dotted.join("', '")}'. Dotted fact " \
181
181
  "names are deprecated and Bolt does not automatically convert facts with dotted names to " \
182
- "structured facts. For more information, see https://pup.pt/bolt-dotted-facts"
182
+ "structured facts. For more information, see https://docs.openvoxproject.org/openbolt/latest/developer_updates.html#deprecating-dotted-fact-names"
183
183
  )
184
184
  end
185
185
  end
@@ -72,7 +72,7 @@ module Bolt
72
72
  File.open(path, 'w') do |file|
73
73
  if moduledir
74
74
  file.puts "# This Puppetfile is managed by Bolt. Do not edit."
75
- file.puts "# For more information, see https://pup.pt/bolt-modules"
75
+ file.puts "# For more information, see https://docs.openvoxproject.org/openbolt/latest/modules.html"
76
76
  file.puts
77
77
  file.puts "# The following directive installs modules to the managed moduledir."
78
78
  file.puts "moduledir '#{moduledir.basename}'"
@@ -57,7 +57,7 @@ module Bolt
57
57
  Invalid module specification:
58
58
  #{hash.to_yaml.lines.drop(1).join.chomp}
59
59
 
60
- To read more about specifying modules, see https://pup.pt/bolt-module-specs
60
+ To read more about specifying modules, see https://docs.openvoxproject.org/openbolt/latest/bolt_installing_modules.html#manually-specify-modules-in-a-bolt-project
61
61
  MESSAGE
62
62
  end
63
63
 
@@ -51,7 +51,6 @@ module Bolt
51
51
 
52
52
  @spinning = false
53
53
  @spin_thread.terminate
54
- @stream.print("\b")
55
54
  end
56
55
 
57
56
  def remove_trail(string)
@@ -645,7 +644,7 @@ module Bolt
645
644
  info << indent(2, "#{modulepath.join(File::PATH_SEPARATOR)}\n\n")
646
645
 
647
646
  info << colorize(:cyan, "Additional information\n")
648
- info << indent(2, "For more information about using plugins see https://pup.pt/bolt-plugins")
647
+ info << indent(2, "For more information about using plugins see https://docs.openvoxproject.org/openbolt/latest/using_plugins.html")
649
648
 
650
649
  @stream.puts info.chomp
651
650
  end
@@ -113,7 +113,7 @@ module Bolt
113
113
  private_class_method def self.yaml_plan(plan_name)
114
114
  <<~YAML
115
115
  # This is the structure of a simple plan. To learn more about writing
116
- # YAML plans, see the documentation: http://pup.pt/bolt-yaml-plans
116
+ # YAML plans, see the documentation: https://docs.openvoxproject.org/openbolt/latest/writing_yaml_plans.html
117
117
 
118
118
  # The description sets the description of the plan that will appear
119
119
  # in 'bolt plan show' output.
@@ -146,7 +146,7 @@ module Bolt
146
146
  private_class_method def self.yaml_script_plan(script)
147
147
  <<~YAML
148
148
  # This is the structure of a simple plan. To learn more about writing
149
- # YAML plans, see the documentation: http://pup.pt/bolt-yaml-plans
149
+ # YAML plans, see the documentation: https://docs.openvoxproject.org/openbolt/latest/writing_yaml_plans.html
150
150
 
151
151
  # The description sets the description of the plan that will appear
152
152
  # in 'bolt plan show' output.
@@ -177,7 +177,7 @@ module Bolt
177
177
  private_class_method def self.puppet_plan(plan_name)
178
178
  <<~PUPPET
179
179
  # This is the structure of a simple plan. To learn more about writing
180
- # Puppet plans, see the documentation: http://pup.pt/bolt-puppet-plans
180
+ # Puppet plans, see the documentation: https://docs.openvoxproject.org/openbolt/latest/writing_plans.html
181
181
 
182
182
  # The summary sets the description of the plan that will appear
183
183
  # in 'bolt plan show' output. Bolt uses puppet-strings to parse the
@@ -202,7 +202,7 @@ module Bolt
202
202
  private_class_method def self.puppet_script_plan(plan_name, script)
203
203
  <<~PUPPET
204
204
  # This is the structure of a simple plan. To learn more about writing
205
- # Puppet plans, see the documentation: http://pup.pt/bolt-puppet-plans
205
+ # Puppet plans, see the documentation: https://docs.openvoxproject.org/openbolt/latest/writing_plans.html
206
206
 
207
207
  # The summary sets the description of the plan that will appear
208
208
  # in 'bolt plan show' output. Bolt uses puppet-strings to parse the
@@ -4,7 +4,7 @@ require 'fiber'
4
4
 
5
5
  module Bolt
6
6
  class PlanFuture
7
- attr_reader :fiber, :id, :scope
7
+ attr_reader :fiber, :id, :scope, :nested_fibers
8
8
  attr_accessor :value, :plan_stack
9
9
 
10
10
  def initialize(fiber, id, plan_id:, name: nil, scope: nil)
@@ -24,6 +24,10 @@ module Bolt
24
24
  # since now we're in the calling plan. These IDs are unique to each plan
25
25
  # invocation, not just plan names.
26
26
  @plan_stack = [plan_id]
27
+
28
+ # Fibers this Future started with FiberExecutor#run_in_new_fiber, which
29
+ # still belong to this Future
30
+ @nested_fibers = []
27
31
  end
28
32
 
29
33
  def original_plan
@@ -35,7 +35,7 @@ module Bolt
35
35
  rescue StandardError => e
36
36
  raise Bolt::FileError.new(
37
37
  "Unable to write to #{inventory_file}: #{e.message}. See " \
38
- "http://pup.pt/bolt-inventory to manually update.",
38
+ "https://docs.openvoxproject.org/openbolt/latest/inventory_files.html to manually update.",
39
39
  inventory_file
40
40
  )
41
41
  end
@@ -23,7 +23,7 @@ module Bolt
23
23
  @outputter.print_action_step(
24
24
  "Project has a non-default configured modulepath, unable to automatically " \
25
25
  "migrate project modules. To migrate project modules manually, see " \
26
- "http://pup.pt/bolt-modules"
26
+ "https://docs.openvoxproject.org/openbolt/latest/modules.html"
27
27
  )
28
28
  true
29
29
  # Migrate modules from Puppetfile
@@ -8,7 +8,7 @@ module Bolt
8
8
  class ProjectManager
9
9
  INVENTORY_TEMPLATE = <<~INVENTORY
10
10
  # This is an example inventory.yaml
11
- # To read more about inventory files, see https://pup.pt/bolt-inventory
11
+ # To read more about inventory files, see https://docs.openvoxproject.org/openbolt/latest/inventory_files.html
12
12
  #
13
13
  # groups:
14
14
  # - name: linux
@@ -230,15 +230,17 @@ module Bolt
230
230
  ''
231
231
  end
232
232
 
233
+ # GNU sudo and sudo-rs word these differently
233
234
  def handle_sudo_errors(err)
234
235
  case err
235
- when /^#{conn.user} is not in the sudoers file\./
236
+ when /^#{conn.user} is not in the sudoers file\./,
237
+ /Sorry, user #{conn.user} (may not run|is not allowed to execute)/
236
238
  @logger.trace { err }
237
239
  raise Bolt::Node::EscalateError.new(
238
240
  "User #{conn.user} does not have sudo permission on #{target}",
239
241
  'SUDO_DENIED'
240
242
  )
241
- when /^Sorry, try again\./
243
+ when /^Sorry, try again\./, /Authentication failed, try again/
242
244
  @logger.trace { err }
243
245
  raise Bolt::Node::EscalateError.new(
244
246
  "Sudo password for user #{conn.user} not recognized on #{target}",
@@ -299,7 +301,7 @@ module Bolt
299
301
 
300
302
  result = execute(command)
301
303
  if result.exit_code != 0
302
- raise Bolt::Node::FileError.new("Could not make tmpdir: #{result.stderr.string}", 'TMPDIR_ERROR')
304
+ raise Bolt::Node::FileError.new("Could not make tmpdir #{tmppath}: #{result.stderr.string}", 'TMPDIR_ERROR')
303
305
  end
304
306
 
305
307
  path = tmppath || result.stdout.string.chomp
@@ -484,7 +486,8 @@ module Bolt
484
486
  @logger.trace { "Command `#{command_str}` returned successfully" }
485
487
  when 126
486
488
  msg = "\n\nThis might be caused by the default tmpdir being mounted " \
487
- "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
489
+ "using 'noexec'. See https://docs.openvoxproject.org/openbolt/latest/troubleshooting.html" \
490
+ "#my-task-fails-with-a-permission-denied-error-noexec-issue for details and workarounds."
488
491
  result_output.stderr << msg
489
492
  result_output.merged_output << msg
490
493
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
@@ -1,10 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
4
+
3
5
  module Bolt
4
6
  class Shell
5
7
  class Powershell < Shell
6
8
  module Snippets
7
9
  class << self
10
+ # An expression that evaluates to the value as a string. The value
11
+ # is sent as base64, so no character in it can end a quoted string
12
+ # or here-string, whatever encoding PowerShell reads the script in.
13
+ # A binary string, such as a command-line argument under the C
14
+ # locale, goes as its bytes, which PowerShell reads as UTF-8.
15
+ def base64_string(value)
16
+ value = value.to_s
17
+ value = value.encode('UTF-8') unless value.encoding == Encoding::BINARY
18
+ "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{Base64.strict_encode64(value)}'))"
19
+ end
20
+
8
21
  def execute_process(command)
9
22
  <<~PS
10
23
  if ([Console]::InputEncoding -eq [System.Text.Encoding]::UTF8) {
@@ -46,7 +59,7 @@ module Bolt
46
59
 
47
60
  def run_script(arguments, script_path)
48
61
  build_arg_list = arguments.map do |a|
49
- "$invokeArgs.ArgumentList += @'\n#{a}\n'@"
62
+ "$invokeArgs.ArgumentList += #{base64_string(a)}"
50
63
  end.join("\n")
51
64
  <<~PS
52
65
  $invokeArgs = @{
@@ -138,6 +151,12 @@ module Bolt
138
151
  "${boltBaseDir}\\hiera\\lib;" +
139
152
  $ENV:RUBYLIB
140
153
 
154
+ #{json_functions.chomp}
155
+ PS
156
+ end
157
+
158
+ def json_functions
159
+ <<~PS
141
160
  function ConvertFrom-PSCustomObject
142
161
  {
143
162
  PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
@@ -88,12 +88,12 @@ module Bolt
88
88
 
89
89
  def env_declarations(env_vars)
90
90
  env_vars.map do |var, val|
91
- "[Environment]::SetEnvironmentVariable('#{var}', @'\n#{val}\n'@)"
91
+ "[Environment]::SetEnvironmentVariable(#{quote_string(var.to_s)}, #{Snippets.base64_string(val)})"
92
92
  end
93
93
  end
94
94
 
95
95
  def quote_string(string)
96
- "'" + string.gsub("'", "''") + "'"
96
+ "'" + Bolt::Util.escape_powershell_single_quotes(string) + "'"
97
97
  end
98
98
 
99
99
  def write_executable(dir, file, filename = nil)
@@ -165,18 +165,23 @@ module Bolt
165
165
  #
166
166
  # Transports may override this method to implement their own batch processing.
167
167
  def batch_download(targets, source, destination, options = {}, position = [], &callback)
168
- require 'erb'
169
-
170
168
  assert_batch_size_one("batch_download()", targets)
171
169
  target = targets.first
172
170
  with_events(target, callback, 'download', position) do
173
- escaped_name = ERB::Util.url_encode(target.safe_name)
174
- target_destination = File.expand_path(escaped_name, destination)
171
+ target_destination = download_dir(destination, target)
175
172
  @logger.debug { "Downloading: '#{source}' on #{target.safe_name} to #{target_destination}" }
176
173
  download(target, source, target_destination, options)
177
174
  end
178
175
  end
179
176
 
177
+ # The directory under the destination that receives one target's
178
+ # download, named after the target so that a batch keeps them apart.
179
+ def download_dir(destination, target)
180
+ require 'erb'
181
+
182
+ File.expand_path(ERB::Util.url_encode(target.safe_name), destination)
183
+ end
184
+
180
185
  def batch_connected?(targets)
181
186
  assert_batch_size_one("connected?()", targets)
182
187
  connected?(targets.first)
@@ -3,10 +3,9 @@
3
3
  module Bolt
4
4
  module Transport
5
5
  class Choria
6
- SHELL_MIN_VERSION = '1.2.1'
7
-
8
6
  AGENT_MIN_VERSIONS = {
9
- 'shell' => SHELL_MIN_VERSION
7
+ 'shell' => '1.2.1',
8
+ 'file_transfer' => '1.0.0'
10
9
  }.freeze
11
10
 
12
11
  # Discover agents and detect OS on targets via two batched RPC calls
@@ -14,18 +13,20 @@ module Bolt
14
13
  # Populates @agent_cache with { agents: [...], os: 'redhat' | 'windows' | ... }.
15
14
  #
16
15
  # @param targets [Array<Bolt::Target>] Targets to discover agents on
16
+ # @return [Hash{Bolt::Target => Hash}] What discover_agent_list returns for the uncached
17
+ # targets, or an empty hash when every target was already discovered.
17
18
  def discover_agents(targets)
18
- uncached = targets.reject { |target| @agent_cache.key?(choria_identity(target)) }
19
- return if uncached.empty?
19
+ uncached = targets.reject { |target| discovered?(target) }
20
+ return {} if uncached.empty?
20
21
 
21
22
  logger.debug { "Discovering agents on #{target_count(uncached)}" }
22
- discover_agent_list(uncached)
23
+ failures = discover_agent_list(uncached)
23
24
  discover_os_family(uncached)
25
+ failures
26
+ end
24
27
 
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
28
+ def discovered?(target)
29
+ @agent_cache.key?(choria_identity(target))
29
30
  end
30
31
 
31
32
  def has_agent?(target, agent_name)
@@ -40,26 +41,37 @@ module Bolt
40
41
  # and populate @agent_cache with agent lists.
41
42
  #
42
43
  # @param targets [Array<Bolt::Target>] Targets to query for agent inventory
44
+ # @return [Hash{Bolt::Target => Hash}] The error output of each target that did not answer,
45
+ # answered with an RPC error or answered without an agent list, or of every target when
46
+ # the call failed as a whole, such as when the client could not connect. An empty hash
47
+ # when discovery itself raises, which leaves those targets undiscovered.
43
48
  def discover_agent_list(targets)
44
49
  response = rpc_request('rpcutil', targets, 'rpcutil.agent_inventory') do |client|
45
50
  client.agent_inventory
46
51
  end
47
- response[:errors].each { |target, output| logger.debug { "agent_inventory failed for #{target.safe_name}: #{output[:error]}" } }
52
+ response[:errors].each { |target, output| logger.warn { "agent_inventory failed for #{target.safe_name}: #{output[:error]}" } }
48
53
 
54
+ malformed = {}
49
55
  response[:responded].each do |target, data|
50
56
  sender = choria_identity(target)
51
- agents = filter_agents(sender, data[:agents])
57
+ # A node can send data that isn't a hash, which would raise and stop
58
+ # discovery for every target after it
59
+ agents = filter_agents(sender, data[:agents]) if data.is_a?(Hash)
52
60
  unless agents
53
- logger.warn { "Unexpected agent_inventory response from #{sender}. This target will be treated as unreachable." }
61
+ logger.warn { "Unexpected agent_inventory response from #{sender}" }
62
+ malformed[target] = error_output("rpcutil.agent_inventory on #{target.safe_name} returned no agent list",
63
+ 'bolt/choria-missing-data')
54
64
  next
55
65
  end
56
66
  @agent_cache[sender] = { agents: agents }
57
67
  logger.debug { "Discovered agents on #{sender}: #{agents.join(', ')}" }
58
68
  end
69
+ response[:errors].merge(malformed)
59
70
  rescue StandardError => e
60
71
  raise if e.is_a?(Bolt::Error)
61
72
 
62
73
  logger.warn { "Agent discovery failed: #{e.class}: #{e.message}" }
74
+ {}
63
75
  end
64
76
 
65
77
  # Detect the OS family on targets via rpcutil.get_fact and update
@@ -68,7 +80,7 @@ module Bolt
68
80
  # @param targets [Array<Bolt::Target>] Targets to detect OS on
69
81
  def discover_os_family(targets)
70
82
  # Only fetch OS for targets that responded to agent_inventory
71
- responded = targets.select { |target| @agent_cache.key?(choria_identity(target)) }
83
+ responded = targets.select { |target| discovered?(target) }
72
84
  return if responded.empty?
73
85
 
74
86
  response = rpc_request('rpcutil', responded, 'rpcutil.get_fact') do |client|
@@ -82,6 +94,10 @@ module Bolt
82
94
 
83
95
  response[:responded].each do |target, data|
84
96
  sender = choria_identity(target)
97
+ unless data.is_a?(Hash)
98
+ logger.warn { "Unexpected get_fact response from #{sender}. Defaulting to POSIX command syntax." }
99
+ next
100
+ end
85
101
  os_family = data[:value].to_s.downcase
86
102
  if os_family.empty?
87
103
  logger.warn { "os.family fact is empty on #{sender}. Defaulting to POSIX command syntax." }
@@ -107,6 +123,8 @@ module Bolt
107
123
  return nil unless agent_list.is_a?(Array)
108
124
 
109
125
  agent_list.filter_map do |entry|
126
+ next unless entry.is_a?(Hash)
127
+
110
128
  name = entry['agent']
111
129
  next unless name
112
130