bolt 2.18.0 → 2.23.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  5. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  8. data/lib/bolt/analytics.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +74 -24
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +305 -108
  12. data/lib/bolt/config.rb +18 -10
  13. data/lib/bolt/config/options.rb +14 -0
  14. data/lib/bolt/executor.rb +26 -5
  15. data/lib/bolt/inventory/group.rb +3 -2
  16. data/lib/bolt/inventory/inventory.rb +4 -3
  17. data/lib/bolt/logger.rb +9 -0
  18. data/lib/bolt/module.rb +2 -1
  19. data/lib/bolt/outputter.rb +56 -0
  20. data/lib/bolt/outputter/human.rb +0 -9
  21. data/lib/bolt/outputter/json.rb +0 -4
  22. data/lib/bolt/outputter/rainbow.rb +9 -2
  23. data/lib/bolt/pal.rb +11 -9
  24. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  25. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  26. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  27. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  28. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  29. data/lib/bolt/plugin/module.rb +2 -4
  30. data/lib/bolt/plugin/prompt.rb +3 -3
  31. data/lib/bolt/plugin/puppetdb.rb +3 -2
  32. data/lib/bolt/project.rb +14 -9
  33. data/lib/bolt/puppetdb/client.rb +2 -0
  34. data/lib/bolt/puppetdb/config.rb +16 -0
  35. data/lib/bolt/result.rb +7 -0
  36. data/lib/bolt/shell/bash.rb +24 -4
  37. data/lib/bolt/shell/powershell.rb +10 -4
  38. data/lib/bolt/transport/base.rb +24 -0
  39. data/lib/bolt/transport/docker.rb +8 -0
  40. data/lib/bolt/transport/docker/connection.rb +20 -2
  41. data/lib/bolt/transport/local/connection.rb +14 -1
  42. data/lib/bolt/transport/orch.rb +12 -0
  43. data/lib/bolt/transport/simple.rb +6 -0
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
  46. data/lib/bolt/transport/winrm/connection.rb +118 -8
  47. data/lib/bolt/util.rb +26 -11
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_server/pe/pal.rb +1 -1
  50. data/lib/bolt_server/transport_app.rb +3 -2
  51. data/lib/bolt_spec/bolt_context.rb +7 -2
  52. data/lib/bolt_spec/plans.rb +15 -2
  53. data/lib/bolt_spec/plans/action_stubs.rb +3 -2
  54. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  55. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  56. data/lib/bolt_spec/run.rb +22 -0
  57. data/libexec/apply_catalog.rb +2 -2
  58. data/libexec/bolt_catalog +4 -3
  59. data/libexec/custom_facts.rb +1 -1
  60. data/libexec/query_resources.rb +1 -1
  61. data/modules/secure_env_vars/plans/init.pp +20 -0
  62. metadata +8 -2
@@ -12,7 +12,19 @@ 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[
16
+ command
17
+ destination
18
+ download
19
+ eval
20
+ message
21
+ plan
22
+ resources
23
+ script
24
+ source
25
+ task
26
+ upload
27
+ ].freeze
16
28
 
17
29
  def self.create(step_body, step_number)
18
30
  type_keys = (STEP_KEYS & step_body.keys)
@@ -22,8 +34,10 @@ module Bolt
22
34
  when 1
23
35
  type = type_keys.first
24
36
  else
25
- if type_keys.to_set == Set['source', 'destination']
37
+ if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
26
38
  type = 'upload'
39
+ elsif type_keys.to_set == Set['download', 'destination']
40
+ type = 'download'
27
41
  else
28
42
  raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
29
43
  end
@@ -89,6 +103,12 @@ module Bolt
89
103
  missing_keys -= ['targets']
90
104
  end
91
105
 
106
+ # Handle cases where upload step uses deprecated 'source' key instead of 'upload'
107
+ # TODO: Remove when 'source' is removed
108
+ if body.include?('source')
109
+ missing_keys -= ['upload']
110
+ end
111
+
92
112
  if missing_keys.any?
93
113
  error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
94
114
  err = step_error(error_message, body['name'], step_number)
@@ -156,3 +176,5 @@ require 'bolt/pal/yaml_plan/step/resources'
156
176
  require 'bolt/pal/yaml_plan/step/script'
157
177
  require 'bolt/pal/yaml_plan/step/task'
158
178
  require 'bolt/pal/yaml_plan/step/upload'
179
+ require 'bolt/pal/yaml_plan/step/download'
180
+ require 'bolt/pal/yaml_plan/step/message'
@@ -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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Message < Step
8
+ def self.allowed_keys
9
+ super + Set['message']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['message']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @message = step_body['message']
19
+ end
20
+
21
+ def transpile
22
+ code = String.new(" ")
23
+ code << function_call('out::message', [@message])
24
+ code << "\n"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ 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
 
@@ -184,12 +184,10 @@ module Bolt
184
184
  # Raises a deprecation warning if the pkcs7 plugin is using deprecated keys and
185
185
  # modifies the keys so they are the correct format
186
186
  def handle_deprecated_pkcs7_keys(params)
187
- if (params.key?('private-key') || params.key?('public-key')) && !@deprecation_warning_issued
188
- @deprecation_warning_issued = true
189
-
187
+ if params.key?('private-key') || params.key?('public-key')
190
188
  message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
191
189
  "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
192
- Logging.logger[self].warn(message)
190
+ Bolt::Logger.deprecation_warning('PKCS7 keys using hyphens, not underscores', message)
193
191
  end
194
192
 
195
193
  params['private_key'] = params.delete('private-key') if params.key?('private-key')
@@ -18,9 +18,9 @@ module Bolt
18
18
  end
19
19
 
20
20
  def resolve_reference(opts)
21
- STDERR.print("#{opts['message']}: ")
22
- value = STDIN.noecho(&:gets).to_s.chomp
23
- STDERR.puts
21
+ $stderr.print("#{opts['message']}: ")
22
+ value = $stdin.noecho(&:gets).to_s.chomp
23
+ $stderr.puts
24
24
 
25
25
  value
26
26
  end
@@ -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")
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pathname'
4
- require 'bolt/pal'
5
4
  require 'bolt/config'
5
+ require 'bolt/pal'
6
+ require 'bolt/module'
6
7
 
7
8
  module Bolt
8
9
  class Project
@@ -16,7 +17,8 @@ module Bolt
16
17
  }.freeze
17
18
 
18
19
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
20
+ :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
21
+ :deprecations, :downloads, :plans_path
20
22
 
21
23
  def self.default_project
22
24
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -65,11 +67,12 @@ module Bolt
65
67
  @project_file = @path + 'bolt-project.yaml'
66
68
 
67
69
  @warnings = []
70
+ @deprecations = []
68
71
  if (@path + 'bolt.yaml').file? && project_file?
69
72
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
70
73
  "Transport config should be set in inventory.yaml, all other config should be set in "\
71
74
  "bolt-project.yaml."
72
- @warnings << { msg: msg }
75
+ @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
73
76
  end
74
77
 
75
78
  @inventory_file = @path + 'inventory.yaml'
@@ -79,6 +82,8 @@ module Bolt
79
82
  @rerunfile = @path + '.rerun.json'
80
83
  @resource_types = @path + '.resource_types'
81
84
  @type = type
85
+ @downloads = @path + 'downloads'
86
+ @plans_path = @path + 'plans'
82
87
 
83
88
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
84
89
  if tc.any?
@@ -109,7 +114,7 @@ module Bolt
109
114
  # This API is used to prepend the project as a module to Puppet's internal
110
115
  # module_references list. CHANGE AT YOUR OWN RISK
111
116
  def to_h
112
- { path: @path, name: name }
117
+ { path: @path.to_s, name: name }
113
118
  end
114
119
 
115
120
  def eql?(other)
@@ -135,10 +140,10 @@ module Bolt
135
140
 
136
141
  def validate
137
142
  if name
138
- name_regex = /^[a-z][a-z0-9_]*$/
139
- if name !~ name_regex
143
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
140
144
  raise Bolt::ValidationError, <<~ERROR_STRING
141
- Invalid project name '#{name}' in bolt-project.yaml; project name must match #{name_regex.inspect}
145
+ Invalid project name '#{name}' in bolt-project.yaml; project name must begin with a lowercase letter
146
+ and can include lowercase letters, numbers, and underscores.
142
147
  ERROR_STRING
143
148
  elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
144
149
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
@@ -158,8 +163,8 @@ module Bolt
158
163
 
159
164
  def check_deprecated_file
160
165
  if (@path + 'project.yaml').file?
161
- logger = Logging.logger[self]
162
- logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
166
+ msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
167
+ Bolt::Logger.deprecation_warning('Using project.yaml instead of bolt-project.yaml', msg)
163
168
  end
164
169
  end
165
170
  end
@@ -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)
@@ -167,6 +167,25 @@ module Bolt
167
167
  end
168
168
  end
169
169
 
170
+ # Downloads the given source file from a batch of targets to the destination location
171
+ # on the host.
172
+ #
173
+ # The default implementation only supports batches of size 1 and will fail otherwise.
174
+ #
175
+ # Transports may override this method to implement their own batch processing.
176
+ def batch_download(targets, source, destination, options = {}, &callback)
177
+ require 'erb'
178
+
179
+ assert_batch_size_one("batch_download()", targets)
180
+ target = targets.first
181
+ with_events(target, callback, 'download') do
182
+ escaped_name = ERB::Util.url_encode(target.safe_name)
183
+ target_destination = File.expand_path(escaped_name, destination)
184
+ @logger.debug { "Downloading: '#{source}' on #{target.safe_name} to #{target_destination}" }
185
+ download(target, source, target_destination, options)
186
+ end
187
+ end
188
+
170
189
  def batch_connected?(targets)
171
190
  assert_batch_size_one("connected?()", targets)
172
191
  connected?(targets.first)
@@ -201,6 +220,11 @@ module Bolt
201
220
  raise NotImplementedError, "upload() must be implemented by the transport class"
202
221
  end
203
222
 
223
+ # Transports should override this method with their own implementation of file download.
224
+ def download(*_args)
225
+ raise NotImplementedError, "download() must be implemented by the transport class"
226
+ end
227
+
204
228
  # Transports should override this method with their own implementation of a connection test.
205
229
  def connected?(_targets)
206
230
  raise NotImplementedError, "connected?() must be implemented by the transport class"