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
@@ -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'
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -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
|
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
|
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
|
84
|
+
@client.post('inventory', nodes: get_certnames(targets))
|
81
85
|
end
|
82
86
|
end
|
83
87
|
end
|
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bolt/node/errors'
|
4
|
-
require 'bolt/transport/
|
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 <
|
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
|
76
|
-
conn = Connection.new(target, @transport_logger
|
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 =>
|
83
|
-
logger.info("Failed to close connection to #{target.uri} : #{
|
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
|
|