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.
- checksums.yaml +4 -4
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +4 -4
- data/lib/bolt.rb +3 -0
- data/lib/bolt/analytics.rb +7 -2
- data/lib/bolt/applicator.rb +6 -2
- data/lib/bolt/bolt_option_parser.rb +4 -4
- data/lib/bolt/cli.rb +8 -4
- data/lib/bolt/config.rb +6 -6
- data/lib/bolt/executor.rb +2 -7
- data/lib/bolt/inventory.rb +37 -6
- data/lib/bolt/inventory/group2.rb +314 -0
- data/lib/bolt/inventory/inventory2.rb +261 -0
- data/lib/bolt/outputter/human.rb +3 -1
- data/lib/bolt/pal.rb +8 -7
- data/lib/bolt/puppetdb/client.rb +6 -5
- data/lib/bolt/target.rb +34 -14
- data/lib/bolt/task.rb +2 -2
- data/lib/bolt/transport/base.rb +2 -2
- data/lib/bolt/transport/docker.rb +1 -1
- data/lib/bolt/transport/docker/connection.rb +2 -0
- data/lib/bolt/transport/local.rb +9 -181
- data/lib/bolt/transport/local/shell.rb +202 -12
- data/lib/bolt/transport/local_windows.rb +203 -0
- data/lib/bolt/transport/orch.rb +6 -4
- data/lib/bolt/transport/orch/connection.rb +6 -2
- data/lib/bolt/transport/ssh.rb +10 -150
- data/lib/bolt/transport/ssh/connection.rb +15 -116
- data/lib/bolt/transport/sudoable.rb +163 -0
- data/lib/bolt/transport/sudoable/connection.rb +76 -0
- data/lib/bolt/transport/sudoable/tmpdir.rb +59 -0
- data/lib/bolt/transport/winrm.rb +4 -4
- data/lib/bolt/transport/winrm/connection.rb +1 -0
- data/lib/bolt/util.rb +2 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +0 -1
- data/lib/bolt_server/transport_app.rb +3 -1
- data/lib/logging_extensions/logging.rb +13 -0
- data/lib/plan_executor/orch_client.rb +4 -0
- 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 <
|
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
|
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 =
|
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
|
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
|