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
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'open3'
6
+ require 'tmpdir'
7
+ require 'bolt/node/output'
8
+ require 'bolt/transport/base'
9
+ require 'bolt/transport/powershell'
10
+ require 'bolt/util'
11
+
12
+ module Bolt
13
+ module Transport
14
+ class LocalWindows < Base
15
+ def self.options
16
+ %w[tmpdir interpreters run-as run-as-command sudo-password]
17
+ end
18
+
19
+ def provided_features
20
+ ['powershell']
21
+ end
22
+
23
+ def default_input_method(executable)
24
+ input_method ||= Powershell.powershell_file?(executable) ? 'powershell' : 'both'
25
+ input_method
26
+ end
27
+
28
+ def self.validate(options)
29
+ logger = Logging.logger[self]
30
+ if options['sudo-password'] || options['run-as'] || options['run-as-command'] || options['_run_as']
31
+ logger.warn("run-as is not supported for Windows hosts using the local transport")
32
+ end
33
+ end
34
+
35
+ def in_tmpdir(base)
36
+ args = base ? [nil, base] : []
37
+ dir = begin
38
+ Dir.mktmpdir(*args)
39
+ rescue StandardError => e
40
+ raise Bolt::Node::FileError.new("Could not make tempdir: #{e.message}", 'TEMPDIR_ERROR')
41
+ end
42
+
43
+ yield dir
44
+ ensure
45
+ FileUtils.remove_entry dir if dir
46
+ end
47
+ private :in_tmpdir
48
+
49
+ def copy_file(source, destination)
50
+ FileUtils.cp_r(source, destination, remove_destination: true)
51
+ rescue StandardError => e
52
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
53
+ end
54
+
55
+ def with_tmpscript(script, base)
56
+ in_tmpdir(base) do |dir|
57
+ dest = File.join(dir, File.basename(script))
58
+ copy_file(script, dest)
59
+ File.chmod(0o750, dest)
60
+ yield dest, dir
61
+ end
62
+ end
63
+ private :with_tmpscript
64
+
65
+ def execute(*command, options)
66
+ command.unshift(options[:interpreter]) if options[:interpreter]
67
+ command = [options[:env]] + command if options[:env]
68
+
69
+ if options[:stdin]
70
+ stdout, stderr, rc = Open3.capture3(*command, stdin_data: options[:stdin], chdir: options[:dir])
71
+ else
72
+ stdout, stderr, rc = Open3.capture3(*command, chdir: options[:dir])
73
+ end
74
+
75
+ result_output = Bolt::Node::Output.new
76
+ result_output.stdout << stdout unless stdout.nil?
77
+ result_output.stderr << stderr unless stderr.nil?
78
+ result_output.exit_code = rc.exitstatus
79
+ result_output
80
+ end
81
+
82
+ def upload(target, source, destination, options = {})
83
+ self.class.validate(options)
84
+ copy_file(source, destination)
85
+ Bolt::Result.for_upload(target, source, destination)
86
+ end
87
+
88
+ def run_command(target, command, options = {})
89
+ self.class.validate(options)
90
+ in_tmpdir(target.options['tmpdir']) do |dir|
91
+ output = execute(command, dir: dir)
92
+ Bolt::Result.for_command(target,
93
+ output.stdout.string,
94
+ output.stderr.string,
95
+ output.exit_code,
96
+ 'command', command)
97
+ end
98
+ end
99
+
100
+ def run_script(target, script, arguments, options = {})
101
+ self.class.validate(options)
102
+ with_tmpscript(File.absolute_path(script), target.options['tmpdir']) do |file, dir|
103
+ logger.debug "Running '#{file}' with #{arguments}"
104
+
105
+ # unpack any Sensitive data AFTER we log
106
+ arguments = unwrap_sensitive_args(arguments)
107
+ if Powershell.powershell_file?(file)
108
+ command = Powershell.run_script(arguments, file)
109
+ output = execute(command, dir: dir, env: "powershell.exe")
110
+ else
111
+ path, args = *Powershell.process_from_extension(file)
112
+ args += Powershell.escape_arguments(arguments)
113
+ command = args.unshift(path).join(' ')
114
+ output = execute(command, dir: dir)
115
+ end
116
+ Bolt::Result.for_command(target,
117
+ output.stdout.string,
118
+ output.stderr.string,
119
+ output.exit_code,
120
+ 'script', script)
121
+ end
122
+ end
123
+
124
+ def run_task(target, task, arguments, options = {})
125
+ self.class.validate(options)
126
+ implementation = select_implementation(target, task)
127
+ executable = implementation['path']
128
+ input_method = implementation['input_method']
129
+ extra_files = implementation['files']
130
+
131
+ in_tmpdir(target.options['tmpdir']) do |dir|
132
+ if extra_files.empty?
133
+ script = File.join(dir, File.basename(executable))
134
+ else
135
+ arguments['_installdir'] = dir
136
+ script_dest = File.join(dir, task.tasks_dir)
137
+ FileUtils.mkdir_p([script_dest] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
138
+
139
+ script = File.join(script_dest, File.basename(executable))
140
+ extra_files.each do |file|
141
+ dest = File.join(dir, file['name'])
142
+ copy_file(file['path'], dest)
143
+ File.chmod(0o750, dest)
144
+ end
145
+ end
146
+
147
+ copy_file(executable, script)
148
+ File.chmod(0o750, script)
149
+
150
+ interpreter = select_interpreter(script, target.options['interpreters'])
151
+ interpreter_debug = interpreter ? " using '#{interpreter}' interpreter" : nil
152
+ # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
153
+ logger.debug("Running '#{script}' with #{arguments}#{interpreter_debug}")
154
+ unwrapped_arguments = unwrap_sensitive_args(arguments)
155
+
156
+ stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
157
+ if ENVIRONMENT_METHODS.include?(input_method)
158
+ environment_params = envify_params(unwrapped_arguments).each_with_object([]) do |(arg, val), list|
159
+ list << Powershell.set_env(arg, val)
160
+ end
161
+ environment_params = environment_params.join("\n") + "\n"
162
+ else
163
+ environment_params = ""
164
+ end
165
+
166
+ if Powershell.powershell_file?(script) && stdin.nil?
167
+ command = Powershell.run_ps_task(arguments, script, input_method)
168
+ command = environment_params + Powershell.shell_init + command
169
+ interpreter ||= 'powershell.exe'
170
+ output =
171
+ if input_method == 'powershell'
172
+ execute(command, dir: dir, interpreter: interpreter)
173
+ else
174
+ execute(command, dir: dir, stdin: stdin, interpreter: interpreter)
175
+ end
176
+ end
177
+ unless output
178
+ if interpreter
179
+ env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
180
+ output = execute(script, stdin: stdin, env: env, dir: dir, interpreter: interpreter)
181
+ else
182
+ path, args = *Powershell.process_from_extension(script)
183
+ command = args.unshift(path).join(' ')
184
+ command = environment_params + Powershell.shell_init + command
185
+ output = execute(command, dir: dir, stdin: stdin, interpreter: 'powershell.exe')
186
+ end
187
+ end
188
+ Bolt::Result.for_task(target,
189
+ output.stdout.string,
190
+ output.stderr.string,
191
+ output.exit_code,
192
+ task.name)
193
+ end
194
+ end
195
+
196
+ def connected?(_targets)
197
+ true
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ require 'bolt/transport/local/shell'
@@ -3,9 +3,7 @@
3
3
  require 'base64'
4
4
  require 'find'
5
5
  require 'json'
6
- require 'minitar'
7
6
  require 'pathname'
8
- require 'zlib'
9
7
  require 'bolt/transport/base'
10
8
  require 'bolt/transport/orch/connection'
11
9
 
@@ -24,7 +22,7 @@ module Bolt
24
22
  attr_writer :plan_context
25
23
 
26
24
  def self.options
27
- %w[service-url cacert token-file task-environment]
25
+ %w[host service-url cacert token-file task-environment]
28
26
  end
29
27
 
30
28
  def self.default_options
@@ -68,7 +66,7 @@ module Bolt
68
66
  end
69
67
 
70
68
  def process_run_results(targets, results, task_name)
71
- targets_by_name = Hash[targets.map(&:host).zip(targets)]
69
+ targets_by_name = Hash[targets.map { |t| t.host || t.name }.zip(targets)]
72
70
  results.map do |node_result|
73
71
  target = targets_by_name[node_result['name']]
74
72
  state = node_result['state']
@@ -128,6 +126,10 @@ module Bolt
128
126
  end
129
127
 
130
128
  def pack(directory)
129
+ # lazy-load expensive gem code
130
+ require 'minitar'
131
+ require 'zlib'
132
+
131
133
  start_time = Time.now
132
134
  io = StringIO.new
133
135
  output = Minitar::Output.new(Zlib::GzipWriter.new(io))
@@ -58,13 +58,17 @@ module Bolt
58
58
  end
59
59
  end
60
60
 
61
+ def get_certnames(targets)
62
+ targets.map { |t| t.host || t.name }
63
+ end
64
+
61
65
  def build_request(targets, task, arguments, description = nil)
62
66
  body = { task: task.name,
63
67
  environment: @environment,
64
68
  noop: arguments['_noop'],
65
69
  params: arguments.reject { |k, _| k.start_with?('_') },
66
70
  scope: {
67
- nodes: targets.map(&:host)
71
+ nodes: get_certnames(targets)
68
72
  } }
69
73
  body[:description] = description if description
70
74
  body[:plan_job] = @plan_job if @plan_job
@@ -77,7 +81,7 @@ module Bolt
77
81
  end
78
82
 
79
83
  def query_inventory(targets)
80
- @client.post('inventory', nodes: targets.map(&:host))
84
+ @client.post('inventory', nodes: get_certnames(targets))
81
85
  end
82
86
  end
83
87
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/node/errors'
4
- require 'bolt/transport/base'
4
+ require 'bolt/transport/sudoable'
5
5
  require 'json'
6
6
  require 'shellwords'
7
7
 
8
8
  module Bolt
9
9
  module Transport
10
- class SSH < Base
10
+ class SSH < Sudoable
11
11
  def self.options
12
- %w[port user password sudo-password private-key host-key-check
12
+ %w[host port user password sudo-password private-key host-key-check
13
13
  connect-timeout tmpdir run-as tty run-as-command proxyjump interpreters]
14
14
  end
15
15
 
@@ -17,7 +17,8 @@ module Bolt
17
17
  {
18
18
  'connect-timeout' => 10,
19
19
  'host-key-check' => true,
20
- 'tty' => false
20
+ 'tty' => false,
21
+ 'load-config' => true
21
22
  }
22
23
  end
23
24
 
@@ -27,11 +28,7 @@ module Bolt
27
28
 
28
29
  def self.validate(options)
29
30
  logger = Logging.logger[self]
30
-
31
- if options['sudo-password'] && options['run-as'].nil?
32
- logger.warn("--sudo-password will not be used without specifying a " \
33
- "user to escalate to with --run-as")
34
- end
31
+ validate_sudo_options(options, logger)
35
32
 
36
33
  host_key = options['host-key-check']
37
34
  unless !!host_key == host_key
@@ -50,11 +47,6 @@ module Bolt
50
47
  error_msg = "connect-timeout value must be an Integer, received #{timeout_value}:#{timeout_value.class}"
51
48
  raise Bolt::ValidationError, error_msg
52
49
  end
53
-
54
- run_as_cmd = options['run-as-command']
55
- if run_as_cmd && (!run_as_cmd.is_a?(Array) || run_as_cmd.any? { |n| !n.is_a?(String) })
56
- raise Bolt::ValidationError, "run-as-command must be an Array of Strings, received #{run_as_cmd}"
57
- end
58
50
  end
59
51
 
60
52
  def initialize
@@ -72,147 +64,15 @@ module Bolt
72
64
  @transport_logger.level = :warn
73
65
  end
74
66
 
75
- def with_connection(target, load_config = true)
76
- conn = Connection.new(target, @transport_logger, load_config)
67
+ def with_connection(target)
68
+ conn = Connection.new(target, @transport_logger)
77
69
  conn.connect
78
70
  yield conn
79
71
  ensure
80
72
  begin
81
73
  conn&.disconnect
82
- rescue StandardError => ex
83
- logger.info("Failed to close connection to #{target.uri} : #{ex.message}")
84
- end
85
- end
86
-
87
- def upload(target, source, destination, options = {})
88
- with_connection(target) do |conn|
89
- conn.running_as(options['_run_as']) do
90
- conn.with_remote_tempdir do |dir|
91
- basename = File.basename(destination)
92
- tmpfile = "#{dir}/#{basename}"
93
- conn.write_remote_file(source, tmpfile)
94
- # pass over file ownership if we're using run-as to be a different user
95
- dir.chown(conn.run_as)
96
- result = conn.execute(['mv', tmpfile, destination], sudoable: true)
97
- if result.exit_code != 0
98
- message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}"
99
- raise Bolt::Node::FileError.new(message, 'MV_ERROR')
100
- end
101
- end
102
- Bolt::Result.for_upload(target, source, destination)
103
- end
104
- end
105
- end
106
-
107
- def run_command(target, command, options = {})
108
- with_connection(target) do |conn|
109
- conn.running_as(options['_run_as']) do
110
- output = conn.execute(command, sudoable: true)
111
- Bolt::Result.for_command(target,
112
- output.stdout.string,
113
- output.stderr.string,
114
- output.exit_code,
115
- 'command', command)
116
- end
117
- end
118
- end
119
-
120
- def run_script(target, script, arguments, options = {})
121
- # unpack any Sensitive data
122
- arguments = unwrap_sensitive_args(arguments)
123
-
124
- with_connection(target) do |conn|
125
- conn.running_as(options['_run_as']) do
126
- conn.with_remote_tempdir do |dir|
127
- remote_path = conn.write_remote_executable(dir, script)
128
- dir.chown(conn.run_as)
129
- output = conn.execute([remote_path, *arguments], sudoable: true)
130
- Bolt::Result.for_command(target,
131
- output.stdout.string,
132
- output.stderr.string,
133
- output.exit_code,
134
- 'script', script)
135
- end
136
- end
137
- end
138
- end
139
-
140
- def run_task(target, task, arguments, options = {})
141
- implementation = select_implementation(target, task)
142
- executable = implementation['path']
143
- input_method = implementation['input_method']
144
- extra_files = implementation['files']
145
-
146
- # unpack any Sensitive data
147
- arguments = unwrap_sensitive_args(arguments)
148
- with_connection(target, options.fetch('_load_config', true)) do |conn|
149
- conn.running_as(options['_run_as']) do
150
- stdin, output = nil
151
- command = []
152
- execute_options = {}
153
- execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
154
-
155
- conn.with_remote_tempdir do |dir|
156
- if extra_files.empty?
157
- task_dir = dir
158
- else
159
- # TODO: optimize upload of directories
160
- arguments['_installdir'] = dir.to_s
161
- task_dir = File.join(dir.to_s, task.tasks_dir)
162
- dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
163
- extra_files.each do |file|
164
- conn.write_remote_file(file['path'], File.join(dir.to_s, file['name']))
165
- end
166
- end
167
-
168
- remote_task_path = conn.write_remote_executable(task_dir, executable)
169
-
170
- if STDIN_METHODS.include?(input_method)
171
- stdin = JSON.dump(arguments)
172
- end
173
-
174
- if ENVIRONMENT_METHODS.include?(input_method)
175
- execute_options[:environment] = envify_params(arguments)
176
- end
177
-
178
- if conn.run_as && stdin
179
- # Inject interpreter in to wrapper script and remove from execute options
180
- wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
181
- execute_options.delete(:interpreter)
182
- remote_wrapper_path = conn.write_remote_executable(dir, wrapper, 'wrapper.sh')
183
- command << remote_wrapper_path
184
- else
185
- command << remote_task_path
186
- execute_options[:stdin] = stdin
187
- end
188
- dir.chown(conn.run_as)
189
-
190
- execute_options[:sudoable] = true if conn.run_as
191
- output = conn.execute(command, execute_options)
192
- end
193
- Bolt::Result.for_task(target, output.stdout.string,
194
- output.stderr.string,
195
- output.exit_code,
196
- task.name)
197
- end
198
- end
199
- end
200
-
201
- def make_wrapper_stringio(task_path, stdin, interpreter = nil)
202
- if interpreter
203
- StringIO.new(<<-SCRIPT)
204
- #!/bin/sh
205
- '#{interpreter}' '#{task_path}' <<'EOF'
206
- #{stdin}
207
- EOF
208
- SCRIPT
209
- else
210
- StringIO.new(<<-SCRIPT)
211
- #!/bin/sh
212
- '#{task_path}' <<'EOF'
213
- #{stdin}
214
- EOF
215
- SCRIPT
74
+ rescue StandardError => e
75
+ logger.info("Failed to close connection to #{target.uri} : #{e.message}")
216
76
  end
217
77
  end
218
78