bolt 3.1.0 → 3.3.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.
- checksums.yaml +4 -4
- data/Puppetfile +8 -8
- data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -5
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
- data/lib/bolt/apply_result.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +6 -3
- data/lib/bolt/cli.rb +37 -12
- data/lib/bolt/config.rb +4 -0
- data/lib/bolt/config/options.rb +21 -3
- data/lib/bolt/config/transport/lxd.rb +21 -0
- data/lib/bolt/config/transport/options.rb +1 -1
- data/lib/bolt/executor.rb +10 -3
- data/lib/bolt/logger.rb +8 -0
- data/lib/bolt/module_installer.rb +2 -2
- data/lib/bolt/module_installer/puppetfile.rb +2 -2
- data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
- data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
- data/lib/bolt/outputter/human.rb +47 -12
- data/lib/bolt/pal.rb +2 -2
- data/lib/bolt/pal/yaml_plan.rb +1 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -141
- data/lib/bolt/pal/yaml_plan/step.rb +91 -31
- data/lib/bolt/pal/yaml_plan/step/command.rb +16 -16
- data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
- data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
- data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
- data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
- data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
- data/lib/bolt/pal/yaml_plan/step/script.rb +32 -17
- data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
- data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
- data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
- data/lib/bolt/plan_creator.rb +1 -1
- data/lib/bolt/project_manager.rb +1 -1
- data/lib/bolt/project_manager/module_migrator.rb +1 -1
- data/lib/bolt/shell.rb +16 -0
- data/lib/bolt/shell/bash.rb +48 -21
- data/lib/bolt/shell/bash/tmpdir.rb +2 -2
- data/lib/bolt/shell/powershell.rb +24 -5
- data/lib/bolt/task.rb +1 -1
- data/lib/bolt/transport/lxd.rb +26 -0
- data/lib/bolt/transport/lxd/connection.rb +99 -0
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/transport/winrm/connection.rb +1 -1
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +13 -1
- data/lib/bolt_spec/plans/action_stubs.rb +1 -1
- data/lib/bolt_spec/plans/mock_executor.rb +4 -0
- metadata +5 -2
@@ -6,35 +6,50 @@ module Bolt
|
|
6
6
|
class Step
|
7
7
|
class Script < Step
|
8
8
|
def self.allowed_keys
|
9
|
-
super + Set['
|
9
|
+
super + Set['arguments', 'pwsh_params']
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.option_keys
|
13
|
+
Set['catch_errors', 'env_vars', 'run_as']
|
10
14
|
end
|
11
15
|
|
12
16
|
def self.required_keys
|
13
|
-
Set['targets']
|
17
|
+
Set['script', 'targets']
|
14
18
|
end
|
15
19
|
|
16
|
-
def
|
20
|
+
def self.validate_step_keys(body, number)
|
17
21
|
super
|
18
|
-
|
19
|
-
|
20
|
-
|
22
|
+
|
23
|
+
if body.key?('arguments') && !body['arguments'].nil? && !body['arguments'].is_a?(Array)
|
24
|
+
raise StepError.new('arguments key must be an array', body['name'], number)
|
25
|
+
end
|
26
|
+
|
27
|
+
if body.key?('pwsh_params') && !body['pwsh_params'].nil? && !body['pwsh_params'].is_a?(Hash)
|
28
|
+
raise StepError.new('pwsh_params key must be a hash', body['name'], number)
|
29
|
+
end
|
21
30
|
end
|
22
31
|
|
23
|
-
|
24
|
-
|
25
|
-
|
32
|
+
# Returns an array of arguments to pass to the step's function call
|
33
|
+
#
|
34
|
+
private def format_args(body)
|
35
|
+
args = body['arguments'] || []
|
36
|
+
pwsh_params = body['pwsh_params'] || {}
|
26
37
|
|
27
|
-
|
28
|
-
|
38
|
+
opts = format_options(body)
|
39
|
+
opts = opts.merge('arguments' => args) if args.any?
|
40
|
+
opts = opts.merge('pwsh_params' => pwsh_params) if pwsh_params.any?
|
29
41
|
|
30
|
-
|
31
|
-
args
|
32
|
-
args <<
|
33
|
-
args << options unless options.empty?
|
42
|
+
args = [body['script'], body['targets']]
|
43
|
+
args << body['description'] if body['description']
|
44
|
+
args << opts if opts.any?
|
34
45
|
|
35
|
-
|
46
|
+
args
|
47
|
+
end
|
36
48
|
|
37
|
-
|
49
|
+
# Returns the function corresponding to the step
|
50
|
+
#
|
51
|
+
private def function
|
52
|
+
'run_script'
|
38
53
|
end
|
39
54
|
end
|
40
55
|
end
|
@@ -6,31 +6,34 @@ module Bolt
|
|
6
6
|
class Step
|
7
7
|
class Task < Step
|
8
8
|
def self.allowed_keys
|
9
|
-
super + Set['
|
9
|
+
super + Set['parameters']
|
10
10
|
end
|
11
11
|
|
12
|
-
def self.
|
13
|
-
Set['
|
12
|
+
def self.option_keys
|
13
|
+
Set['catch_errors', 'noop', 'run_as']
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
@task = step_body['task']
|
19
|
-
@parameters = step_body.fetch('parameters', {})
|
16
|
+
def self.required_keys
|
17
|
+
Set['targets', 'task']
|
20
18
|
end
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
# Returns an array of arguments to pass to the step's function call
|
21
|
+
#
|
22
|
+
private def format_args(body)
|
23
|
+
opts = format_options(body)
|
24
|
+
params = (body['parameters'] || {}).merge(opts)
|
25
25
|
|
26
|
-
|
27
|
-
args
|
28
|
-
args <<
|
29
|
-
args << @parameters unless @parameters.empty?
|
26
|
+
args = [body['task'], body['targets']]
|
27
|
+
args << body['description'] if body['description']
|
28
|
+
args << params if params.any?
|
30
29
|
|
31
|
-
|
30
|
+
args
|
31
|
+
end
|
32
32
|
|
33
|
-
|
33
|
+
# Returns the function corresponding to the step
|
34
|
+
#
|
35
|
+
private def function
|
36
|
+
'run_task'
|
34
37
|
end
|
35
38
|
end
|
36
39
|
end
|
@@ -5,31 +5,30 @@ module Bolt
|
|
5
5
|
class YamlPlan
|
6
6
|
class Step
|
7
7
|
class Upload < Step
|
8
|
-
def self.
|
9
|
-
|
8
|
+
def self.option_keys
|
9
|
+
Set['catch_errors', 'run_as']
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.required_keys
|
13
|
-
Set['
|
13
|
+
Set['destination', 'targets', 'upload']
|
14
14
|
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def transpile
|
23
|
-
code = String.new(" ")
|
24
|
-
code << "$#{@name} = " if @name
|
16
|
+
# Returns an array of arguments to pass to the step's function call
|
17
|
+
#
|
18
|
+
private def format_args(body)
|
19
|
+
opts = format_options(body)
|
25
20
|
|
26
|
-
|
27
|
-
args
|
28
|
-
args <<
|
21
|
+
args = [body['upload'], body['destination'], body['targets']]
|
22
|
+
args << body['description'] if body['description']
|
23
|
+
args << opts if opts.any?
|
29
24
|
|
30
|
-
|
25
|
+
args
|
26
|
+
end
|
31
27
|
|
32
|
-
|
28
|
+
# Returns the function corresponding to the step
|
29
|
+
#
|
30
|
+
private def function
|
31
|
+
'upload_file'
|
33
32
|
end
|
34
33
|
end
|
35
34
|
end
|
@@ -29,7 +29,7 @@ module Bolt
|
|
29
29
|
|
30
30
|
plan_string = String.new('')
|
31
31
|
plan_string << "# #{plan_object.description}\n" if plan_object.description
|
32
|
-
plan_string << "# WARNING: This is an autogenerated plan. It
|
32
|
+
plan_string << "# WARNING: This is an autogenerated plan. It might not behave as expected.\n"
|
33
33
|
plan_string << "# @private #{plan_object.private}\n" unless plan_object.private.nil?
|
34
34
|
plan_string << "#{param_descriptions}\n" unless param_descriptions.empty?
|
35
35
|
|
data/lib/bolt/plan_creator.rb
CHANGED
@@ -22,7 +22,7 @@ module Bolt
|
|
22
22
|
Invalid plan name '#{plan_name}'. Plan names are composed of one or more name segments
|
23
23
|
separated by double colons '::'.
|
24
24
|
|
25
|
-
Each name segment must begin with a lowercase letter, and
|
25
|
+
Each name segment must begin with a lowercase letter, and can only include lowercase
|
26
26
|
letters, digits, and underscores.
|
27
27
|
|
28
28
|
Examples of valid plan names:
|
data/lib/bolt/project_manager.rb
CHANGED
@@ -143,7 +143,7 @@ module Bolt
|
|
143
143
|
@outputter.print_message("Migrating project #{@config.project.path}\n\n")
|
144
144
|
|
145
145
|
@outputter.print_action_step(
|
146
|
-
"Migrating a Bolt project
|
146
|
+
"Migrating a Bolt project might make irreversible changes to the project's "\
|
147
147
|
"configuration and inventory files. Before continuing, make sure the "\
|
148
148
|
"project has a backup or uses a version control system."
|
149
149
|
)
|
@@ -66,7 +66,7 @@ module Bolt
|
|
66
66
|
# Attempt to resolve dependencies
|
67
67
|
begin
|
68
68
|
@outputter.print_message('')
|
69
|
-
@outputter.print_action_step("Resolving module dependencies, this
|
69
|
+
@outputter.print_action_step("Resolving module dependencies, this might take a moment")
|
70
70
|
puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs)
|
71
71
|
rescue Bolt::Error => e
|
72
72
|
@outputter.print_action_error("#{e.message}\nSkipping module migration.")
|
data/lib/bolt/shell.rb
CHANGED
@@ -8,6 +8,22 @@ module Bolt
|
|
8
8
|
@target = target
|
9
9
|
@conn = conn
|
10
10
|
@logger = Bolt::Logger.logger(@target.safe_name)
|
11
|
+
|
12
|
+
if Bolt::Logger.stream
|
13
|
+
Bolt::Logger.warn_once("stream_experimental",
|
14
|
+
"The 'stream' option is experimental, and might "\
|
15
|
+
"include breaking changes between minor versions.")
|
16
|
+
@stream_logger = Bolt::Logger.logger(:stream)
|
17
|
+
# Don't send stream messages to the parent logger
|
18
|
+
@stream_logger.additive = false
|
19
|
+
|
20
|
+
# Log stream messages without any other data or color
|
21
|
+
pattern = Logging.layouts.pattern(pattern: '%m\n')
|
22
|
+
@stream_logger.appenders = Logging.appenders.stdout(
|
23
|
+
'console',
|
24
|
+
layout: pattern
|
25
|
+
)
|
26
|
+
end
|
11
27
|
end
|
12
28
|
|
13
29
|
def run_command(*_args)
|
data/lib/bolt/shell/bash.rb
CHANGED
@@ -12,7 +12,6 @@ module Bolt
|
|
12
12
|
super
|
13
13
|
|
14
14
|
@run_as = nil
|
15
|
-
|
16
15
|
@sudo_id = SecureRandom.uuid
|
17
16
|
@sudo_password = @target.options['sudo-password'] || @target.password
|
18
17
|
end
|
@@ -54,19 +53,35 @@ module Bolt
|
|
54
53
|
|
55
54
|
def download(source, destination, options = {})
|
56
55
|
running_as(options[:run_as]) do
|
57
|
-
# Target OS may be either Unix or Windows. Without knowing the target OS before-hand
|
58
|
-
# we can't assume whether the path separator is '/' or '\'. Assume we're connecting
|
59
|
-
# to a target with Unix and then check if the path exists after downloading.
|
60
56
|
download = File.join(destination, Bolt::Util.unix_basename(source))
|
61
57
|
|
62
|
-
|
58
|
+
# If using run-as, the file is copied to a tmpdir and chowned to the
|
59
|
+
# connecting user. This is a workaround for limitations in net-ssh that
|
60
|
+
# only allow for downloading files as the connecting user, which is a
|
61
|
+
# problem for users who cannot connect to targets as the root user.
|
62
|
+
# This temporary copy should *always* be deleted.
|
63
|
+
if run_as
|
64
|
+
with_tmpdir(force_cleanup: true) do |dir|
|
65
|
+
tmpfile = File.join(dir.to_s, Bolt::Util.unix_basename(source))
|
66
|
+
|
67
|
+
result = execute(['cp', '-r', source, dir.to_s], sudoable: true)
|
68
|
+
|
69
|
+
if result.exit_code != 0
|
70
|
+
message = "Could not copy file '#{source}' to temporary directory '#{dir}': #{result.stderr.string}"
|
71
|
+
raise Bolt::Node::FileError.new(message, 'CP_ERROR')
|
72
|
+
end
|
73
|
+
|
74
|
+
# We need to force the chown, otherwise this will just return
|
75
|
+
# without doing anything since the chown user is the same as the
|
76
|
+
# connecting user.
|
77
|
+
dir.chown(conn.user, force: true)
|
63
78
|
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
#
|
68
|
-
|
69
|
-
|
79
|
+
conn.download_file(tmpfile, destination, download)
|
80
|
+
end
|
81
|
+
# If not using run-as, we can skip creating a temporary copy and just
|
82
|
+
# download the file directly.
|
83
|
+
else
|
84
|
+
conn.download_file(source, destination, download)
|
70
85
|
end
|
71
86
|
|
72
87
|
Bolt::Result.for_download(target, source, destination, download)
|
@@ -145,7 +160,10 @@ module Bolt
|
|
145
160
|
|
146
161
|
dir.chown(run_as)
|
147
162
|
|
148
|
-
|
163
|
+
# Don't pass parameters on stdin if using a tty, as the parameters are
|
164
|
+
# already part of the wrapper script.
|
165
|
+
execute_options[:stdin] = stdin unless stdin && target.options['tty']
|
166
|
+
|
149
167
|
execute_options[:sudoable] = true if run_as
|
150
168
|
output = execute(remote_task_path, **execute_options)
|
151
169
|
end
|
@@ -291,12 +309,12 @@ module Bolt
|
|
291
309
|
|
292
310
|
# A helper to create and delete a tmpdir on the remote system. Yields the
|
293
311
|
# directory name.
|
294
|
-
def with_tmpdir
|
312
|
+
def with_tmpdir(force_cleanup: false)
|
295
313
|
dir = make_tmpdir
|
296
314
|
yield dir
|
297
315
|
ensure
|
298
316
|
if dir
|
299
|
-
if target.options['cleanup']
|
317
|
+
if target.options['cleanup'] || force_cleanup
|
300
318
|
dir.delete
|
301
319
|
else
|
302
320
|
Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
|
@@ -390,14 +408,23 @@ module Bolt
|
|
390
408
|
# See if we can read from out or err, or write to in
|
391
409
|
ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
|
392
410
|
|
393
|
-
# Read from out and err
|
394
411
|
ready_read&.each do |stream|
|
412
|
+
stream_name = stream == out ? 'out' : 'err'
|
395
413
|
# Check for sudo prompt
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
414
|
+
to_print = if use_sudo
|
415
|
+
check_sudo(stream, inp, options[:stdin])
|
416
|
+
else
|
417
|
+
stream.readpartial(CHUNK_SIZE)
|
418
|
+
end
|
419
|
+
|
420
|
+
if !to_print.chomp.empty? && @stream_logger
|
421
|
+
formatted = to_print.lines.map do |msg|
|
422
|
+
"[#{@target.safe_name}] #{stream_name}: #{msg.chomp}"
|
423
|
+
end.join("\n")
|
424
|
+
@stream_logger.warn(formatted)
|
425
|
+
end
|
426
|
+
|
427
|
+
read_streams[stream] << to_print
|
401
428
|
rescue EOFError
|
402
429
|
end
|
403
430
|
|
@@ -445,7 +472,7 @@ module Bolt
|
|
445
472
|
when 0
|
446
473
|
@logger.trace { "Command `#{command_str}` returned successfully" }
|
447
474
|
when 126
|
448
|
-
msg = "\n\nThis
|
475
|
+
msg = "\n\nThis might be caused by the default tmpdir being mounted "\
|
449
476
|
"using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
|
450
477
|
result_output.stderr << msg
|
451
478
|
@logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
|
@@ -24,8 +24,8 @@ module Bolt
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def chown(owner)
|
28
|
-
return if owner.nil? || owner == @owner
|
27
|
+
def chown(owner, force: false)
|
28
|
+
return if owner.nil? || (owner == @owner && !force)
|
29
29
|
|
30
30
|
result = @shell.execute(['id', '-g', owner])
|
31
31
|
if result.exit_code != 0
|
@@ -208,16 +208,21 @@ module Bolt
|
|
208
208
|
arguments = unwrap_sensitive_args(arguments)
|
209
209
|
with_tmpdir do |dir|
|
210
210
|
script_path = write_executable(dir, script)
|
211
|
-
command = if powershell_file?(script_path)
|
211
|
+
command = if powershell_file?(script_path) && options[:pwsh_params]
|
212
|
+
# Scripts run with pwsh_params can be run like tasks
|
213
|
+
Snippets.ps_task(script_path, options[:pwsh_params])
|
214
|
+
elsif powershell_file?(script_path)
|
212
215
|
Snippets.run_script(arguments, script_path)
|
213
216
|
else
|
214
217
|
path, args = *process_from_extension(script_path)
|
215
218
|
args += escape_arguments(arguments)
|
216
219
|
execute_process(path, args)
|
217
220
|
end
|
218
|
-
|
221
|
+
env_assignments = options[:env_vars] ? env_declarations(options[:env_vars]) : []
|
222
|
+
shell_init = options[:pwsh_params] ? Snippets.shell_init : ''
|
223
|
+
|
224
|
+
output = execute([shell_init, *env_assignments, command].join("\r\n"))
|
219
225
|
|
220
|
-
output = execute(command)
|
221
226
|
Bolt::Result.for_command(target,
|
222
227
|
output.stdout.string,
|
223
228
|
output.stderr.string,
|
@@ -310,12 +315,26 @@ module Bolt
|
|
310
315
|
# the proper encoding so the string isn't later misinterpreted
|
311
316
|
encoding = out.external_encoding
|
312
317
|
out.binmode
|
313
|
-
|
318
|
+
to_print = out.read.force_encoding(encoding)
|
319
|
+
if !to_print.chomp.empty? && @stream_logger
|
320
|
+
formatted = to_print.lines.map do |msg|
|
321
|
+
"[#{@target.safe_name}] out: #{msg.chomp}"
|
322
|
+
end.join("\n")
|
323
|
+
@stream_logger.warn(formatted)
|
324
|
+
end
|
325
|
+
result.stdout << to_print
|
314
326
|
end
|
315
327
|
stderr = Thread.new do
|
316
328
|
encoding = err.external_encoding
|
317
329
|
err.binmode
|
318
|
-
|
330
|
+
to_print = err.read.force_encoding(encoding)
|
331
|
+
if !to_print.chomp.empty? && @stream_logger
|
332
|
+
formatted = to_print.lines.map do |msg|
|
333
|
+
"[#{@target.safe_name}] err: #{msg.chomp}"
|
334
|
+
end.join("\n")
|
335
|
+
@stream_logger.warn(formatted)
|
336
|
+
end
|
337
|
+
result.stderr << to_print
|
319
338
|
end
|
320
339
|
|
321
340
|
stdout.join
|
data/lib/bolt/task.rb
CHANGED
@@ -148,7 +148,7 @@ module Bolt
|
|
148
148
|
|
149
149
|
if unknown_keys.any?
|
150
150
|
msg = "Metadata for task '#{@name}' contains unknown keys: #{unknown_keys.join(', ')}."
|
151
|
-
msg += " This could be a typo in the task metadata or
|
151
|
+
msg += " This could be a typo in the task metadata or might result in incorrect behavior."
|
152
152
|
Bolt::Logger.warn("unknown_task_metadata_keys", msg)
|
153
153
|
end
|
154
154
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/logger'
|
4
|
+
require 'bolt/node/errors'
|
5
|
+
require 'bolt/transport/simple'
|
6
|
+
|
7
|
+
module Bolt
|
8
|
+
module Transport
|
9
|
+
class LXD < Simple
|
10
|
+
def provided_features
|
11
|
+
['shell']
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_connection(target, options = {})
|
15
|
+
Bolt::Logger.warn_once("lxd_experimental",
|
16
|
+
"The LXD transport is experimental, and might "\
|
17
|
+
"include breaking changes between minor versions.")
|
18
|
+
conn = Connection.new(target, options)
|
19
|
+
conn.connect
|
20
|
+
yield conn
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'bolt/transport/lxd/connection'
|