bolt 2.19.0 → 2.20.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  3. data/lib/bolt/bolt_option_parser.rb +25 -2
  4. data/lib/bolt/catalog.rb +3 -2
  5. data/lib/bolt/cli.rb +139 -92
  6. data/lib/bolt/config.rb +1 -1
  7. data/lib/bolt/config/options.rb +14 -0
  8. data/lib/bolt/executor.rb +15 -0
  9. data/lib/bolt/inventory/group.rb +3 -2
  10. data/lib/bolt/inventory/inventory.rb +4 -3
  11. data/lib/bolt/outputter/rainbow.rb +3 -2
  12. data/lib/bolt/pal.rb +8 -2
  13. data/lib/bolt/pal/yaml_plan/evaluator.rb +18 -1
  14. data/lib/bolt/pal/yaml_plan/step.rb +11 -2
  15. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  16. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  17. data/lib/bolt/plugin/puppetdb.rb +3 -2
  18. data/lib/bolt/project.rb +2 -1
  19. data/lib/bolt/puppetdb/client.rb +2 -0
  20. data/lib/bolt/puppetdb/config.rb +16 -0
  21. data/lib/bolt/result.rb +7 -0
  22. data/lib/bolt/shell/bash.rb +24 -4
  23. data/lib/bolt/shell/powershell.rb +10 -4
  24. data/lib/bolt/transport/base.rb +24 -0
  25. data/lib/bolt/transport/docker.rb +8 -0
  26. data/lib/bolt/transport/docker/connection.rb +20 -2
  27. data/lib/bolt/transport/local/connection.rb +14 -1
  28. data/lib/bolt/transport/orch.rb +12 -0
  29. data/lib/bolt/transport/simple.rb +6 -0
  30. data/lib/bolt/transport/ssh/connection.rb +9 -1
  31. data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
  32. data/lib/bolt/transport/winrm/connection.rb +109 -8
  33. data/lib/bolt/util.rb +26 -11
  34. data/lib/bolt/version.rb +1 -1
  35. data/lib/bolt_server/transport_app.rb +3 -2
  36. data/lib/bolt_spec/bolt_context.rb +7 -2
  37. data/lib/bolt_spec/plans.rb +15 -2
  38. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  39. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  40. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  41. data/lib/bolt_spec/run.rb +22 -0
  42. data/libexec/bolt_catalog +3 -2
  43. metadata +5 -2
@@ -499,7 +499,7 @@ module Bolt
499
499
  end
500
500
 
501
501
  def matching_paths(paths)
502
- [*paths].map { |p| Dir.glob([p, casefold(p)]) }.flatten.uniq.reject { |p| [*paths].include?(p) }
502
+ Array(paths).map { |p| Dir.glob([p, casefold(p)]) }.flatten.uniq.reject { |p| Array(paths).include?(p) }
503
503
  end
504
504
 
505
505
  private def casefold(path)
@@ -275,11 +275,25 @@ module Bolt
275
275
  type: String,
276
276
  _example: "/etc/puppetlabs/puppet/ssl/certs/my-host.example.com.pem"
277
277
  },
278
+ "connect_timeout" => {
279
+ description: "How long to wait in seconds when establishing connections with PuppetDB.",
280
+ type: Integer,
281
+ minimum: 1,
282
+ _default: 60,
283
+ _example: 120
284
+ },
278
285
  "key" => {
279
286
  description: "The private key for the certificate.",
280
287
  type: String,
281
288
  _example: "/etc/puppetlabs/puppet/ssl/private_keys/my-host.example.com.pem"
282
289
  },
290
+ "read_timeout" => {
291
+ description: "How long to wait in seconds for a response from PuppetDB.",
292
+ type: Integer,
293
+ minimum: 1,
294
+ _default: 60,
295
+ _example: 120
296
+ },
283
297
  "server_urls" => {
284
298
  description: "An array containing the PuppetDB host to connect to. Include the protocol `https` "\
285
299
  "and the port, which is usually `8081`. For example, "\
@@ -320,6 +320,21 @@ module Bolt
320
320
  end
321
321
  end
322
322
 
323
+ def download_file(targets, source, destination, options = {})
324
+ description = options.fetch(:description, "file download from #{source} to #{destination}")
325
+ FileUtils.mkdir_p(destination)
326
+
327
+ log_action(description, targets) do
328
+ options[:run_as] = run_as if run_as && !options.key?(:run_as)
329
+
330
+ batch_execute(targets) do |transport, batch|
331
+ with_node_logging("Downloading file #{source} to #{destination}", batch) do
332
+ transport.batch_download(batch, source, destination, options, &method(:publish_event))
333
+ end
334
+ end
335
+ end
336
+ end
337
+
323
338
  def run_plan(scope, plan, params)
324
339
  plan.call_by_name_with_scope(scope, params, true)
325
340
  end
@@ -50,10 +50,11 @@ module Bolt
50
50
  # or it could be a name/alias of a target defined in another group.
51
51
  # We can't tell the difference until all groups have been resolved,
52
52
  # so we store the string on its own here and process it later.
53
- if target.is_a?(String)
53
+ case target
54
+ when String
54
55
  @string_targets << target
55
56
  # Handle plugins at this level so that lookups cannot trigger recursive lookups
56
- elsif target.is_a?(Hash)
57
+ when Hash
57
58
  add_target_definition(target)
58
59
  else
59
60
  raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
@@ -109,11 +109,12 @@ module Bolt
109
109
  private :resolve_name
110
110
 
111
111
  def expand_targets(targets)
112
- if targets.is_a? Bolt::Target
112
+ case targets
113
+ when Bolt::Target
113
114
  targets
114
- elsif targets.is_a? Array
115
+ when Array
115
116
  targets.map { |tish| expand_targets(tish) }
116
- elsif targets.is_a? String
117
+ when String
117
118
  # Expand a comma-separated list
118
119
  targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
119
120
  ts = resolve_name(name)
@@ -39,9 +39,10 @@ module Bolt
39
39
  a = string.chars.map do |c|
40
40
  case @state
41
41
  when :normal
42
- if c == "\e"
42
+ case c
43
+ when "\e"
43
44
  @state = :ansi
44
- elsif c == "\n"
45
+ when "\n"
45
46
  @line_color += 1
46
47
  @color = @line_color
47
48
  c
@@ -150,7 +150,12 @@ module Bolt
150
150
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
151
151
  # Only load the project if it a) exists, b) has a name it can be loaded with
152
152
  bolt_project = @project if @project&.name
153
+ # Puppet currently won't receive the project unless it is a named project. Since
154
+ # the download_file plan function needs access to the project path, add it to the
155
+ # context.
156
+ bolt_project_data = @project
153
157
  Puppet.override(bolt_project: bolt_project,
158
+ bolt_project_data: bolt_project_data,
154
159
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
155
160
  pal.with_script_compiler do |compiler|
156
161
  alias_types(compiler)
@@ -279,9 +284,10 @@ module Bolt
279
284
 
280
285
  def parse_params(type, object_name, params)
281
286
  in_bolt_compiler do |compiler|
282
- if type == 'task'
287
+ case type
288
+ when 'task'
283
289
  param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
284
- elsif type == 'plan'
290
+ when 'plan'
285
291
  plan = compiler.plan_signature(object_name)
286
292
  param_spec = plan.params_type.elements&.each_with_object({}) { |t, h| h[t.name] = t.value_type } if plan
287
293
  end
@@ -73,7 +73,7 @@ module Bolt
73
73
  end
74
74
 
75
75
  def upload_step(scope, step)
76
- source = step['source']
76
+ source = step['upload'] || step['source']
77
77
  destination = step['destination']
78
78
  targets = step['targets'] || step['target']
79
79
  description = step['description']
@@ -83,6 +83,17 @@ module Bolt
83
83
  scope.call_function('upload_file', args)
84
84
  end
85
85
 
86
+ def download_step(scope, step)
87
+ source = step['download']
88
+ destination = step['destination']
89
+ targets = step['targets'] || step['target']
90
+ description = step['description']
91
+
92
+ args = [source, destination, targets]
93
+ args << description if description
94
+ scope.call_function('download_file', args)
95
+ end
96
+
86
97
  def eval_step(_scope, step)
87
98
  step['eval']
88
99
  end
@@ -145,6 +156,12 @@ module Bolt
145
156
  Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
146
157
  end
147
158
 
159
+ if plan.steps.any? { |step| step.body.key?('source') }
160
+ msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
161
+ "in a future version of Bolt. Use the 'upload' parameter instead."
162
+ Bolt::Logger.deprecation_warning("Using 'source' parameter for YAML upload steps, not 'upload'", msg)
163
+ end
164
+
148
165
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|
149
166
  plan.steps.each do |step|
150
167
  step_result = dispatch_step(scope, step)
@@ -12,7 +12,7 @@ module Bolt
12
12
  Set['name', 'description', 'target', 'targets']
13
13
  end
14
14
 
15
- STEP_KEYS = %w[command script task plan source destination eval resources].freeze
15
+ STEP_KEYS = %w[command script task plan source destination eval resources upload download].freeze
16
16
 
17
17
  def self.create(step_body, step_number)
18
18
  type_keys = (STEP_KEYS & step_body.keys)
@@ -22,8 +22,10 @@ module Bolt
22
22
  when 1
23
23
  type = type_keys.first
24
24
  else
25
- if type_keys.to_set == Set['source', 'destination']
25
+ if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
26
26
  type = 'upload'
27
+ elsif type_keys.to_set == Set['download', 'destination']
28
+ type = 'download'
27
29
  else
28
30
  raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
29
31
  end
@@ -89,6 +91,12 @@ module Bolt
89
91
  missing_keys -= ['targets']
90
92
  end
91
93
 
94
+ # Handle cases where upload step uses deprecated 'source' key instead of 'upload'
95
+ # TODO: Remove when 'source' is removed
96
+ if body.include?('source')
97
+ missing_keys -= ['upload']
98
+ end
99
+
92
100
  if missing_keys.any?
93
101
  error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
94
102
  err = step_error(error_message, body['name'], step_number)
@@ -156,3 +164,4 @@ require 'bolt/pal/yaml_plan/step/resources'
156
164
  require 'bolt/pal/yaml_plan/step/script'
157
165
  require 'bolt/pal/yaml_plan/step/task'
158
166
  require 'bolt/pal/yaml_plan/step/upload'
167
+ require 'bolt/pal/yaml_plan/step/download'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Download < Step
8
+ def self.allowed_keys
9
+ super + Set['download', 'destination']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['download', 'destination', 'targets']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @source = step_body['download']
19
+ @destination = step_body['destination']
20
+ end
21
+
22
+ def transpile
23
+ code = String.new(" ")
24
+ code << "$#{@name} = " if @name
25
+
26
+ fn = 'download_file'
27
+ args = [@source, @destination, @targets]
28
+ args << @description if @description
29
+
30
+ code << function_call(fn, args)
31
+
32
+ code << "\n"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -6,16 +6,16 @@ module Bolt
6
6
  class Step
7
7
  class Upload < Step
8
8
  def self.allowed_keys
9
- super + Set['source', 'destination']
9
+ super + Set['source', 'destination', 'upload']
10
10
  end
11
11
 
12
12
  def self.required_keys
13
- Set['destination', 'source', 'targets']
13
+ Set['upload', 'destination', 'targets']
14
14
  end
15
15
 
16
16
  def initialize(step_body)
17
17
  super
18
- @source = step_body['source']
18
+ @source = step_body['upload'] || step_body['source']
19
19
  @destination = step_body['destination']
20
20
  end
21
21
 
@@ -85,7 +85,8 @@ module Bolt
85
85
 
86
86
  def resolve_facts(config, certname, target_data)
87
87
  Bolt::Util.walk_vals(config) do |value|
88
- if value.is_a?(String)
88
+ case value
89
+ when String
89
90
  if value == 'certname'
90
91
  certname
91
92
  else
@@ -94,7 +95,7 @@ module Bolt
94
95
  # If there's no fact data this will be nil
95
96
  data&.fetch('value', nil)
96
97
  end
97
- elsif value.is_a?(Array) || value.is_a?(Hash)
98
+ when Array, Hash
98
99
  value
99
100
  else
100
101
  raise FactLookupError.new(value, "fact lookups must be a string")
@@ -17,7 +17,7 @@ module Bolt
17
17
 
18
18
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
19
  :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
- :deprecations
20
+ :deprecations, :downloads
21
21
 
22
22
  def self.default_project
23
23
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -81,6 +81,7 @@ module Bolt
81
81
  @rerunfile = @path + '.rerun.json'
82
82
  @resource_types = @path + '.resource_types'
83
83
  @type = type
84
+ @downloads = @path + 'downloads'
84
85
 
85
86
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
86
87
  if tc.any?
@@ -96,6 +96,8 @@ module Bolt
96
96
  @http = HTTPClient.new
97
97
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
98
98
  @http.ssl_config.add_trust_ca(@config.cacert)
99
+ @http.connect_timeout = @config.connect_timeout if @config.connect_timeout
100
+ @http.receive_timeout = @config.read_timeout if @config.read_timeout
99
101
 
100
102
  @http
101
103
  end
@@ -132,6 +132,22 @@ module Bolt
132
132
  end
133
133
  end
134
134
 
135
+ def connect_timeout
136
+ validate_timeout('connect_timeout')
137
+ @settings['connect_timeout']
138
+ end
139
+
140
+ def read_timeout
141
+ validate_timeout('read_timeout')
142
+ @settings['read_timeout']
143
+ end
144
+
145
+ def validate_timeout(timeout)
146
+ unless @settings[timeout].nil? || (@settings[timeout].is_a?(Integer) && @settings[timeout] > 0)
147
+ raise Bolt::PuppetDBError, "#{timeout} must be a positive integer, received #{@settings[timeout]}"
148
+ end
149
+ end
150
+
135
151
  def to_hash
136
152
  @settings.dup
137
153
  end
@@ -79,6 +79,13 @@ module Bolt
79
79
  new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
80
80
  end
81
81
 
82
+ def self.for_download(target, source, destination, download)
83
+ msg = "Downloaded '#{target.host}:#{source}' to '#{destination}'"
84
+ value = { 'path' => download }
85
+
86
+ new(target, value: value, message: msg, action: 'download', object: source)
87
+ end
88
+
82
89
  # Satisfies the Puppet datatypes API
83
90
  def self.from_asserted_args(target, value)
84
91
  new(target, value: value)
@@ -37,7 +37,7 @@ module Bolt
37
37
  with_tmpdir do |dir|
38
38
  basename = File.basename(source)
39
39
  tmpfile = File.join(dir.to_s, basename)
40
- conn.copy_file(source, tmpfile)
40
+ conn.upload_file(source, tmpfile)
41
41
  # pass over file ownership if we're using run-as to be a different user
42
42
  dir.chown(run_as)
43
43
  result = execute(['mv', '-f', tmpfile, destination], sudoable: true)
@@ -50,6 +50,27 @@ module Bolt
50
50
  end
51
51
  end
52
52
 
53
+ def download(source, destination, options = {})
54
+ running_as(options[:run_as]) do
55
+ # Target OS may be either Unix or Windows. Without knowing the target OS before-hand
56
+ # we can't assume whether the path separator is '/' or '\'. Assume we're connecting
57
+ # to a target with Unix and then check if the path exists after downloading.
58
+ download = File.join(destination, Bolt::Util.unix_basename(source))
59
+
60
+ conn.download_file(source, destination, download)
61
+
62
+ # If the download path doesn't exist, then the file was likely downloaded from Windows
63
+ # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
64
+ # should be saved to the expected location, so update the download path assuming a
65
+ # Windows basename so the result shows the correct local path.
66
+ unless File.exist?(download)
67
+ download = File.join(destination, Bolt::Util.windows_basename(source))
68
+ end
69
+
70
+ Bolt::Result.for_download(target, source, destination, download)
71
+ end
72
+ end
73
+
53
74
  def run_script(script, arguments, options = {})
54
75
  # unpack any Sensitive data
55
76
  arguments = unwrap_sensitive_args(arguments)
@@ -95,7 +116,7 @@ module Bolt
95
116
  task_dir = File.join(dir.to_s, task.tasks_dir)
96
117
  dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
97
118
  extra_files.each do |file|
98
- conn.copy_file(file['path'], File.join(dir.to_s, file['name']))
119
+ conn.upload_file(file['path'], File.join(dir.to_s, file['name']))
99
120
  end
100
121
  end
101
122
 
@@ -257,7 +278,7 @@ module Bolt
257
278
  def write_executable(dir, file, filename = nil)
258
279
  filename ||= File.basename(file)
259
280
  remote_path = File.join(dir.to_s, filename)
260
- conn.copy_file(file, remote_path)
281
+ conn.upload_file(file, remote_path)
261
282
  make_executable(remote_path)
262
283
  remote_path
263
284
  end
@@ -314,7 +335,6 @@ module Bolt
314
335
  sudo_str = if use_sudo
315
336
  sudo_exec = target.options['sudo-executable'] || "sudo"
316
337
  sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
317
- sudo_flags += ["-E"] if options[:environment]
318
338
  Shellwords.shelljoin(sudo_flags)
319
339
  else
320
340
  Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
@@ -85,7 +85,7 @@ module Bolt
85
85
  filename ||= File.basename(file)
86
86
  validate_extensions(File.extname(filename))
87
87
  destination = "#{dir}\\#{filename}"
88
- conn.copy_file(file, destination)
88
+ conn.upload_file(file, destination)
89
89
  destination
90
90
  end
91
91
 
@@ -164,10 +164,16 @@ module Bolt
164
164
  end
165
165
 
166
166
  def upload(source, destination, _options = {})
167
- conn.copy_file(source, destination)
167
+ conn.upload_file(source, destination)
168
168
  Bolt::Result.for_upload(target, source, destination)
169
169
  end
170
170
 
171
+ def download(source, destination, _options = {})
172
+ download = File.join(destination, Bolt::Util.windows_basename(source))
173
+ conn.download_file(source, destination, download)
174
+ Bolt::Result.for_download(target, source, destination, download)
175
+ end
176
+
171
177
  def run_command(command, options = {})
172
178
  command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
173
179
 
@@ -220,7 +226,7 @@ module Bolt
220
226
  task_dir = File.join(dir, task.tasks_dir)
221
227
  mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
222
228
  extra_files.each do |file|
223
- conn.copy_file(file['path'], File.join(dir, file['name']))
229
+ conn.upload_file(file['path'], File.join(dir, file['name']))
224
230
  end
225
231
  end
226
232
 
@@ -262,7 +268,7 @@ module Bolt
262
268
  return with_tmpdir do |dir|
263
269
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
264
270
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
265
- conn.copy_file(StringIO.new(command), script_file)
271
+ conn.upload_file(StringIO.new(command), script_file)
266
272
  args = escape_arguments([script_file])
267
273
  script_invocation = ['powershell.exe', *PS_ARGS, '-File', *args].join(' ')
268
274
  execute(script_invocation)