bolt 2.4.0 → 2.5.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 +3 -1
- data/lib/bolt/bolt_option_parser.rb +1 -1
- data/lib/bolt/executor.rb +3 -3
- data/lib/bolt/inventory/target.rb +0 -6
- data/lib/bolt/result.rb +3 -3
- data/lib/bolt/shell.rb +88 -0
- data/lib/bolt/shell/bash.rb +431 -0
- data/lib/bolt/{transport/sudoable → shell/bash}/tmpdir.rb +10 -10
- data/lib/bolt/target.rb +5 -4
- data/lib/bolt/task.rb +3 -0
- data/lib/bolt/transport/base.rb +6 -9
- data/lib/bolt/transport/docker.rb +2 -2
- data/lib/bolt/transport/local.rb +8 -11
- data/lib/bolt/transport/local/connection.rb +58 -0
- data/lib/bolt/transport/local_windows.rb +3 -3
- data/lib/bolt/transport/orch.rb +1 -1
- data/lib/bolt/transport/remote.rb +1 -1
- data/lib/bolt/transport/simple.rb +48 -0
- data/lib/bolt/transport/ssh.rb +2 -14
- data/lib/bolt/transport/ssh/connection.rb +66 -111
- data/lib/bolt/transport/winrm.rb +2 -2
- data/lib/bolt/util.rb +17 -1
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/bolt_context.rb +206 -0
- data/lib/bolt_spec/plans.rb +7 -94
- metadata +8 -6
- data/lib/bolt/transport/local/shell.rb +0 -216
- data/lib/bolt/transport/sudoable.rb +0 -150
- data/lib/bolt/transport/sudoable/connection.rb +0 -118
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Bolt
|
4
|
-
|
5
|
-
class
|
4
|
+
class Shell
|
5
|
+
class Bash < Shell
|
6
6
|
class Tmpdir
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
@owner =
|
7
|
+
def initialize(shell, path)
|
8
|
+
@shell = shell
|
9
|
+
@owner = shell.conn.user
|
10
10
|
@path = path
|
11
|
-
@logger =
|
11
|
+
@logger = shell.logger
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_s
|
@@ -17,7 +17,7 @@ module Bolt
|
|
17
17
|
|
18
18
|
def mkdirs(subdirs)
|
19
19
|
abs_subdirs = subdirs.map { |subdir| File.join(@path, subdir) }
|
20
|
-
result = @
|
20
|
+
result = @shell.execute(['mkdir', '-p'] + abs_subdirs)
|
21
21
|
if result.exit_code != 0
|
22
22
|
message = "Could not create subdirectories in '#{@path}': #{result.stderr.string}"
|
23
23
|
raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
|
@@ -27,7 +27,7 @@ module Bolt
|
|
27
27
|
def chown(owner)
|
28
28
|
return if owner.nil? || owner == @owner
|
29
29
|
|
30
|
-
result = @
|
30
|
+
result = @shell.execute(['id', '-g', owner])
|
31
31
|
if result.exit_code != 0
|
32
32
|
message = "Could not identify group of user #{owner}: #{result.stderr.string}"
|
33
33
|
raise Bolt::Node::FileError.new(message, 'ID_ERROR')
|
@@ -35,7 +35,7 @@ module Bolt
|
|
35
35
|
group = result.stdout.string.chomp
|
36
36
|
|
37
37
|
# Chown can only be run by root.
|
38
|
-
result = @
|
38
|
+
result = @shell.execute(['chown', '-R', "#{owner}:#{group}", @path], sudoable: true, run_as: 'root')
|
39
39
|
if result.exit_code != 0
|
40
40
|
message = "Could not change owner of '#{@path}' to #{owner}: #{result.stderr.string}"
|
41
41
|
raise Bolt::Node::FileError.new(message, 'CHOWN_ERROR')
|
@@ -46,7 +46,7 @@ module Bolt
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def delete
|
49
|
-
result = @
|
49
|
+
result = @shell.execute(['rm', '-rf', @path], sudoable: true, run_as: @owner)
|
50
50
|
if result.exit_code != 0
|
51
51
|
@logger.warn("Failed to clean up tempdir '#{@path}': #{result.stderr.string}")
|
52
52
|
end
|
data/lib/bolt/target.rb
CHANGED
@@ -129,6 +129,11 @@ module Bolt
|
|
129
129
|
inventory_target.transport
|
130
130
|
end
|
131
131
|
|
132
|
+
def transport_config
|
133
|
+
inventory_target.transport_config.to_h
|
134
|
+
end
|
135
|
+
alias options transport_config
|
136
|
+
|
132
137
|
def protocol
|
133
138
|
inventory_target.protocol || inventory_target.transport
|
134
139
|
end
|
@@ -141,10 +146,6 @@ module Bolt
|
|
141
146
|
inventory_target.password
|
142
147
|
end
|
143
148
|
|
144
|
-
def options
|
145
|
-
inventory_target.options
|
146
|
-
end
|
147
|
-
|
148
149
|
def plugin_hooks
|
149
150
|
inventory_target.plugin_hooks
|
150
151
|
end
|
data/lib/bolt/task.rb
CHANGED
@@ -9,6 +9,9 @@ module Bolt
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class Task
|
12
|
+
STDIN_METHODS = %w[both stdin].freeze
|
13
|
+
ENVIRONMENT_METHODS = %w[both environment].freeze
|
14
|
+
|
12
15
|
METADATA_KEYS = %w[description extensions files implementations
|
13
16
|
input_method parameters private puppet_task_version
|
14
17
|
remote supports_noop].freeze
|
data/lib/bolt/transport/base.rb
CHANGED
@@ -37,22 +37,19 @@ module Bolt
|
|
37
37
|
# before executing, and a :node_result event for each Target after
|
38
38
|
# execution.
|
39
39
|
class Base
|
40
|
-
STDIN_METHODS = %w[both stdin].freeze
|
41
|
-
ENVIRONMENT_METHODS = %w[both environment].freeze
|
42
|
-
|
43
40
|
attr_reader :logger
|
44
41
|
|
45
42
|
def initialize
|
46
43
|
@logger = Logging.logger[self]
|
47
44
|
end
|
48
45
|
|
49
|
-
def with_events(target, callback)
|
46
|
+
def with_events(target, callback, action)
|
50
47
|
callback&.call(type: :node_start, target: target)
|
51
48
|
|
52
49
|
result = begin
|
53
50
|
yield
|
54
51
|
rescue StandardError, NotImplementedError => e
|
55
|
-
Bolt::Result.from_exception(target, e)
|
52
|
+
Bolt::Result.from_exception(target, e, action: action)
|
56
53
|
end
|
57
54
|
|
58
55
|
callback&.call(type: :node_result, result: result)
|
@@ -106,7 +103,7 @@ module Bolt
|
|
106
103
|
def batch_task(targets, task, arguments, options = {}, &callback)
|
107
104
|
assert_batch_size_one("batch_task()", targets)
|
108
105
|
target = targets.first
|
109
|
-
with_events(target, callback) do
|
106
|
+
with_events(target, callback, 'task') do
|
110
107
|
@logger.debug { "Running task run '#{task}' on #{target.safe_name}" }
|
111
108
|
run_task(target, task, arguments, options)
|
112
109
|
end
|
@@ -120,7 +117,7 @@ module Bolt
|
|
120
117
|
def batch_command(targets, command, options = {}, &callback)
|
121
118
|
assert_batch_size_one("batch_command()", targets)
|
122
119
|
target = targets.first
|
123
|
-
with_events(target, callback) do
|
120
|
+
with_events(target, callback, 'command') do
|
124
121
|
@logger.debug("Running command '#{command}' on #{target.safe_name}")
|
125
122
|
run_command(target, command, options)
|
126
123
|
end
|
@@ -134,7 +131,7 @@ module Bolt
|
|
134
131
|
def batch_script(targets, script, arguments, options = {}, &callback)
|
135
132
|
assert_batch_size_one("batch_script()", targets)
|
136
133
|
target = targets.first
|
137
|
-
with_events(target, callback) do
|
134
|
+
with_events(target, callback, 'script') do
|
138
135
|
@logger.debug { "Running script '#{script}' on #{target.safe_name}" }
|
139
136
|
run_script(target, script, arguments, options)
|
140
137
|
end
|
@@ -148,7 +145,7 @@ module Bolt
|
|
148
145
|
def batch_upload(targets, source, destination, options = {}, &callback)
|
149
146
|
assert_batch_size_one("batch_upload()", targets)
|
150
147
|
target = targets.first
|
151
|
-
with_events(target, callback) do
|
148
|
+
with_events(target, callback, 'upload') do
|
152
149
|
@logger.debug { "Uploading: '#{source}' to #{destination} on #{target.safe_name}" }
|
153
150
|
upload(target, source, destination, options)
|
154
151
|
end
|
@@ -92,11 +92,11 @@ module Bolt
|
|
92
92
|
|
93
93
|
remote_task_path = conn.write_remote_executable(task_dir, executable)
|
94
94
|
|
95
|
-
if STDIN_METHODS.include?(input_method)
|
95
|
+
if Bolt::Task::STDIN_METHODS.include?(input_method)
|
96
96
|
execute_options[:stdin] = StringIO.new(JSON.dump(arguments))
|
97
97
|
end
|
98
98
|
|
99
|
-
if ENVIRONMENT_METHODS.include?(input_method)
|
99
|
+
if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
|
100
100
|
execute_options[:environment] = envify_params(arguments)
|
101
101
|
end
|
102
102
|
|
data/lib/bolt/transport/local.rb
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bolt/transport/simple'
|
4
|
+
|
3
5
|
module Bolt
|
4
6
|
module Transport
|
5
|
-
class Local <
|
6
|
-
def
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
def with_connection(target, *_args)
|
11
|
-
conn = Shell.new(target)
|
12
|
-
yield conn
|
7
|
+
class Local < Simple
|
8
|
+
def connected?(_target)
|
9
|
+
true
|
13
10
|
end
|
14
11
|
|
15
|
-
def
|
16
|
-
|
12
|
+
def with_connection(target)
|
13
|
+
yield Connection.new(target)
|
17
14
|
end
|
18
15
|
end
|
19
16
|
end
|
20
17
|
end
|
21
18
|
|
22
|
-
require 'bolt/transport/local/
|
19
|
+
require 'bolt/transport/local/connection'
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'bolt/node/output'
|
6
|
+
require 'bolt/util'
|
7
|
+
|
8
|
+
module Bolt
|
9
|
+
module Transport
|
10
|
+
class Local < Simple
|
11
|
+
class Connection
|
12
|
+
attr_accessor :user, :logger, :target
|
13
|
+
|
14
|
+
def initialize(target)
|
15
|
+
@target = target
|
16
|
+
# The familiar problem: Etc.getlogin is broken on osx
|
17
|
+
@user = ENV['USER'] || Etc.getlogin
|
18
|
+
@logger = Logging.logger[self]
|
19
|
+
end
|
20
|
+
|
21
|
+
def shell
|
22
|
+
@shell ||= if Bolt::Util.windows?
|
23
|
+
Bolt::Shell::Powershell.new(target, self)
|
24
|
+
else
|
25
|
+
Bolt::Shell::Bash.new(target, self)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy_file(source, dest)
|
30
|
+
@logger.debug { "Uploading #{source}, to #{dest}" }
|
31
|
+
if source.is_a?(StringIO)
|
32
|
+
Tempfile.create(File.basename(dest)) do |f|
|
33
|
+
f.write(source.read)
|
34
|
+
FileUtils.mv(t, dest)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
# Mimic the behavior of `cp --remove-destination`
|
38
|
+
# since the flag isn't supported on MacOS
|
39
|
+
FileUtils.cp_r(source, dest, remove_destination: true)
|
40
|
+
end
|
41
|
+
rescue StandardError => e
|
42
|
+
message = "Could not copy file to #{dest}: #{e}"
|
43
|
+
raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
|
44
|
+
end
|
45
|
+
|
46
|
+
def execute(command)
|
47
|
+
Open3.popen3(command)
|
48
|
+
end
|
49
|
+
|
50
|
+
# This is used by the Bash shell to decide whether to `cd` before
|
51
|
+
# executing commands as a run-as user
|
52
|
+
def reset_cwd?
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -141,8 +141,8 @@ module Bolt
|
|
141
141
|
logger.debug("Running '#{script}' with #{arguments.to_json}#{interpreter_debug}")
|
142
142
|
unwrapped_arguments = unwrap_sensitive_args(arguments)
|
143
143
|
|
144
|
-
stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
|
145
|
-
if ENVIRONMENT_METHODS.include?(input_method)
|
144
|
+
stdin = Bolt::Task::STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
|
145
|
+
if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
|
146
146
|
environment_params = envify_params(unwrapped_arguments).each_with_object([]) do |(arg, val), list|
|
147
147
|
list << Powershell.set_env(arg, val)
|
148
148
|
end
|
@@ -164,7 +164,7 @@ module Bolt
|
|
164
164
|
end
|
165
165
|
unless output
|
166
166
|
if interpreter
|
167
|
-
env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
|
167
|
+
env = Bolt::Task::ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
|
168
168
|
output = execute(script, stdin: stdin, env: env, dir: dir, interpreter: interpreter)
|
169
169
|
else
|
170
170
|
path, args = *Powershell.process_from_extension(script)
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -34,7 +34,7 @@ module Bolt
|
|
34
34
|
remote_task = task.remote_instance
|
35
35
|
|
36
36
|
result = transport.run_task(proxy_target, remote_task, arguments, options)
|
37
|
-
Bolt::Result.new(target, value: result.value)
|
37
|
+
Bolt::Result.new(target, value: result.value, action: 'task', object: task.name)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logging'
|
4
|
+
require 'bolt/result'
|
5
|
+
require 'bolt/shell'
|
6
|
+
require 'bolt/transport/base'
|
7
|
+
|
8
|
+
module Bolt
|
9
|
+
module Transport
|
10
|
+
# A simple transport has a single connection per target and delegates its
|
11
|
+
# operation to a target-specific shell.
|
12
|
+
class Simple < Base
|
13
|
+
def with_connection(_target)
|
14
|
+
raise NotImplementedError, "with_connection() must be implemented by the transport class"
|
15
|
+
end
|
16
|
+
|
17
|
+
def connected?(target)
|
18
|
+
with_connection(target) { true }
|
19
|
+
rescue Bolt::Node::ConnectError
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_command(target, command, options = {})
|
24
|
+
with_connection(target) do |conn|
|
25
|
+
conn.shell.run_command(command, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def upload(target, source, destination, options = {})
|
30
|
+
with_connection(target) do |conn|
|
31
|
+
conn.shell.upload(source, destination, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_script(target, script, arguments, options = {})
|
36
|
+
with_connection(target) do |conn|
|
37
|
+
conn.shell.run_script(script, arguments, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def run_task(target, task, arguments, options = {})
|
42
|
+
with_connection(target) do |conn|
|
43
|
+
conn.shell.run_task(task, arguments, options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -1,17 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bolt/node/errors'
|
4
|
-
require 'bolt/transport/
|
5
|
-
require 'json'
|
6
|
-
require 'shellwords'
|
4
|
+
require 'bolt/transport/simple'
|
7
5
|
|
8
6
|
module Bolt
|
9
7
|
module Transport
|
10
|
-
class SSH <
|
11
|
-
def provided_features
|
12
|
-
['shell']
|
13
|
-
end
|
14
|
-
|
8
|
+
class SSH < Simple
|
15
9
|
def initialize
|
16
10
|
super
|
17
11
|
|
@@ -38,12 +32,6 @@ module Bolt
|
|
38
32
|
logger.info("Failed to close connection to #{target.safe_name} : #{e.message}")
|
39
33
|
end
|
40
34
|
end
|
41
|
-
|
42
|
-
def connected?(target)
|
43
|
-
with_connection(target) { true }
|
44
|
-
rescue Bolt::Node::ConnectError
|
45
|
-
false
|
46
|
-
end
|
47
35
|
end
|
48
36
|
end
|
49
37
|
end
|
@@ -4,15 +4,13 @@ require 'logging'
|
|
4
4
|
require 'shellwords'
|
5
5
|
require 'bolt/node/errors'
|
6
6
|
require 'bolt/node/output'
|
7
|
-
require 'bolt/transport/sudoable/connection'
|
8
7
|
require 'bolt/util'
|
9
8
|
|
10
9
|
module Bolt
|
11
10
|
module Transport
|
12
|
-
class SSH <
|
13
|
-
class Connection
|
11
|
+
class SSH < Simple
|
12
|
+
class Connection
|
14
13
|
attr_reader :logger, :user, :target
|
15
|
-
attr_writer :run_as
|
16
14
|
|
17
15
|
def initialize(target, transport_logger)
|
18
16
|
# lazy-load expensive gem code
|
@@ -20,22 +18,18 @@ module Bolt
|
|
20
18
|
require 'net/ssh/proxy/jump'
|
21
19
|
|
22
20
|
raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
|
23
|
-
@sudo_id = SecureRandom.uuid
|
24
21
|
|
25
22
|
@target = target
|
26
23
|
@load_config = target.options['load-config']
|
27
24
|
|
28
25
|
ssh_config = @load_config ? Net::SSH::Config.for(target.host) : {}
|
29
26
|
@user = @target.user || ssh_config[:user] || Etc.getlogin
|
30
|
-
@run_as = nil
|
31
27
|
@strict_host_key_checking = ssh_config[:strict_host_key_checking]
|
32
28
|
|
33
29
|
@logger = Logging.logger[@target.safe_name]
|
34
30
|
@transport_logger = transport_logger
|
35
31
|
@logger.debug("Initializing ssh connection to #{@target.safe_name}")
|
36
32
|
|
37
|
-
@sudo_password = @target.options['sudo-password'] || @target.password
|
38
|
-
|
39
33
|
if target.options['private-key']&.instance_of?(String)
|
40
34
|
begin
|
41
35
|
Bolt::Util.validate_file('ssh key', target.options['private-key'])
|
@@ -148,119 +142,70 @@ module Bolt
|
|
148
142
|
end
|
149
143
|
end
|
150
144
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
#
|
159
|
-
channel.
|
160
|
-
raise Bolt::Node::EscalateError.new(
|
161
|
-
"Sudo password for user #{@user} was not provided for #{target.safe_name}",
|
162
|
-
'NO_PASSWORD'
|
163
|
-
)
|
164
|
-
end
|
165
|
-
elsif data =~ /^#{@sudo_id}/
|
166
|
-
if stdin
|
167
|
-
channel.send_data(stdin)
|
168
|
-
channel.eof!
|
169
|
-
end
|
170
|
-
return true
|
171
|
-
elsif data =~ /^#{@user} is not in the sudoers file\./
|
172
|
-
@logger.debug { data }
|
173
|
-
raise Bolt::Node::EscalateError.new(
|
174
|
-
"User #{@user} does not have sudo permission on #{target.safe_name}",
|
175
|
-
'SUDO_DENIED'
|
176
|
-
)
|
177
|
-
elsif data =~ /^Sorry, try again\./
|
178
|
-
@logger.debug { data }
|
179
|
-
raise Bolt::Node::EscalateError.new(
|
180
|
-
"Sudo password for user #{@user} not recognized on #{target.safe_name}",
|
181
|
-
'BAD_PASSWORD'
|
182
|
-
)
|
183
|
-
end
|
184
|
-
false
|
185
|
-
end
|
186
|
-
|
187
|
-
def execute(command, sudoable: false, **options)
|
188
|
-
result_output = Bolt::Node::Output.new
|
189
|
-
run_as = options[:run_as] || self.run_as
|
190
|
-
escalate = sudoable && run_as && @user != run_as
|
191
|
-
use_sudo = escalate && @target.options['run-as-command'].nil?
|
192
|
-
|
193
|
-
command_str = inject_interpreter(options[:interpreter], command)
|
194
|
-
if escalate
|
195
|
-
if use_sudo
|
196
|
-
sudo_exec = target.options['sudo-executable'] || "sudo"
|
197
|
-
sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", Sudoable.sudo_prompt]
|
198
|
-
sudo_flags += ["-E"] if options[:environment]
|
199
|
-
sudo_str = Shellwords.shelljoin(sudo_flags)
|
200
|
-
else
|
201
|
-
sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
|
202
|
-
end
|
203
|
-
command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options.merge(reset_cwd: true))
|
204
|
-
end
|
205
|
-
|
206
|
-
# Including the environment declarations in the shelljoin will escape
|
207
|
-
# the = sign, so we have to handle them separately.
|
208
|
-
if options[:environment]
|
209
|
-
env_decls = options[:environment].map do |env, val|
|
210
|
-
"#{env}=#{Shellwords.shellescape(val)}"
|
211
|
-
end
|
212
|
-
command_str = "#{env_decls.join(' ')} #{command_str}"
|
213
|
-
end
|
214
|
-
|
215
|
-
@logger.debug { "Executing: #{command_str}" }
|
216
|
-
|
217
|
-
session_channel = @session.open_channel do |channel|
|
218
|
-
# Request a pseudo tty
|
219
|
-
channel.request_pty if target.options['tty']
|
145
|
+
def execute(command_str)
|
146
|
+
in_rd, in_wr = IO.pipe
|
147
|
+
out_rd, out_wr = IO.pipe
|
148
|
+
err_rd, err_wr = IO.pipe
|
149
|
+
th = Thread.new do
|
150
|
+
exit_code = nil
|
151
|
+
session_channel = @session.open_channel do |channel|
|
152
|
+
# Request a pseudo tty
|
153
|
+
channel.request_pty if target.options['tty']
|
220
154
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
155
|
+
channel.exec(command_str) do |_, success|
|
156
|
+
unless success
|
157
|
+
raise Bolt::Node::ConnectError.new(
|
158
|
+
"Could not execute command: #{command_str.inspect}",
|
159
|
+
'EXEC_ERROR'
|
160
|
+
)
|
161
|
+
end
|
228
162
|
|
229
|
-
|
230
|
-
|
231
|
-
result_output.stdout << data
|
163
|
+
channel.on_data do |_, data|
|
164
|
+
out_wr << data
|
232
165
|
end
|
233
|
-
@logger.debug { "stdout: #{data.strip}" }
|
234
|
-
end
|
235
166
|
|
236
|
-
|
237
|
-
|
238
|
-
result_output.stderr << data
|
167
|
+
channel.on_extended_data do |_, _, data|
|
168
|
+
err_wr << data
|
239
169
|
end
|
240
|
-
@logger.debug { "stderr: #{data.strip}" }
|
241
|
-
end
|
242
170
|
|
243
|
-
|
244
|
-
|
171
|
+
channel.on_request("exit-status") do |_, data|
|
172
|
+
exit_code = data.read_long
|
173
|
+
end
|
245
174
|
end
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
175
|
+
end
|
176
|
+
write_th = Thread.new do
|
177
|
+
chunk_size = 4096
|
178
|
+
eof = false
|
179
|
+
active = true
|
180
|
+
readable = false
|
181
|
+
while active && !eof
|
182
|
+
@session.loop(0.1) do
|
183
|
+
active = session_channel.active?
|
184
|
+
readable = select([in_rd], [], [], 0)
|
185
|
+
# Loop as long as the channel is still live and there's nothing to be written
|
186
|
+
active && !readable
|
187
|
+
end
|
188
|
+
if readable
|
189
|
+
if in_rd.eof?
|
190
|
+
session_channel.eof!
|
191
|
+
eof = true
|
192
|
+
else
|
193
|
+
to_write = in_rd.readpartial(chunk_size)
|
194
|
+
session_channel.send_data(to_write)
|
195
|
+
end
|
196
|
+
end
|
250
197
|
end
|
198
|
+
session_channel.wait
|
251
199
|
end
|
200
|
+
write_th.join
|
201
|
+
exit_code
|
202
|
+
ensure
|
203
|
+
write_th.terminate
|
204
|
+
in_rd.close
|
205
|
+
out_wr.close
|
206
|
+
err_wr.close
|
252
207
|
end
|
253
|
-
|
254
|
-
|
255
|
-
if result_output.exit_code == 0
|
256
|
-
@logger.debug { "Command returned successfully" }
|
257
|
-
else
|
258
|
-
@logger.info { "Command failed with exit code #{result_output.exit_code}" }
|
259
|
-
end
|
260
|
-
result_output
|
261
|
-
rescue StandardError
|
262
|
-
@logger.debug { "Command aborted" }
|
263
|
-
raise
|
208
|
+
[in_wr, out_rd, err_rd, th]
|
264
209
|
end
|
265
210
|
|
266
211
|
def copy_file(source, destination)
|
@@ -295,6 +240,16 @@ module Bolt
|
|
295
240
|
end
|
296
241
|
end
|
297
242
|
end
|
243
|
+
|
244
|
+
def shell
|
245
|
+
@shell ||= Bolt::Shell::Bash.new(target, self)
|
246
|
+
end
|
247
|
+
|
248
|
+
# This is used by the Bash shell to decide whether to `cd` before
|
249
|
+
# executing commands as a run-as user
|
250
|
+
def reset_cwd?
|
251
|
+
true
|
252
|
+
end
|
298
253
|
end
|
299
254
|
end
|
300
255
|
end
|