bolt 1.29.0 → 1.29.1
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/lib/bolt/transport/docker.rb +3 -10
- data/lib/bolt/transport/docker/connection.rb +112 -22
- data/lib/bolt/transport/local/shell.rb +18 -19
- data/lib/bolt/transport/local_windows.rb +0 -2
- data/lib/bolt/transport/ssh/connection.rb +16 -13
- data/lib/bolt/transport/sudoable.rb +10 -11
- data/lib/bolt/transport/sudoable/connection.rb +33 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/schemas/action-run_command.json +12 -0
- data/lib/bolt_server/transport_app.rb +10 -2
- metadata +4 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 319eb2a2f42911eed9c4a7e9aa8e9bd351146f7ab3cdf69b34c9e364a2fbf574
|
4
|
+
data.tar.gz: ad22b5b96fc923b68d7109fdeba19f4a1046b327d0dcb0a1934bfc7a774425f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2e9e0277e1b7ee2cb7eb86120250e62a2ef91068893e7d86e7fb8ce542b3eaf016f0f7ca8a8245202a6c1c64a4a3d1af82c6e10fd826d5bec8df0f9f42c8d65
|
7
|
+
data.tar.gz: 64d7965e9e75abee048a0c251bcaaf2ce6f9a592ac86bc8f73fc330dba77ff847acaf4574ebdd194c6747d90b3d8496ae85c34658d3c0831c85befbe3a8b0aa9
|
@@ -8,7 +8,7 @@ module Bolt
|
|
8
8
|
module Transport
|
9
9
|
class Docker < Base
|
10
10
|
def self.options
|
11
|
-
%w[host service-url
|
11
|
+
%w[host service-url tmpdir interpreters shell-command tty]
|
12
12
|
end
|
13
13
|
|
14
14
|
def provided_features
|
@@ -21,12 +21,6 @@ module Bolt
|
|
21
21
|
raise Bolt::ValidationError, 'service-url must be a string'
|
22
22
|
end
|
23
23
|
end
|
24
|
-
|
25
|
-
if (opts = options['service-options'])
|
26
|
-
unless opts.instance_of?(Hash)
|
27
|
-
raise Bolt::ValidationError, 'service-options must be a hash'
|
28
|
-
end
|
29
|
-
end
|
30
24
|
end
|
31
25
|
|
32
26
|
def with_connection(target)
|
@@ -57,9 +51,8 @@ module Bolt
|
|
57
51
|
end
|
58
52
|
|
59
53
|
def run_command(target, command, options = {})
|
60
|
-
|
61
|
-
|
62
|
-
end
|
54
|
+
options[:tty] = target.options['tty']
|
55
|
+
|
63
56
|
if target.options['shell-command'] && !target.options['shell-command'].empty?
|
64
57
|
# escape any double quotes in command
|
65
58
|
command = command.gsub('"', '\"')
|
@@ -8,21 +8,24 @@ module Bolt
|
|
8
8
|
class Docker < Base
|
9
9
|
class Connection
|
10
10
|
def initialize(target)
|
11
|
-
# lazy-load expensive gem code
|
12
|
-
require 'docker'
|
13
|
-
|
14
11
|
raise Bolt::ValidationError, "Target #{target.name} does not have a host" unless target.host
|
15
|
-
|
16
12
|
@target = target
|
17
13
|
@logger = Logging.logger[target.host]
|
14
|
+
@docker_host = @target.options['service-url']
|
18
15
|
end
|
19
16
|
|
20
17
|
def connect
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
# We don't actually have a connection, but we do need to
|
19
|
+
# check that the container exists and is running.
|
20
|
+
output = execute_local_docker_json_command('ps')
|
21
|
+
index = output.find_index { |item| item["ID"] == @target.host || item["Names"] == @target.host }
|
22
|
+
raise "Could not find a container with name or ID matching '#{@target.host}'" if index.nil?
|
23
|
+
# Now find the indepth container information
|
24
|
+
output = execute_local_docker_json_command('inspect', [output[index]["ID"]])
|
25
|
+
# Store the container information for later
|
26
|
+
@container_info = output[0]
|
25
27
|
@logger.debug { "Opened session" }
|
28
|
+
true
|
26
29
|
rescue StandardError => e
|
27
30
|
raise Bolt::Node::ConnectError.new(
|
28
31
|
"Failed to connect to #{@target.uri}: #{e.message}",
|
@@ -30,38 +33,61 @@ module Bolt
|
|
30
33
|
)
|
31
34
|
end
|
32
35
|
|
36
|
+
# Executes a command inside the target container
|
37
|
+
#
|
38
|
+
# @param command [Array] The command to run, expressed as an array of strings
|
39
|
+
# @param options [Hash] command specific options
|
40
|
+
# @option opts [String] :interpreter statements that are prefixed to the command e.g `/bin/bash` or `cmd.exe /c`
|
41
|
+
# @option opts [Hash] :environment A hash of environment variables that will be injected into the command
|
42
|
+
# @option opts [IO] :stdin An IO object that will be used to redirect STDIN for the docker command
|
33
43
|
def execute(*command, options)
|
34
44
|
command.unshift(options[:interpreter]) if options[:interpreter]
|
45
|
+
# Build the `--env` parameters
|
46
|
+
envs = []
|
35
47
|
if options[:environment]
|
36
|
-
|
37
|
-
command = ['env'] + envs + command
|
48
|
+
options[:environment].each { |env, val| envs.concat(['--env', "#{env}=#{val}"]) }
|
38
49
|
end
|
39
50
|
|
40
|
-
|
41
|
-
|
42
|
-
|
51
|
+
command_options = []
|
52
|
+
# Need to be interactive if redirecting STDIN
|
53
|
+
command_options << '--interactive' unless options[:stdin].nil?
|
54
|
+
command_options << '--tty' if options[:tty]
|
55
|
+
command_options.concat(envs) unless envs.empty?
|
56
|
+
command_options << container_id
|
57
|
+
command_options.concat(command)
|
58
|
+
|
59
|
+
@logger.debug { "Executing: exec #{command_options}" }
|
60
|
+
|
61
|
+
stdout_str, stderr_str, status = execute_local_docker_command('exec', command_options, options[:stdin])
|
62
|
+
|
63
|
+
# The actual result is the exitstatus not the process object
|
64
|
+
status = status.nil? ? -32768 : status.exitstatus
|
65
|
+
if status == 0
|
43
66
|
@logger.debug { "Command returned successfully" }
|
44
67
|
else
|
45
|
-
@logger.info { "Command failed with exit code #{
|
68
|
+
@logger.info { "Command failed with exit code #{status}" }
|
46
69
|
end
|
47
|
-
|
48
|
-
|
49
|
-
|
70
|
+
stdout_str.force_encoding(Encoding::UTF_8)
|
71
|
+
stderr_str.force_encoding(Encoding::UTF_8)
|
72
|
+
# Normalise line endings
|
73
|
+
stdout_str.gsub!("\r\n", "\n")
|
74
|
+
stderr_str.gsub!("\r\n", "\n")
|
75
|
+
[stdout_str, stderr_str, status]
|
50
76
|
rescue StandardError
|
51
77
|
@logger.debug { "Command aborted" }
|
52
78
|
raise
|
53
79
|
end
|
54
80
|
|
55
81
|
def write_remote_file(source, destination)
|
56
|
-
|
82
|
+
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
83
|
+
raise "Error writing file to container #{@container_id}: #{stdout_str}" unless status.exitstatus.zero?
|
57
84
|
rescue StandardError => e
|
58
85
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
59
86
|
end
|
60
87
|
|
61
88
|
def write_remote_directory(source, destination)
|
62
|
-
|
63
|
-
|
64
|
-
@container.archive_in_stream(destination) { tar.read(Excon.defaults[:chunk_size]).to_s }
|
89
|
+
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
90
|
+
raise "Error writing directory to container #{@container_id}: #{stdout_str}" unless status.exitstatus.zero?
|
65
91
|
rescue StandardError => e
|
66
92
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
67
93
|
end
|
@@ -75,7 +101,7 @@ module Bolt
|
|
75
101
|
end
|
76
102
|
|
77
103
|
def make_tempdir
|
78
|
-
tmpdir = @target.options.fetch('tmpdir',
|
104
|
+
tmpdir = @target.options.fetch('tmpdir', container_tmpdir)
|
79
105
|
tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
|
80
106
|
|
81
107
|
stdout, stderr, exitcode = execute('mkdir', '-m', '700', tmppath, {})
|
@@ -112,6 +138,70 @@ module Bolt
|
|
112
138
|
raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
|
113
139
|
end
|
114
140
|
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Converts the JSON encoded STDOUT string from the docker cli into ruby objects
|
145
|
+
#
|
146
|
+
# @param stdout_string [String] The string to convert
|
147
|
+
# @return [Object] Ruby object representation of the JSON string
|
148
|
+
def extract_json(stdout_string)
|
149
|
+
# The output from the docker format command is a JSON string per line.
|
150
|
+
# We can't do a direct convert but this helper method will convert it into
|
151
|
+
# an array of Objects
|
152
|
+
stdout_string.split("\n")
|
153
|
+
.reject { |str| str.strip.empty? }
|
154
|
+
.map { |str| JSON.parse(str) }
|
155
|
+
end
|
156
|
+
|
157
|
+
# rubocop:disable Metrics/LineLength
|
158
|
+
# Executes a Docker CLI command
|
159
|
+
#
|
160
|
+
# @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
|
161
|
+
# @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
|
162
|
+
# @param redir_stdin [IO] IO object which will be use to as STDIN in the docker command. Default is nil, which does not perform redirection
|
163
|
+
# @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
|
164
|
+
# rubocop:enable Metrics/LineLength
|
165
|
+
def execute_local_docker_command(subcommand, command_options = [], redir_stdin = nil)
|
166
|
+
env_hash = {}
|
167
|
+
# Set the DOCKER_HOST if we are using a non-default service-url
|
168
|
+
env_hash['DOCKER_HOST'] = @docker_host unless @docker_host.nil?
|
169
|
+
|
170
|
+
command_options = [] if command_options.nil?
|
171
|
+
docker_command = [subcommand].concat(command_options)
|
172
|
+
|
173
|
+
# Always use binary mode for any text data
|
174
|
+
capture_options = { binmode: true }
|
175
|
+
capture_options[:stdin_data] = redir_stdin unless redir_stdin.nil?
|
176
|
+
stdout_str, stderr_str, status = Open3.capture3(env_hash, 'docker', *docker_command, capture_options)
|
177
|
+
[stdout_str, stderr_str, status]
|
178
|
+
end
|
179
|
+
|
180
|
+
# Executes a Docker CLI command and parses the output in JSON format
|
181
|
+
#
|
182
|
+
# @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
|
183
|
+
# @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
|
184
|
+
# @return [Object] Ruby object representation of the JSON string
|
185
|
+
def execute_local_docker_json_command(subcommand, command_options = [])
|
186
|
+
command_options = [] if command_options.nil?
|
187
|
+
command_options = ['--format', '{{json .}}'].concat(command_options)
|
188
|
+
stdout_str, _stderr_str, _status = execute_local_docker_command(subcommand, command_options)
|
189
|
+
extract_json(stdout_str)
|
190
|
+
end
|
191
|
+
|
192
|
+
# The full ID of the target container
|
193
|
+
#
|
194
|
+
# @return [String] The full ID of the target container
|
195
|
+
def container_id
|
196
|
+
@container_info["Id"]
|
197
|
+
end
|
198
|
+
|
199
|
+
# The temp path inside the target container
|
200
|
+
#
|
201
|
+
# @return [String] The absolute path to the temp directory
|
202
|
+
def container_tmpdir
|
203
|
+
'/tmp'
|
204
|
+
end
|
115
205
|
end
|
116
206
|
end
|
117
207
|
end
|
@@ -20,17 +20,18 @@ module Bolt
|
|
20
20
|
@user = ENV['USER'] || Etc.getlogin
|
21
21
|
@run_as = target.options['run-as']
|
22
22
|
@logger = Logging.logger[self]
|
23
|
+
@sudo_id = SecureRandom.uuid
|
23
24
|
end
|
24
25
|
|
25
26
|
# If prompted for sudo password, send password to stdin and return an
|
26
27
|
# empty string. Otherwise, check for sudo errors and raise Bolt error.
|
28
|
+
# If sudo_id is detected, that means the task needs to have stdin written.
|
27
29
|
# If error is not sudo-related, return the stderr string to be added to
|
28
30
|
# node output
|
29
|
-
def handle_sudo(stdin, err, pid)
|
31
|
+
def handle_sudo(stdin, err, pid, sudo_stdin)
|
30
32
|
if err.include?(Sudoable.sudo_prompt)
|
31
33
|
# A wild sudo prompt has appeared!
|
32
34
|
if @target.options['sudo-password']
|
33
|
-
# Hopefully no one's sudo-password is > 64kb
|
34
35
|
stdin.write("#{@target.options['sudo-password']}\n")
|
35
36
|
''
|
36
37
|
else
|
@@ -39,6 +40,12 @@ module Bolt
|
|
39
40
|
'NO_PASSWORD'
|
40
41
|
)
|
41
42
|
end
|
43
|
+
elsif err =~ /^#{@sudo_id}/
|
44
|
+
if sudo_stdin
|
45
|
+
stdin.write("#{sudo_stdin}\n")
|
46
|
+
stdin.close
|
47
|
+
end
|
48
|
+
''
|
42
49
|
else
|
43
50
|
handle_sudo_errors(err, pid)
|
44
51
|
end
|
@@ -96,12 +103,12 @@ module Bolt
|
|
96
103
|
|
97
104
|
# See if there's a sudo prompt in the output
|
98
105
|
# If not, return the output
|
99
|
-
def check_sudo(out, inp, pid)
|
106
|
+
def check_sudo(out, inp, pid, stdin)
|
100
107
|
buffer = out.readpartial(CHUNK_SIZE)
|
101
108
|
# Split on newlines, including the newline
|
102
109
|
lines = buffer.split(/(?<=[\n])/)
|
103
110
|
# handle_sudo will return the line if it is not a sudo prompt or error
|
104
|
-
lines.map! { |line| handle_sudo(inp, line, pid) }
|
111
|
+
lines.map! { |line| handle_sudo(inp, line, pid, stdin) }
|
105
112
|
lines.join("")
|
106
113
|
# If stream has reached EOF, no password prompt is expected
|
107
114
|
# return an empty string
|
@@ -114,33 +121,25 @@ module Bolt
|
|
114
121
|
escalate = sudoable && run_as && @user != run_as
|
115
122
|
use_sudo = escalate && @target.options['run-as-command'].nil?
|
116
123
|
|
117
|
-
|
118
|
-
if command.is_a?(Array)
|
119
|
-
command.unshift(options[:interpreter])
|
120
|
-
else
|
121
|
-
command = [options[:interpreter], command]
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
|
124
|
+
command_str = inject_interpreter(options[:interpreter], command)
|
126
125
|
|
127
126
|
if escalate
|
128
127
|
if use_sudo
|
129
128
|
sudo_flags = ["sudo", "-k", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
|
130
129
|
sudo_flags += ["-E"] if options[:environment]
|
131
130
|
sudo_str = Shellwords.shelljoin(sudo_flags)
|
132
|
-
command_str = "#{sudo_str} #{command_str}"
|
133
131
|
else
|
134
|
-
|
135
|
-
command_str = "#{run_as_str} #{command_str}"
|
132
|
+
sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
|
136
133
|
end
|
134
|
+
command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
|
137
135
|
end
|
138
136
|
|
139
137
|
command_arr = options[:environment].nil? ? [command_str] : [options[:environment], command_str]
|
140
138
|
|
141
139
|
# Prepare the variables!
|
142
140
|
result_output = Bolt::Node::Output.new
|
143
|
-
|
141
|
+
# Sudo handler will pass stdin if needed.
|
142
|
+
in_buffer = !use_sudo && options[:stdin] ? options[:stdin] : ''
|
144
143
|
# Chunks of this size will be read in one iteration
|
145
144
|
index = 0
|
146
145
|
timeout = 0.1
|
@@ -153,7 +152,7 @@ module Bolt
|
|
153
152
|
# See if there's a sudo prompt
|
154
153
|
if use_sudo
|
155
154
|
ready_read = select([err], nil, nil, timeout * 5)
|
156
|
-
read_streams[err] << check_sudo(err, inp, t.pid) if ready_read
|
155
|
+
read_streams[err] << check_sudo(err, inp, t.pid, options[:stdin]) if ready_read
|
157
156
|
end
|
158
157
|
|
159
158
|
# True while the process is running or waiting for IO input
|
@@ -166,7 +165,7 @@ module Bolt
|
|
166
165
|
begin
|
167
166
|
# Check for sudo prompt
|
168
167
|
read_streams[stream] << if use_sudo
|
169
|
-
check_sudo(stream, inp, t.pid)
|
168
|
+
check_sudo(stream, inp, t.pid, options[:stdin])
|
170
169
|
else
|
171
170
|
stream.readpartial(CHUNK_SIZE)
|
172
171
|
end
|
@@ -20,6 +20,7 @@ module Bolt
|
|
20
20
|
require 'net/ssh/proxy/jump'
|
21
21
|
|
22
22
|
raise Bolt::ValidationError, "Target #{target.name} does not have a host" unless target.host
|
23
|
+
@sudo_id = SecureRandom.uuid
|
23
24
|
|
24
25
|
@target = target
|
25
26
|
@load_config = target.options['load-config']
|
@@ -139,10 +140,10 @@ module Bolt
|
|
139
140
|
end
|
140
141
|
end
|
141
142
|
|
142
|
-
def handled_sudo(channel, data)
|
143
|
+
def handled_sudo(channel, data, stdin)
|
143
144
|
if data.lines.include?(Sudoable.sudo_prompt)
|
144
145
|
if target.options['sudo-password']
|
145
|
-
channel.send_data
|
146
|
+
channel.send_data("#{target.options['sudo-password']}\n")
|
146
147
|
channel.wait
|
147
148
|
return true
|
148
149
|
else
|
@@ -153,6 +154,12 @@ module Bolt
|
|
153
154
|
'NO_PASSWORD'
|
154
155
|
)
|
155
156
|
end
|
157
|
+
elsif data =~ /^#{@sudo_id}/
|
158
|
+
if stdin
|
159
|
+
channel.send_data(stdin)
|
160
|
+
channel.eof!
|
161
|
+
end
|
162
|
+
return true
|
156
163
|
elsif data =~ /^#{@user} is not in the sudoers file\./
|
157
164
|
@logger.debug { data }
|
158
165
|
raise Bolt::Node::EscalateError.new(
|
@@ -175,21 +182,17 @@ module Bolt
|
|
175
182
|
escalate = sudoable && run_as && @user != run_as
|
176
183
|
use_sudo = escalate && @target.options['run-as-command'].nil?
|
177
184
|
|
178
|
-
|
179
|
-
command.is_a?(Array) ? command.unshift(options[:interpreter]) : [options[:interpreter], command]
|
180
|
-
end
|
185
|
+
command_str = inject_interpreter(options[:interpreter], command)
|
181
186
|
|
182
|
-
command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
|
183
187
|
if escalate
|
184
188
|
if use_sudo
|
185
189
|
sudo_flags = ["sudo", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
|
186
190
|
sudo_flags += ["-E"] if options[:environment]
|
187
191
|
sudo_str = Shellwords.shelljoin(sudo_flags)
|
188
|
-
command_str = "#{sudo_str} #{command_str}"
|
189
192
|
else
|
190
|
-
|
191
|
-
command_str = "#{run_as_str} #{command_str}"
|
193
|
+
sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
|
192
194
|
end
|
195
|
+
command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
|
193
196
|
end
|
194
197
|
|
195
198
|
# Including the environment declarations in the shelljoin will escape
|
@@ -216,14 +219,14 @@ module Bolt
|
|
216
219
|
end
|
217
220
|
|
218
221
|
channel.on_data do |_, data|
|
219
|
-
unless use_sudo && handled_sudo(channel, data)
|
222
|
+
unless use_sudo && handled_sudo(channel, data, options[:stdin])
|
220
223
|
result_output.stdout << data
|
221
224
|
end
|
222
225
|
@logger.debug { "stdout: #{data.strip}" }
|
223
226
|
end
|
224
227
|
|
225
228
|
channel.on_extended_data do |_, _, data|
|
226
|
-
unless use_sudo && handled_sudo(channel, data)
|
229
|
+
unless use_sudo && handled_sudo(channel, data, options[:stdin])
|
227
230
|
result_output.stderr << data
|
228
231
|
end
|
229
232
|
@logger.debug { "stderr: #{data.strip}" }
|
@@ -232,8 +235,8 @@ module Bolt
|
|
232
235
|
channel.on_request("exit-status") do |_, data|
|
233
236
|
result_output.exit_code = data.read_long
|
234
237
|
end
|
235
|
-
|
236
|
-
if options[:stdin]
|
238
|
+
# A wrapper is used to direct stdin when elevating privilage or using tty
|
239
|
+
if options[:stdin] && !use_sudo && !options[:wrapper]
|
237
240
|
channel.send_data(options[:stdin])
|
238
241
|
channel.eof!
|
239
242
|
end
|
@@ -79,7 +79,6 @@ module Bolt
|
|
79
79
|
with_connection(target) do |conn|
|
80
80
|
conn.running_as(options['_run_as']) do
|
81
81
|
stdin, output = nil
|
82
|
-
command = []
|
83
82
|
execute_options = {}
|
84
83
|
execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
|
85
84
|
interpreter_debug = if execute_options[:interpreter]
|
@@ -103,8 +102,6 @@ module Bolt
|
|
103
102
|
end
|
104
103
|
end
|
105
104
|
|
106
|
-
remote_task_path = conn.write_executable(task_dir, executable)
|
107
|
-
|
108
105
|
if STDIN_METHODS.include?(input_method)
|
109
106
|
stdin = JSON.dump(arguments)
|
110
107
|
end
|
@@ -113,20 +110,22 @@ module Bolt
|
|
113
110
|
execute_options[:environment] = envify_params(arguments)
|
114
111
|
end
|
115
112
|
|
116
|
-
|
117
|
-
|
113
|
+
remote_task_path = conn.write_executable(task_dir, executable)
|
114
|
+
|
115
|
+
# Avoid the horrors of passing data on stdin via a tty on multiple platforms
|
116
|
+
# by writing a wrapper script that directs stdin to the task.
|
117
|
+
if stdin && target.options['tty']
|
118
118
|
wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
|
119
119
|
execute_options.delete(:interpreter)
|
120
|
-
|
121
|
-
|
122
|
-
else
|
123
|
-
command << remote_task_path
|
124
|
-
execute_options[:stdin] = stdin
|
120
|
+
execute_options[:wrapper] = true
|
121
|
+
remote_task_path = conn.write_executable(dir, wrapper, 'wrapper.sh')
|
125
122
|
end
|
123
|
+
|
126
124
|
dir.chown(conn.run_as)
|
127
125
|
|
126
|
+
execute_options[:stdin] = stdin
|
128
127
|
execute_options[:sudoable] = true if conn.run_as
|
129
|
-
output = conn.execute(
|
128
|
+
output = conn.execute(remote_task_path, execute_options)
|
130
129
|
end
|
131
130
|
Bolt::Result.for_task(target, output.stdout.string,
|
132
131
|
output.stderr.string,
|
@@ -70,6 +70,39 @@ module Bolt
|
|
70
70
|
message = "#{self.class.name} must implement #{method} to execute commands"
|
71
71
|
raise NotImplementedError, message
|
72
72
|
end
|
73
|
+
|
74
|
+
# In the case where a task is run with elevated privilege and needs stdin
|
75
|
+
# a random string is echoed to stderr indicating that the stdin is available
|
76
|
+
# for task input data because the sudo password has already either been
|
77
|
+
# provided on stdin or was not needed.
|
78
|
+
def prepend_sudo_success(sudo_id, command_str)
|
79
|
+
"sh -c 'echo #{sudo_id} 1>&2; #{command_str}'"
|
80
|
+
end
|
81
|
+
|
82
|
+
# A helper to build up a single string that contains all of the options for
|
83
|
+
# privilege escalation. A wrapper script is used to direct task input to stdin
|
84
|
+
# when a tty is allocated and thus we do not need to prepend_sudo_success when
|
85
|
+
# using the wrapper or when the task does not require stdin data.
|
86
|
+
def build_sudoable_command_str(command_str, sudo_str, sudo_id, options)
|
87
|
+
if options[:stdin] && !options[:wrapper]
|
88
|
+
"#{sudo_str} #{prepend_sudo_success(sudo_id, command_str)}"
|
89
|
+
else
|
90
|
+
"#{sudo_str} #{command_str}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns string with the interpreter conditionally prepended
|
95
|
+
def inject_interpreter(interpreter, command)
|
96
|
+
if interpreter
|
97
|
+
if command.is_a?(Array)
|
98
|
+
command.unshift(interpreter)
|
99
|
+
else
|
100
|
+
command = [interpreter, command]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
command.is_a?(String) ? command : Shellwords.shelljoin(command)
|
105
|
+
end
|
73
106
|
end
|
74
107
|
end
|
75
108
|
end
|
data/lib/bolt/version.rb
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
3
|
+
"title": "run_command request",
|
4
|
+
"description": "POST <transport>/run_command request schema",
|
5
|
+
"type": "object",
|
6
|
+
"properties": {
|
7
|
+
"command": { "type": "string" },
|
8
|
+
"target": { "$ref": "partial:target-any" }
|
9
|
+
},
|
10
|
+
"required": ["command", "target"],
|
11
|
+
"additionalProperties": false
|
12
|
+
}
|
@@ -19,7 +19,7 @@ module BoltServer
|
|
19
19
|
PARTIAL_SCHEMAS = %w[target-any target-ssh target-winrm task].freeze
|
20
20
|
|
21
21
|
# These schemas combine shared schemas to describe client requests
|
22
|
-
REQUEST_SCHEMAS = %w[action-run_task transport-ssh transport-winrm].freeze
|
22
|
+
REQUEST_SCHEMAS = %w[action-run_task action-run_command transport-ssh transport-winrm].freeze
|
23
23
|
|
24
24
|
def initialize(config)
|
25
25
|
@config = config
|
@@ -65,6 +65,14 @@ module BoltServer
|
|
65
65
|
@executor.run_task(target, task, parameters)
|
66
66
|
end
|
67
67
|
|
68
|
+
def run_command(target, body)
|
69
|
+
error = validate_schema(@schemas["action-run_command"], body)
|
70
|
+
return [400, error.to_json] unless error.nil?
|
71
|
+
|
72
|
+
command = body['command']
|
73
|
+
@executor.run_command(target, command)
|
74
|
+
end
|
75
|
+
|
68
76
|
get '/' do
|
69
77
|
200
|
70
78
|
end
|
@@ -84,7 +92,7 @@ module BoltServer
|
|
84
92
|
raise 'Unexpected error'
|
85
93
|
end
|
86
94
|
|
87
|
-
ACTIONS = %w[run_task].freeze
|
95
|
+
ACTIONS = %w[run_task run_command].freeze
|
88
96
|
|
89
97
|
post '/ssh/:action' do
|
90
98
|
not_found unless ACTIONS.include?(params[:action])
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bolt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.29.
|
4
|
+
version: 1.29.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-08-
|
11
|
+
date: 2019-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -66,20 +66,6 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: docker-api
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '1.34'
|
76
|
-
type: :runtime
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '1.34'
|
83
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: hiera-eyaml
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -454,6 +440,7 @@ files:
|
|
454
440
|
- lib/bolt_server/base_config.rb
|
455
441
|
- lib/bolt_server/config.rb
|
456
442
|
- lib/bolt_server/file_cache.rb
|
443
|
+
- lib/bolt_server/schemas/action-run_command.json
|
457
444
|
- lib/bolt_server/schemas/action-run_task.json
|
458
445
|
- lib/bolt_server/schemas/partials/target-any.json
|
459
446
|
- lib/bolt_server/schemas/partials/target-ssh.json
|
@@ -509,8 +496,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
509
496
|
- !ruby/object:Gem::Version
|
510
497
|
version: '0'
|
511
498
|
requirements: []
|
512
|
-
|
513
|
-
rubygems_version: 2.7.7
|
499
|
+
rubygems_version: 3.0.4
|
514
500
|
signing_key:
|
515
501
|
specification_version: 4
|
516
502
|
summary: Execute commands remotely over SSH and WinRM
|