bolt 1.15.0 → 1.16.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +4 -4
  4. data/lib/bolt.rb +3 -0
  5. data/lib/bolt/analytics.rb +7 -2
  6. data/lib/bolt/applicator.rb +6 -2
  7. data/lib/bolt/bolt_option_parser.rb +4 -4
  8. data/lib/bolt/cli.rb +8 -4
  9. data/lib/bolt/config.rb +6 -6
  10. data/lib/bolt/executor.rb +2 -7
  11. data/lib/bolt/inventory.rb +37 -6
  12. data/lib/bolt/inventory/group2.rb +314 -0
  13. data/lib/bolt/inventory/inventory2.rb +261 -0
  14. data/lib/bolt/outputter/human.rb +3 -1
  15. data/lib/bolt/pal.rb +8 -7
  16. data/lib/bolt/puppetdb/client.rb +6 -5
  17. data/lib/bolt/target.rb +34 -14
  18. data/lib/bolt/task.rb +2 -2
  19. data/lib/bolt/transport/base.rb +2 -2
  20. data/lib/bolt/transport/docker.rb +1 -1
  21. data/lib/bolt/transport/docker/connection.rb +2 -0
  22. data/lib/bolt/transport/local.rb +9 -181
  23. data/lib/bolt/transport/local/shell.rb +202 -12
  24. data/lib/bolt/transport/local_windows.rb +203 -0
  25. data/lib/bolt/transport/orch.rb +6 -4
  26. data/lib/bolt/transport/orch/connection.rb +6 -2
  27. data/lib/bolt/transport/ssh.rb +10 -150
  28. data/lib/bolt/transport/ssh/connection.rb +15 -116
  29. data/lib/bolt/transport/sudoable.rb +163 -0
  30. data/lib/bolt/transport/sudoable/connection.rb +76 -0
  31. data/lib/bolt/transport/sudoable/tmpdir.rb +59 -0
  32. data/lib/bolt/transport/winrm.rb +4 -4
  33. data/lib/bolt/transport/winrm/connection.rb +1 -0
  34. data/lib/bolt/util.rb +2 -0
  35. data/lib/bolt/version.rb +1 -1
  36. data/lib/bolt_ext/puppetdb_inventory.rb +0 -1
  37. data/lib/bolt_server/transport_app.rb +3 -1
  38. data/lib/logging_extensions/logging.rb +13 -0
  39. data/lib/plan_executor/orch_client.rb +4 -0
  40. metadata +23 -2
@@ -4,71 +4,27 @@ require 'logging'
4
4
  require 'shellwords'
5
5
  require 'bolt/node/errors'
6
6
  require 'bolt/node/output'
7
+ require 'bolt/transport/sudoable/connection'
7
8
  require 'bolt/util'
8
- require 'net/ssh/proxy/jump'
9
9
 
10
10
  module Bolt
11
11
  module Transport
12
- class SSH < Base
13
- class Connection
14
- class RemoteTempdir
15
- def initialize(node, path)
16
- @node = node
17
- @owner = node.user
18
- @path = path
19
- @logger = node.logger
20
- end
21
-
22
- def to_s
23
- @path
24
- end
25
-
26
- def mkdirs(subdirs)
27
- abs_subdirs = subdirs.map { |subdir| File.join(@path, subdir) }
28
- result = @node.execute(['mkdir', '-p'] + abs_subdirs)
29
- if result.exit_code != 0
30
- message = "Could not create subdirectories in '#{@path}': #{result.stderr.string}"
31
- raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
32
- end
33
- end
34
-
35
- def chown(owner)
36
- return if owner.nil? || owner == @owner
37
-
38
- result = @node.execute(['id', '-g', owner])
39
- if result.exit_code != 0
40
- message = "Could not identify group of user #{owner}: #{result.stderr.string}"
41
- raise Bolt::Node::FileError.new(message, 'ID_ERROR')
42
- end
43
- group = result.stdout.string.chomp
44
-
45
- # Chown can only be run by root.
46
- result = @node.execute(['chown', '-R', "#{owner}:#{group}", @path], sudoable: true, run_as: 'root')
47
- if result.exit_code != 0
48
- message = "Could not change owner of '#{@path}' to #{owner}: #{result.stderr.string}"
49
- raise Bolt::Node::FileError.new(message, 'CHOWN_ERROR')
50
- end
51
-
52
- # File ownership successfully changed, record the new owner.
53
- @owner = owner
54
- end
55
-
56
- def delete
57
- result = @node.execute(['rm', '-rf', @path], sudoable: true, run_as: @owner)
58
- if result.exit_code != 0
59
- @logger.warn("Failed to clean up tempdir '#{@path}': #{result.stderr.string}")
60
- end
61
- end
62
- end
63
-
12
+ class SSH < Sudoable
13
+ class Connection < Sudoable::Connection
64
14
  attr_reader :logger, :user, :target
65
15
  attr_writer :run_as
66
16
 
67
- def initialize(target, transport_logger, load_config = true)
17
+ def initialize(target, transport_logger)
18
+ # lazy-load expensive gem code
19
+ require 'net/ssh'
20
+ require 'net/ssh/proxy/jump'
21
+
22
+ raise Bolt::ValidationError, "Target #{target.name} does not have a host" unless target.host
23
+
68
24
  @target = target
69
- @load_config = load_config
25
+ @load_config = target.options['load-config']
70
26
 
71
- ssh_user = load_config ? Net::SSH::Config.for(target.host)[:user] : nil
27
+ ssh_user = @load_config ? Net::SSH::Config.for(target.host)[:user] : nil
72
28
  @user = @target.user || ssh_user || Etc.getlogin
73
29
  @run_as = nil
74
30
 
@@ -171,27 +127,8 @@ module Bolt
171
127
  end
172
128
  end
173
129
 
174
- # This method allows the @run_as variable to be used as a per-operation
175
- # override for the user to run as. When @run_as is unset, the user
176
- # specified on the target will be used.
177
- def run_as
178
- @run_as || target.options['run-as']
179
- end
180
-
181
- # Run as the specified user for the duration of the block.
182
- def running_as(user)
183
- @run_as = user
184
- yield
185
- ensure
186
- @run_as = nil
187
- end
188
-
189
- def sudo_prompt
190
- '[sudo] Bolt needs to run as another user, password: '
191
- end
192
-
193
130
  def handled_sudo(channel, data)
194
- if data.lines.include?(sudo_prompt)
131
+ if data.lines.include?(Sudoable.sudo_prompt)
195
132
  if target.options['sudo-password']
196
133
  channel.send_data "#{target.options['sudo-password']}\n"
197
134
  channel.wait
@@ -233,7 +170,7 @@ module Bolt
233
170
  command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
234
171
  if escalate
235
172
  if use_sudo
236
- sudo_flags = ["sudo", "-S", "-u", run_as, "-p", sudo_prompt]
173
+ sudo_flags = ["sudo", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
237
174
  sudo_flags += ["-E"] if options[:environment]
238
175
  sudo_str = Shellwords.shelljoin(sudo_flags)
239
176
  command_str = "#{sudo_str} #{command_str}"
@@ -303,56 +240,18 @@ module Bolt
303
240
  raise
304
241
  end
305
242
 
306
- def write_remote_file(source, destination)
243
+ def copy_file(source, destination)
307
244
  @session.scp.upload!(source, destination, recursive: true)
308
245
  rescue StandardError => e
309
246
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
310
247
  end
311
248
 
312
- def make_tempdir
313
- tmpdir = target.options.fetch('tmpdir', '/tmp')
314
- tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
315
- command = ['mkdir', '-m', 700, tmppath]
316
-
317
- result = execute(command)
318
- if result.exit_code != 0
319
- raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
320
- end
321
- path = tmppath || result.stdout.string.chomp
322
- RemoteTempdir.new(self, path)
323
- end
324
-
325
- # A helper to create and delete a tempdir on the remote system. Yields the
326
- # directory name.
327
- def with_remote_tempdir
328
- dir = make_tempdir
329
- yield dir
330
- ensure
331
- dir&.delete
332
- end
333
-
334
- def write_remote_executable(dir, file, filename = nil)
335
- filename ||= File.basename(file)
336
- remote_path = File.join(dir.to_s, filename)
337
- write_remote_file(file, remote_path)
338
- make_executable(remote_path)
339
- remote_path
340
- end
341
-
342
249
  def write_executable_from_content(dest, content, filename)
343
250
  remote_path = File.join(dest.to_s, filename)
344
251
  @session.scp.upload!(StringIO.new(content), remote_path)
345
252
  make_executable(remote_path)
346
253
  remote_path
347
254
  end
348
-
349
- def make_executable(path)
350
- result = execute(['chmod', 'u+x', path])
351
- if result.exit_code != 0
352
- message = "Could not make file '#{path}' executable: #{result.stderr.string}"
353
- raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
354
- end
355
- end
356
255
  end
357
256
  end
358
257
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+ require 'bolt/transport/base'
5
+
6
+ module Bolt
7
+ module Transport
8
+ class Sudoable < Base
9
+ def self.validate_sudo_options(options, logger)
10
+ if options['sudo-password'] && options['run-as'].nil?
11
+ logger.warn("--sudo-password will not be used without specifying a " \
12
+ "user to escalate to with --run-as")
13
+ end
14
+
15
+ run_as_cmd = options['run-as-command']
16
+ if run_as_cmd && (!run_as_cmd.is_a?(Array) || run_as_cmd.any? { |n| !n.is_a?(String) })
17
+ raise Bolt::ValidationError, "run-as-command must be an Array of Strings, received #{run_as_cmd}"
18
+ end
19
+ end
20
+
21
+ def self.sudo_prompt
22
+ '[sudo] Bolt needs to run as another user, password: '
23
+ end
24
+
25
+ def run_command(target, command, options = {})
26
+ with_connection(target) do |conn|
27
+ conn.running_as(options['_run_as']) do
28
+ output = conn.execute(command, sudoable: true)
29
+ Bolt::Result.for_command(target,
30
+ output.stdout.string,
31
+ output.stderr.string,
32
+ output.exit_code,
33
+ 'command', command)
34
+ end
35
+ end
36
+ end
37
+
38
+ def upload(target, source, destination, options = {})
39
+ with_connection(target) do |conn|
40
+ conn.running_as(options['_run_as']) do
41
+ conn.with_tempdir do |dir|
42
+ basename = File.basename(destination)
43
+ tmpfile = File.join(dir.to_s, basename)
44
+ conn.copy_file(source, tmpfile)
45
+ # pass over file ownership if we're using run-as to be a different user
46
+ dir.chown(conn.run_as)
47
+ result = conn.execute(['mv', tmpfile, destination], sudoable: true)
48
+ if result.exit_code != 0
49
+ message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}"
50
+ raise Bolt::Node::FileError.new(message, 'MV_ERROR')
51
+ end
52
+ end
53
+ Bolt::Result.for_upload(target, source, destination)
54
+ end
55
+ end
56
+ end
57
+
58
+ def run_script(target, script, arguments, options = {})
59
+ # unpack any Sensitive data
60
+ arguments = unwrap_sensitive_args(arguments)
61
+
62
+ with_connection(target) do |conn|
63
+ conn.running_as(options['_run_as']) do
64
+ conn.with_tempdir do |dir|
65
+ path = conn.write_executable(dir.to_s, script)
66
+ dir.chown(conn.run_as)
67
+ output = conn.execute([path, *arguments], sudoable: true)
68
+ Bolt::Result.for_command(target,
69
+ output.stdout.string,
70
+ output.stderr.string,
71
+ output.exit_code,
72
+ 'script', script)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def run_task(target, task, arguments, options = {})
79
+ implementation = select_implementation(target, task)
80
+ executable = implementation['path']
81
+ input_method = implementation['input_method']
82
+ extra_files = implementation['files']
83
+
84
+ with_connection(target) do |conn|
85
+ conn.running_as(options['_run_as']) do
86
+ stdin, output = nil
87
+ command = []
88
+ execute_options = {}
89
+ execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
90
+ interpreter_debug = if execute_options[:interpreter]
91
+ " using '#{execute_options[:interpreter]}' interpreter"
92
+ end
93
+ # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
94
+ logger.debug("Running '#{executable}' with #{arguments}#{interpreter_debug}")
95
+ # unpack any Sensitive data
96
+ arguments = unwrap_sensitive_args(arguments)
97
+
98
+ conn.with_tempdir do |dir|
99
+ if extra_files.empty?
100
+ task_dir = dir
101
+ else
102
+ # TODO: optimize upload of directories
103
+ arguments['_installdir'] = dir.to_s
104
+ task_dir = File.join(dir.to_s, task.tasks_dir)
105
+ dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
106
+ extra_files.each do |file|
107
+ conn.copy_file(file['path'], File.join(dir.to_s, file['name']))
108
+ end
109
+ end
110
+
111
+ remote_task_path = conn.write_executable(task_dir, executable)
112
+
113
+ if STDIN_METHODS.include?(input_method)
114
+ stdin = JSON.dump(arguments)
115
+ end
116
+
117
+ if ENVIRONMENT_METHODS.include?(input_method)
118
+ execute_options[:environment] = envify_params(arguments)
119
+ end
120
+
121
+ if conn.run_as && stdin
122
+ # Inject interpreter in to wrapper script and remove from execute options
123
+ wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
124
+ execute_options.delete(:interpreter)
125
+ remote_wrapper_path = conn.write_executable(dir, wrapper, 'wrapper.sh')
126
+ command << remote_wrapper_path
127
+ else
128
+ command << remote_task_path
129
+ execute_options[:stdin] = stdin
130
+ end
131
+ dir.chown(conn.run_as)
132
+
133
+ execute_options[:sudoable] = true if conn.run_as
134
+ output = conn.execute(command, execute_options)
135
+ end
136
+ Bolt::Result.for_task(target, output.stdout.string,
137
+ output.stderr.string,
138
+ output.exit_code,
139
+ task.name)
140
+ end
141
+ end
142
+ end
143
+
144
+ def make_wrapper_stringio(task_path, stdin, interpreter = nil)
145
+ if interpreter
146
+ StringIO.new(<<~SCRIPT)
147
+ #!/bin/sh
148
+ '#{interpreter}' '#{task_path}' <<'EOF'
149
+ #{stdin}
150
+ EOF
151
+ SCRIPT
152
+ else
153
+ StringIO.new(<<~SCRIPT)
154
+ #!/bin/sh
155
+ '#{task_path}' <<'EOF'
156
+ #{stdin}
157
+ EOF
158
+ SCRIPT
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/transport/sudoable/tmpdir'
4
+
5
+ module Bolt
6
+ module Transport
7
+ class Sudoable < Base
8
+ class Connection
9
+ attr_accessor :target
10
+ def initialize(target)
11
+ @target = target
12
+ @run_as = nil
13
+ @logger = Logging.logger[@target.host]
14
+ end
15
+
16
+ # This method allows the @run_as variable to be used as a per-operation
17
+ # override for the user to run as. When @run_as is unset, the user
18
+ # specified on the target will be used.
19
+ def run_as
20
+ @run_as || target.options['run-as']
21
+ end
22
+
23
+ # Run as the specified user for the duration of the block.
24
+ def running_as(user)
25
+ @run_as = user
26
+ yield
27
+ ensure
28
+ @run_as = nil
29
+ end
30
+
31
+ def make_executable(path)
32
+ result = execute(['chmod', 'u+x', path])
33
+ if result.exit_code != 0
34
+ message = "Could not make file '#{path}' executable: #{result.stderr.string}"
35
+ raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
36
+ end
37
+ end
38
+
39
+ def make_tempdir
40
+ tmpdir = @target.options.fetch('tmpdir', '/tmp')
41
+ tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
42
+ command = ['mkdir', '-m', 700, tmppath]
43
+
44
+ result = execute(command)
45
+ if result.exit_code != 0
46
+ raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
47
+ end
48
+ path = tmppath || result.stdout.string.chomp
49
+ Sudoable::Tmpdir.new(self, path)
50
+ end
51
+
52
+ def write_executable(dir, file, filename = nil)
53
+ filename ||= File.basename(file)
54
+ remote_path = File.join(dir.to_s, filename)
55
+ copy_file(file, remote_path)
56
+ make_executable(remote_path)
57
+ remote_path
58
+ end
59
+
60
+ # A helper to create and delete a tempdir on the remote system. Yields the
61
+ # directory name.
62
+ def with_tempdir
63
+ dir = make_tempdir
64
+ yield dir
65
+ ensure
66
+ dir&.delete
67
+ end
68
+
69
+ def execute(*_args)
70
+ message = "#{self.class.name} must implement #{method} to execute commands"
71
+ raise NotImplementedError, message
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ module Transport
5
+ class Sudoable < Base
6
+ class Tmpdir
7
+ def initialize(node, path)
8
+ @conn = node
9
+ @owner = node.user
10
+ @path = path
11
+ @logger = node.logger
12
+ end
13
+
14
+ def to_s
15
+ @path
16
+ end
17
+
18
+ def mkdirs(subdirs)
19
+ abs_subdirs = subdirs.map { |subdir| File.join(@path, subdir) }
20
+ result = @conn.execute(['mkdir', '-p'] + abs_subdirs)
21
+ if result.exit_code != 0
22
+ message = "Could not create subdirectories in '#{@path}': #{result.stderr.string}"
23
+ raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
24
+ end
25
+ end
26
+
27
+ def chown(owner)
28
+ return if owner.nil? || owner == @owner
29
+
30
+ result = @conn.execute(['id', '-g', owner])
31
+ if result.exit_code != 0
32
+ message = "Could not identify group of user #{owner}: #{result.stderr.string}"
33
+ raise Bolt::Node::FileError.new(message, 'ID_ERROR')
34
+ end
35
+ group = result.stdout.string.chomp
36
+
37
+ # Chown can only be run by root.
38
+ result = @conn.execute(['chown', '-R', "#{owner}:#{group}", @path], sudoable: true, run_as: 'root')
39
+ if result.exit_code != 0
40
+ message = "Could not change owner of '#{@path}' to #{owner}: #{result.stderr.string}"
41
+ raise Bolt::Node::FileError.new(message, 'CHOWN_ERROR')
42
+ end
43
+
44
+ # File ownership successfully changed, record the new owner.
45
+ @owner = owner
46
+ end
47
+
48
+ def delete
49
+ result = @conn.execute(['rm', '-rf', @path], sudoable: true, run_as: @owner)
50
+ if result.exit_code != 0
51
+ @logger.warn("Failed to clean up tempdir '#{@path}': #{result.stderr.string}")
52
+ end
53
+ # For testing
54
+ result.stderr.string
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end