bolt 0.16.1 → 0.16.2
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.rb +0 -1
- data/lib/bolt/cli.rb +12 -6
- data/lib/bolt/error.rb +8 -0
- data/lib/bolt/executor.rb +99 -113
- data/lib/bolt/inventory.rb +47 -2
- data/lib/bolt/inventory/group.rb +37 -6
- data/lib/bolt/pal.rb +28 -9
- data/lib/bolt/target.rb +0 -8
- data/lib/bolt/transport/base.rb +159 -0
- data/lib/bolt/transport/orch.rb +158 -0
- data/lib/bolt/transport/ssh.rb +135 -0
- data/lib/bolt/transport/ssh/connection.rb +278 -0
- data/lib/bolt/transport/winrm.rb +165 -0
- data/lib/bolt/transport/winrm/connection.rb +472 -0
- data/lib/bolt/version.rb +1 -1
- data/modules/boltlib/lib/puppet/datatypes/target.rb +5 -0
- data/modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -1
- data/modules/boltlib/lib/puppet/functions/file_upload.rb +1 -5
- data/modules/boltlib/lib/puppet/functions/get_targets.rb +41 -0
- data/modules/boltlib/lib/puppet/functions/run_command.rb +2 -6
- data/modules/boltlib/lib/puppet/functions/run_plan.rb +4 -1
- data/modules/boltlib/lib/puppet/functions/run_script.rb +2 -6
- data/modules/boltlib/lib/puppet/functions/run_task.rb +15 -17
- data/modules/boltlib/types/targetspec.pp +7 -0
- metadata +10 -6
- data/lib/bolt/node.rb +0 -76
- data/lib/bolt/node/orch.rb +0 -126
- data/lib/bolt/node/ssh.rb +0 -356
- data/lib/bolt/node/winrm.rb +0 -598
data/lib/bolt/node/ssh.rb
DELETED
@@ -1,356 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'shellwords'
|
3
|
-
require 'logging'
|
4
|
-
require 'net/ssh'
|
5
|
-
require 'net/scp'
|
6
|
-
require 'bolt/node/output'
|
7
|
-
|
8
|
-
module Bolt
|
9
|
-
class SSH < Node
|
10
|
-
class RemoteTempdir
|
11
|
-
def initialize(node, path)
|
12
|
-
@node = node
|
13
|
-
@owner = node.user
|
14
|
-
@path = path
|
15
|
-
@logger = node.logger
|
16
|
-
end
|
17
|
-
|
18
|
-
def to_s
|
19
|
-
@path
|
20
|
-
end
|
21
|
-
|
22
|
-
def chown(owner)
|
23
|
-
return if owner.nil? || owner == @owner
|
24
|
-
|
25
|
-
@owner = owner
|
26
|
-
result = @node.execute("chown -R '#{@owner}': '#{@path}'", sudoable: true, run_as: 'root')
|
27
|
-
if result.exit_code != 0
|
28
|
-
message = "Could not change owner of '#{@path}' to #{@owner}: #{result.stderr.string}"
|
29
|
-
raise Bolt::Node::FileError.new(message, 'CHOWN_ERROR')
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def delete
|
34
|
-
result = @node.execute("rm -rf '#{@path}'", sudoable: true, run_as: @owner)
|
35
|
-
if result.exit_code != 0
|
36
|
-
@logger.warn("Failed to clean up tempdir '#{@path}': #{result.stderr.string}")
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.initialize_transport(logger)
|
42
|
-
require 'net/ssh/krb'
|
43
|
-
rescue LoadError
|
44
|
-
logger.debug {
|
45
|
-
"Authentication method 'gssapi-with-mic' is not available"
|
46
|
-
}
|
47
|
-
end
|
48
|
-
|
49
|
-
def initialize(target)
|
50
|
-
super(target)
|
51
|
-
@user = @user || Net::SSH::Config.for(target.host)[:user] || Etc.getlogin
|
52
|
-
end
|
53
|
-
|
54
|
-
def protocol
|
55
|
-
'ssh'
|
56
|
-
end
|
57
|
-
|
58
|
-
if !!File::ALT_SEPARATOR
|
59
|
-
require 'ffi'
|
60
|
-
module Win
|
61
|
-
extend FFI::Library
|
62
|
-
ffi_lib 'user32'
|
63
|
-
ffi_convention :stdcall
|
64
|
-
attach_function :FindWindow, :FindWindowW, %i[buffer_in buffer_in], :int
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def connect
|
69
|
-
transport_logger = Logging.logger[Net::SSH]
|
70
|
-
transport_logger.level = :warn
|
71
|
-
options = {
|
72
|
-
logger: transport_logger,
|
73
|
-
non_interactive: true
|
74
|
-
}
|
75
|
-
|
76
|
-
options[:port] = @target.port if @target.port
|
77
|
-
options[:password] = @password if @password
|
78
|
-
options[:keys] = @key if @key
|
79
|
-
options[:verify_host_key] = if @host_key_check
|
80
|
-
Net::SSH::Verifiers::Secure.new
|
81
|
-
else
|
82
|
-
Net::SSH::Verifiers::Lenient.new
|
83
|
-
end
|
84
|
-
options[:timeout] = @connect_timeout if @connect_timeout
|
85
|
-
|
86
|
-
# Mirroring:
|
87
|
-
# https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/agent.rb#L80
|
88
|
-
# https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/pageant.rb#L403
|
89
|
-
if defined?(UNIXSocket) && UNIXSocket
|
90
|
-
if ENV['SSH_AUTH_SOCK'].to_s.empty?
|
91
|
-
@logger.debug { "Disabling use_agent in net-ssh: ssh-agent is not available" }
|
92
|
-
options[:use_agent] = false
|
93
|
-
end
|
94
|
-
elsif !!File::ALT_SEPARATOR
|
95
|
-
pageant_wide = 'Pageant'.encode('UTF-16LE')
|
96
|
-
if Win.FindWindow(pageant_wide, pageant_wide).to_i == 0
|
97
|
-
@logger.debug { "Disabling use_agent in net-ssh: pageant process not running" }
|
98
|
-
options[:use_agent] = false
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
@session = Net::SSH.start(@target.host, @user, options)
|
103
|
-
@logger.debug { "Opened session" }
|
104
|
-
rescue Net::SSH::AuthenticationFailed => e
|
105
|
-
raise Bolt::Node::ConnectError.new(
|
106
|
-
e.message,
|
107
|
-
'AUTH_ERROR'
|
108
|
-
)
|
109
|
-
rescue Net::SSH::HostKeyError => e
|
110
|
-
raise Bolt::Node::ConnectError.new(
|
111
|
-
"Host key verification failed for #{uri}: #{e.message}",
|
112
|
-
'HOST_KEY_ERROR'
|
113
|
-
)
|
114
|
-
rescue Net::SSH::ConnectionTimeout
|
115
|
-
raise Bolt::Node::ConnectError.new(
|
116
|
-
"Timeout after #{@connect_timeout} seconds connecting to #{uri}",
|
117
|
-
'CONNECT_ERROR'
|
118
|
-
)
|
119
|
-
rescue StandardError => e
|
120
|
-
raise Bolt::Node::ConnectError.new(
|
121
|
-
"Failed to connect to #{uri}: #{e.message}",
|
122
|
-
'CONNECT_ERROR'
|
123
|
-
)
|
124
|
-
end
|
125
|
-
|
126
|
-
def disconnect
|
127
|
-
if @session && !@session.closed?
|
128
|
-
@session.close
|
129
|
-
@logger.debug { "Closed session" }
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def sudo_prompt
|
134
|
-
'[sudo] Bolt needs to run as another user, password: '
|
135
|
-
end
|
136
|
-
|
137
|
-
def handled_sudo(channel, data)
|
138
|
-
if data == sudo_prompt
|
139
|
-
if @sudo_password
|
140
|
-
channel.send_data "#{@sudo_password}\n"
|
141
|
-
channel.wait
|
142
|
-
return true
|
143
|
-
else
|
144
|
-
raise Bolt::Node::EscalateError.new(
|
145
|
-
"Sudo password for user #{@user} was not provided for #{uri}",
|
146
|
-
'NO_PASSWORD'
|
147
|
-
)
|
148
|
-
end
|
149
|
-
elsif data =~ /^#{@user} is not in the sudoers file\./
|
150
|
-
@logger.debug { data }
|
151
|
-
raise Bolt::Node::EscalateError.new(
|
152
|
-
"User #{@user} does not have sudo permission on #{uri}",
|
153
|
-
'SUDO_DENIED'
|
154
|
-
)
|
155
|
-
elsif data =~ /^Sorry, try again\./
|
156
|
-
@logger.debug { data }
|
157
|
-
raise Bolt::Node::EscalateError.new(
|
158
|
-
"Sudo password for user #{@user} not recognized on #{uri}",
|
159
|
-
'BAD_PASSWORD'
|
160
|
-
)
|
161
|
-
end
|
162
|
-
false
|
163
|
-
end
|
164
|
-
|
165
|
-
def execute(command, sudoable: false, **options)
|
166
|
-
result_output = Bolt::Node::Output.new
|
167
|
-
run_as = options[:run_as] || @run_as
|
168
|
-
use_sudo = sudoable && run_as && @user != run_as
|
169
|
-
if use_sudo
|
170
|
-
command = "sudo -S -u #{run_as} -p '#{sudo_prompt}' #{command}"
|
171
|
-
end
|
172
|
-
|
173
|
-
@logger.debug { "Executing: #{command}" }
|
174
|
-
|
175
|
-
session_channel = @session.open_channel do |channel|
|
176
|
-
# Request a pseudo tty
|
177
|
-
channel.request_pty if @tty
|
178
|
-
|
179
|
-
channel.exec(command) do |_, success|
|
180
|
-
unless success
|
181
|
-
raise Bolt::Node::ConnectError.new(
|
182
|
-
"Could not execute command: #{command.inspect}",
|
183
|
-
'EXEC_ERROR'
|
184
|
-
)
|
185
|
-
end
|
186
|
-
|
187
|
-
channel.on_data do |_, data|
|
188
|
-
unless use_sudo && handled_sudo(channel, data)
|
189
|
-
result_output.stdout << data
|
190
|
-
end
|
191
|
-
@logger.debug { "stdout: #{data}" }
|
192
|
-
end
|
193
|
-
|
194
|
-
channel.on_extended_data do |_, _, data|
|
195
|
-
unless use_sudo && handled_sudo(channel, data)
|
196
|
-
result_output.stderr << data
|
197
|
-
end
|
198
|
-
@logger.debug { "stderr: #{data}" }
|
199
|
-
end
|
200
|
-
|
201
|
-
channel.on_request("exit-status") do |_, data|
|
202
|
-
result_output.exit_code = data.read_long
|
203
|
-
end
|
204
|
-
|
205
|
-
if options[:stdin]
|
206
|
-
channel.send_data(options[:stdin])
|
207
|
-
channel.eof!
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
session_channel.wait
|
212
|
-
|
213
|
-
if result_output.exit_code == 0
|
214
|
-
@logger.debug { "Command returned successfully" }
|
215
|
-
else
|
216
|
-
@logger.info { "Command failed with exit code #{result_output.exit_code}" }
|
217
|
-
end
|
218
|
-
result_output
|
219
|
-
end
|
220
|
-
|
221
|
-
def upload(source, destination, options = {})
|
222
|
-
@run_as = options['_run_as'] || @conf_run_as
|
223
|
-
with_remote_tempdir do |dir|
|
224
|
-
basename = File.basename(destination)
|
225
|
-
tmpfile = "#{dir}/#{basename}"
|
226
|
-
write_remote_file(source, tmpfile)
|
227
|
-
# pass over file ownership if we're using run-as to be a different user
|
228
|
-
dir.chown(@run_as)
|
229
|
-
result = execute("mv '#{tmpfile}' '#{destination}'", sudoable: true)
|
230
|
-
if result.exit_code != 0
|
231
|
-
message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}"
|
232
|
-
raise FileError.new(message, 'MV_ERROR')
|
233
|
-
end
|
234
|
-
end
|
235
|
-
Bolt::Result.for_upload(@target, source, destination)
|
236
|
-
ensure
|
237
|
-
@run_as = @conf_run_as
|
238
|
-
end
|
239
|
-
|
240
|
-
def write_remote_file(source, destination)
|
241
|
-
@session.scp.upload!(source, destination)
|
242
|
-
rescue StandardError => e
|
243
|
-
raise FileError.new(e.message, 'WRITE_ERROR')
|
244
|
-
end
|
245
|
-
|
246
|
-
def make_tempdir
|
247
|
-
if @tmpdir
|
248
|
-
tmppath = "#{@tmpdir}/#{SecureRandom.uuid}"
|
249
|
-
command = "mkdir -m 700 #{tmppath}"
|
250
|
-
else
|
251
|
-
command = 'mktemp -d'
|
252
|
-
end
|
253
|
-
result = execute(command)
|
254
|
-
if result.exit_code != 0
|
255
|
-
raise FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
|
256
|
-
end
|
257
|
-
path = tmppath || result.stdout.string.chomp
|
258
|
-
RemoteTempdir.new(self, path)
|
259
|
-
end
|
260
|
-
|
261
|
-
# A helper to create and delete a tempdir on the remote system. Yields the
|
262
|
-
# directory name.
|
263
|
-
def with_remote_tempdir
|
264
|
-
dir = make_tempdir
|
265
|
-
yield dir
|
266
|
-
ensure
|
267
|
-
dir.delete if dir
|
268
|
-
end
|
269
|
-
|
270
|
-
def write_remote_executable(dir, file, filename = nil)
|
271
|
-
filename ||= File.basename(file)
|
272
|
-
remote_path = "#{dir}/#{filename}"
|
273
|
-
write_remote_file(file, remote_path)
|
274
|
-
make_executable(remote_path)
|
275
|
-
remote_path
|
276
|
-
end
|
277
|
-
|
278
|
-
def make_executable(path)
|
279
|
-
result = execute("chmod u+x '#{path}'")
|
280
|
-
if result.exit_code != 0
|
281
|
-
raise FileError.new("Could not make file '#{path}' executable: #{result.stderr.string}", 'CHMOD_ERROR')
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
def make_wrapper_stringio(task_path, stdin)
|
286
|
-
StringIO.new(<<-SCRIPT)
|
287
|
-
#!/bin/sh
|
288
|
-
'#{task_path}' <<EOF
|
289
|
-
#{stdin}
|
290
|
-
EOF
|
291
|
-
SCRIPT
|
292
|
-
end
|
293
|
-
|
294
|
-
def run_command(command, options = {})
|
295
|
-
@run_as = options['_run_as'] || @conf_run_as
|
296
|
-
output = execute(command, sudoable: true)
|
297
|
-
Bolt::Result.for_command(@target, output.stdout.string, output.stderr.string, output.exit_code)
|
298
|
-
ensure
|
299
|
-
@run_as = @conf_run_as
|
300
|
-
end
|
301
|
-
|
302
|
-
def run_script(script, arguments, options = {})
|
303
|
-
@run_as = options['_run_as'] || @conf_run_as
|
304
|
-
with_remote_tempdir do |dir|
|
305
|
-
remote_path = write_remote_executable(dir, script)
|
306
|
-
dir.chown(@run_as)
|
307
|
-
output = execute("'#{remote_path}' #{Shellwords.join(arguments)}",
|
308
|
-
sudoable: true)
|
309
|
-
Bolt::Result.for_command(@target, output.stdout.string, output.stderr.string, output.exit_code)
|
310
|
-
end
|
311
|
-
ensure
|
312
|
-
@run_as = @conf_run_as
|
313
|
-
end
|
314
|
-
|
315
|
-
def run_task(task, input_method, arguments, options = {})
|
316
|
-
@run_as = options['_run_as'] || @conf_run_as
|
317
|
-
export_args = {}
|
318
|
-
stdin, output = nil
|
319
|
-
|
320
|
-
if STDIN_METHODS.include?(input_method)
|
321
|
-
stdin = JSON.dump(arguments)
|
322
|
-
end
|
323
|
-
|
324
|
-
if ENVIRONMENT_METHODS.include?(input_method)
|
325
|
-
export_args = arguments.map do |env, val|
|
326
|
-
"PT_#{env}='#{val}'"
|
327
|
-
end.join(' ')
|
328
|
-
end
|
329
|
-
|
330
|
-
command = export_args.empty? ? '' : "#{export_args} "
|
331
|
-
|
332
|
-
execute_options = {}
|
333
|
-
|
334
|
-
with_remote_tempdir do |dir|
|
335
|
-
remote_task_path = write_remote_executable(dir, task)
|
336
|
-
if @run_as && stdin
|
337
|
-
wrapper = make_wrapper_stringio(remote_task_path, stdin)
|
338
|
-
remote_wrapper_path = write_remote_executable(dir, wrapper, 'wrapper.sh')
|
339
|
-
command += "'#{remote_wrapper_path}'"
|
340
|
-
else
|
341
|
-
command += "'#{remote_task_path}'"
|
342
|
-
execute_options[:stdin] = stdin
|
343
|
-
end
|
344
|
-
dir.chown(@run_as)
|
345
|
-
|
346
|
-
execute_options[:sudoable] = true if @run_as
|
347
|
-
output = execute(command, **execute_options)
|
348
|
-
end
|
349
|
-
Bolt::Result.for_task(@target, output.stdout.string,
|
350
|
-
output.stderr.string,
|
351
|
-
output.exit_code)
|
352
|
-
ensure
|
353
|
-
@run_as = @conf_run_as
|
354
|
-
end
|
355
|
-
end
|
356
|
-
end
|
data/lib/bolt/node/winrm.rb
DELETED
@@ -1,598 +0,0 @@
|
|
1
|
-
require 'winrm'
|
2
|
-
require 'winrm-fs'
|
3
|
-
require 'logging'
|
4
|
-
require 'bolt/result'
|
5
|
-
require 'base64'
|
6
|
-
require 'set'
|
7
|
-
|
8
|
-
module Bolt
|
9
|
-
class WinRM < Node
|
10
|
-
def protocol
|
11
|
-
'winrm'
|
12
|
-
end
|
13
|
-
|
14
|
-
HTTP_PORT = 5985
|
15
|
-
HTTPS_PORT = 5986
|
16
|
-
|
17
|
-
def port
|
18
|
-
default_port = @ssl ? HTTPS_PORT : HTTP_PORT
|
19
|
-
@target.port || default_port
|
20
|
-
end
|
21
|
-
|
22
|
-
def initialize(target)
|
23
|
-
super(target)
|
24
|
-
@extensions = DEFAULT_EXTENSIONS.to_set.merge(@extensions || [])
|
25
|
-
@logger.debug { "WinRM initialized for #{@extensions.to_a} extensions" }
|
26
|
-
end
|
27
|
-
|
28
|
-
def connect
|
29
|
-
if @ssl
|
30
|
-
scheme = 'https'
|
31
|
-
transport = :ssl
|
32
|
-
else
|
33
|
-
scheme = 'http'
|
34
|
-
transport = :negotiate
|
35
|
-
end
|
36
|
-
endpoint = "#{scheme}://#{@target.host}:#{port}/wsman"
|
37
|
-
options = { endpoint: endpoint,
|
38
|
-
user: @user,
|
39
|
-
password: @password,
|
40
|
-
retry_limit: 1,
|
41
|
-
transport: transport,
|
42
|
-
ca_trust_path: @cacert }
|
43
|
-
|
44
|
-
Timeout.timeout(@connect_timeout) do
|
45
|
-
@connection = ::WinRM::Connection.new(options)
|
46
|
-
transport_logger = Logging.logger[::WinRM]
|
47
|
-
transport_logger.level = :warn
|
48
|
-
@connection.logger = transport_logger
|
49
|
-
|
50
|
-
@session = @connection.shell(:powershell)
|
51
|
-
@session.run('$PSVersionTable.PSVersion')
|
52
|
-
@logger.debug { "Opened session" }
|
53
|
-
end
|
54
|
-
rescue Timeout::Error
|
55
|
-
# If we're using the default port with SSL, a timeout probably means the
|
56
|
-
# host doesn't support SSL.
|
57
|
-
if @ssl && port == HTTPS_PORT
|
58
|
-
theres_your_problem = "\nUse --no-ssl if this host isn't configured to use SSL for WinRM"
|
59
|
-
end
|
60
|
-
raise Bolt::Node::ConnectError.new(
|
61
|
-
"Timeout after #{@connect_timeout} seconds connecting to #{endpoint}#{theres_your_problem}",
|
62
|
-
'CONNECT_ERROR'
|
63
|
-
)
|
64
|
-
rescue ::WinRM::WinRMAuthorizationError
|
65
|
-
raise Bolt::Node::ConnectError.new(
|
66
|
-
"Authentication failed for #{endpoint}",
|
67
|
-
'AUTH_ERROR'
|
68
|
-
)
|
69
|
-
rescue OpenSSL::SSL::SSLError => e
|
70
|
-
# If we're using SSL with the default non-SSL port, mention that as a likely problem
|
71
|
-
if @ssl && port == HTTP_PORT
|
72
|
-
theres_your_problem = "\nAre you using SSL to connect to a non-SSL port?"
|
73
|
-
end
|
74
|
-
raise Bolt::Node::ConnectError.new(
|
75
|
-
"Failed to connect to #{endpoint}: #{e.message}#{theres_your_problem}",
|
76
|
-
"CONNECT_ERROR"
|
77
|
-
)
|
78
|
-
rescue StandardError => e
|
79
|
-
raise Bolt::Node::ConnectError.new(
|
80
|
-
"Failed to connect to #{endpoint}: #{e.message}",
|
81
|
-
'CONNECT_ERROR'
|
82
|
-
)
|
83
|
-
end
|
84
|
-
|
85
|
-
def disconnect
|
86
|
-
@session.close if @session
|
87
|
-
@logger.debug { "Closed session" }
|
88
|
-
end
|
89
|
-
|
90
|
-
def shell_init
|
91
|
-
return nil if @shell_initialized
|
92
|
-
result = execute(<<-PS)
|
93
|
-
|
94
|
-
$ENV:PATH += ";${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\bin\\;" +
|
95
|
-
"${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\sys\\ruby\\bin\\"
|
96
|
-
$ENV:RUBYLIB = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\lib;" +
|
97
|
-
"${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\facter\\lib;" +
|
98
|
-
"${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\hiera\\lib;" +
|
99
|
-
$ENV:RUBYLIB
|
100
|
-
|
101
|
-
Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
|
102
|
-
$utf8 = [System.Text.Encoding]::UTF8
|
103
|
-
|
104
|
-
function Invoke-Interpreter
|
105
|
-
{
|
106
|
-
[CmdletBinding()]
|
107
|
-
Param (
|
108
|
-
[Parameter()]
|
109
|
-
[String]
|
110
|
-
$Path,
|
111
|
-
|
112
|
-
[Parameter()]
|
113
|
-
[String]
|
114
|
-
$Arguments,
|
115
|
-
|
116
|
-
[Parameter()]
|
117
|
-
[Int32]
|
118
|
-
$Timeout,
|
119
|
-
|
120
|
-
[Parameter()]
|
121
|
-
[String]
|
122
|
-
$StdinInput = $Null
|
123
|
-
)
|
124
|
-
|
125
|
-
try
|
126
|
-
{
|
127
|
-
if (-not (Get-Command $Path -ErrorAction SilentlyContinue))
|
128
|
-
{
|
129
|
-
throw "Could not find executable '$Path' in ${ENV:PATH} on target node"
|
130
|
-
}
|
131
|
-
|
132
|
-
$startInfo = New-Object System.Diagnostics.ProcessStartInfo($Path, $Arguments)
|
133
|
-
$startInfo.UseShellExecute = $false
|
134
|
-
$startInfo.WorkingDirectory = Split-Path -Parent (Get-Command $Path).Path
|
135
|
-
$startInfo.CreateNoWindow = $true
|
136
|
-
if ($StdinInput) { $startInfo.RedirectStandardInput = $true }
|
137
|
-
$startInfo.RedirectStandardOutput = $true
|
138
|
-
$startInfo.RedirectStandardError = $true
|
139
|
-
|
140
|
-
$stdoutHandler = { if (-not ([String]::IsNullOrEmpty($EventArgs.Data))) { $Host.UI.WriteLine($EventArgs.Data) } }
|
141
|
-
$stderrHandler = { if (-not ([String]::IsNullOrEmpty($EventArgs.Data))) { $Host.UI.WriteErrorLine($EventArgs.Data) } }
|
142
|
-
$invocationId = [Guid]::NewGuid().ToString()
|
143
|
-
|
144
|
-
$process = New-Object System.Diagnostics.Process
|
145
|
-
$process.StartInfo = $startInfo
|
146
|
-
$process.EnableRaisingEvents = $true
|
147
|
-
|
148
|
-
# https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standarderror(v=vs.110).aspx#Anchor_2
|
149
|
-
$stdoutEvent = Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -Action $stdoutHandler
|
150
|
-
$stderrEvent = Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -Action $stderrHandler
|
151
|
-
$exitedEvent = Register-ObjectEvent -InputObject $process -EventName 'Exited' -SourceIdentifier $invocationId
|
152
|
-
|
153
|
-
$process.Start() | Out-Null
|
154
|
-
|
155
|
-
$process.BeginOutputReadLine()
|
156
|
-
$process.BeginErrorReadLine()
|
157
|
-
|
158
|
-
if ($StdinInput)
|
159
|
-
{
|
160
|
-
$process.StandardInput.WriteLine($StdinInput)
|
161
|
-
$process.StandardInput.Close()
|
162
|
-
}
|
163
|
-
|
164
|
-
# park current thread until the PS event is signaled upon process exit
|
165
|
-
# OR the timeout has elapsed
|
166
|
-
$waitResult = Wait-Event -SourceIdentifier $invocationId -Timeout $Timeout
|
167
|
-
if (! $process.HasExited)
|
168
|
-
{
|
169
|
-
$Host.UI.WriteErrorLine("Process $Path did not complete in $Timeout seconds")
|
170
|
-
return 1
|
171
|
-
}
|
172
|
-
|
173
|
-
return $process.ExitCode
|
174
|
-
}
|
175
|
-
catch
|
176
|
-
{
|
177
|
-
$Host.UI.WriteErrorLine($_)
|
178
|
-
return 1
|
179
|
-
}
|
180
|
-
finally
|
181
|
-
{
|
182
|
-
@($stdoutEvent, $stderrEvent, $exitedEvent) |
|
183
|
-
? { $_ -ne $Null } |
|
184
|
-
% { Unregister-Event -SourceIdentifier $_.Name }
|
185
|
-
|
186
|
-
if ($process -ne $Null)
|
187
|
-
{
|
188
|
-
if (($process.Handle -ne $Null) -and (! $process.HasExited))
|
189
|
-
{
|
190
|
-
try { $process.Kill() } catch { $Host.UI.WriteErrorLine("Failed To Kill Process $Path") }
|
191
|
-
}
|
192
|
-
$process.Dispose()
|
193
|
-
}
|
194
|
-
}
|
195
|
-
}
|
196
|
-
|
197
|
-
function Write-Stream {
|
198
|
-
PARAM(
|
199
|
-
[Parameter(Position=0)] $stream,
|
200
|
-
[Parameter(ValueFromPipeline=$true)] $string
|
201
|
-
)
|
202
|
-
PROCESS {
|
203
|
-
$bytes = $utf8.GetBytes($string)
|
204
|
-
$stream.Write( $bytes, 0, $bytes.Length )
|
205
|
-
}
|
206
|
-
}
|
207
|
-
|
208
|
-
function Convert-JsonToXml {
|
209
|
-
PARAM([Parameter(ValueFromPipeline=$true)] [string[]] $json)
|
210
|
-
BEGIN {
|
211
|
-
$mStream = New-Object System.IO.MemoryStream
|
212
|
-
}
|
213
|
-
PROCESS {
|
214
|
-
$json | Write-Stream -Stream $mStream
|
215
|
-
}
|
216
|
-
END {
|
217
|
-
$mStream.Position = 0
|
218
|
-
try {
|
219
|
-
$jsonReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($mStream,[System.Xml.XmlDictionaryReaderQuotas]::Max)
|
220
|
-
$xml = New-Object Xml.XmlDocument
|
221
|
-
$xml.Load($jsonReader)
|
222
|
-
$xml
|
223
|
-
} finally {
|
224
|
-
$jsonReader.Close()
|
225
|
-
$mStream.Dispose()
|
226
|
-
}
|
227
|
-
}
|
228
|
-
}
|
229
|
-
|
230
|
-
Function ConvertFrom-Xml {
|
231
|
-
[CmdletBinding(DefaultParameterSetName="AutoType")]
|
232
|
-
PARAM(
|
233
|
-
[Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [Xml.XmlNode] $xml,
|
234
|
-
[Parameter(Mandatory=$true,ParameterSetName="ManualType")] [Type] $Type,
|
235
|
-
[Switch] $ForceType
|
236
|
-
)
|
237
|
-
PROCESS{
|
238
|
-
if (Get-Member -InputObject $xml -Name root) {
|
239
|
-
return $xml.root.Objects | ConvertFrom-Xml
|
240
|
-
} elseif (Get-Member -InputObject $xml -Name Objects) {
|
241
|
-
return $xml.Objects | ConvertFrom-Xml
|
242
|
-
}
|
243
|
-
$propbag = @{}
|
244
|
-
foreach ($name in Get-Member -InputObject $xml -MemberType Properties | Where-Object{$_.Name -notmatch "^__|type"} | Select-Object -ExpandProperty name) {
|
245
|
-
Write-Debug "$Name Type: $($xml.$Name.type)" -Debug:$false
|
246
|
-
$propbag."$Name" = Convert-Properties $xml."$name"
|
247
|
-
}
|
248
|
-
if (!$Type -and $xml.HasAttribute("__type")) { $Type = $xml.__Type }
|
249
|
-
if ($ForceType -and $Type) {
|
250
|
-
try {
|
251
|
-
$output = New-Object $Type -Property $propbag
|
252
|
-
} catch {
|
253
|
-
$output = New-Object PSObject -Property $propbag
|
254
|
-
$output.PsTypeNames.Insert(0, $xml.__type)
|
255
|
-
}
|
256
|
-
} elseif ($propbag.Count -ne 0) {
|
257
|
-
$output = New-Object PSObject -Property $propbag
|
258
|
-
if ($Type) {
|
259
|
-
$output.PsTypeNames.Insert(0, $Type)
|
260
|
-
}
|
261
|
-
}
|
262
|
-
return $output
|
263
|
-
}
|
264
|
-
}
|
265
|
-
|
266
|
-
Function Convert-Properties {
|
267
|
-
PARAM($InputObject)
|
268
|
-
switch ($InputObject.type) {
|
269
|
-
"object" {
|
270
|
-
return (ConvertFrom-Xml -Xml $InputObject)
|
271
|
-
}
|
272
|
-
"string" {
|
273
|
-
$MightBeADate = $InputObject.get_InnerText() -as [DateTime]
|
274
|
-
## Strings that are actually dates (*grumble* JSON is crap)
|
275
|
-
if ($MightBeADate -and $propbag."$Name" -eq $MightBeADate.ToString("G")) {
|
276
|
-
return $MightBeADate
|
277
|
-
} else {
|
278
|
-
return $InputObject.get_InnerText()
|
279
|
-
}
|
280
|
-
}
|
281
|
-
"number" {
|
282
|
-
$number = $InputObject.get_InnerText()
|
283
|
-
if ($number -eq ($number -as [int])) {
|
284
|
-
return $number -as [int]
|
285
|
-
} elseif ($number -eq ($number -as [double])) {
|
286
|
-
return $number -as [double]
|
287
|
-
} else {
|
288
|
-
return $number -as [decimal]
|
289
|
-
}
|
290
|
-
}
|
291
|
-
"boolean" {
|
292
|
-
return [bool]::parse($InputObject.get_InnerText())
|
293
|
-
}
|
294
|
-
"null" {
|
295
|
-
return $null
|
296
|
-
}
|
297
|
-
"array" {
|
298
|
-
[object[]]$Items = $(foreach( $item in $InputObject.GetEnumerator() ) {
|
299
|
-
Convert-Properties $item
|
300
|
-
})
|
301
|
-
return $Items
|
302
|
-
}
|
303
|
-
default {
|
304
|
-
return $InputObject
|
305
|
-
}
|
306
|
-
}
|
307
|
-
}
|
308
|
-
|
309
|
-
Function ConvertFrom-Json2 {
|
310
|
-
[CmdletBinding()]
|
311
|
-
PARAM(
|
312
|
-
[Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [string] $InputObject,
|
313
|
-
[Parameter(Mandatory=$true)] [Type] $Type,
|
314
|
-
[Switch] $ForceType
|
315
|
-
)
|
316
|
-
PROCESS {
|
317
|
-
$null = $PSBoundParameters.Remove("InputObject")
|
318
|
-
[Xml.XmlElement]$xml = (Convert-JsonToXml $InputObject).Root
|
319
|
-
if ($xml) {
|
320
|
-
if ($xml.Objects) {
|
321
|
-
$xml.Objects.Item.GetEnumerator() | ConvertFrom-Xml @PSBoundParameters
|
322
|
-
} elseif ($xml.Item -and $xml.Item -isnot [System.Management.Automation.PSParameterizedProperty]) {
|
323
|
-
$xml.Item | ConvertFrom-Xml @PSBoundParameters
|
324
|
-
} else {
|
325
|
-
$xml | ConvertFrom-Xml @PSBoundParameters
|
326
|
-
}
|
327
|
-
} else {
|
328
|
-
Write-Error "Failed to parse JSON with JsonReader" -Debug:$false
|
329
|
-
}
|
330
|
-
}
|
331
|
-
}
|
332
|
-
|
333
|
-
function ConvertFrom-PSCustomObject
|
334
|
-
{
|
335
|
-
PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
|
336
|
-
PROCESS {
|
337
|
-
if ($null -eq $InputObject) { return $null }
|
338
|
-
|
339
|
-
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
|
340
|
-
$collection = @(
|
341
|
-
foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
|
342
|
-
)
|
343
|
-
|
344
|
-
$collection
|
345
|
-
} elseif ($InputObject -is [System.Management.Automation.PSCustomObject]) {
|
346
|
-
$hash = @{}
|
347
|
-
foreach ($property in $InputObject.PSObject.Properties) {
|
348
|
-
$hash[$property.Name] = ConvertFrom-PSCustomObject $property.Value
|
349
|
-
}
|
350
|
-
|
351
|
-
$hash
|
352
|
-
} else {
|
353
|
-
$InputObject
|
354
|
-
}
|
355
|
-
}
|
356
|
-
}
|
357
|
-
|
358
|
-
function Get-ContentAsJson
|
359
|
-
{
|
360
|
-
[CmdletBinding()]
|
361
|
-
PARAM(
|
362
|
-
[Parameter(Mandatory = $true)] $Text,
|
363
|
-
[Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
|
364
|
-
)
|
365
|
-
|
366
|
-
# using polyfill cmdlet on PS2, so pass type info
|
367
|
-
if ($PSVersionTable.PSVersion -lt [Version]'3.0') {
|
368
|
-
$Text | ConvertFrom-Json2 -Type PSObject | ConvertFrom-PSCustomObject
|
369
|
-
} else {
|
370
|
-
$Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
|
371
|
-
}
|
372
|
-
}
|
373
|
-
PS
|
374
|
-
if result.exit_code != 0
|
375
|
-
raise BaseError.new("Could not initialize shell: #{result.stderr.string}", "SHELL_INIT_ERROR")
|
376
|
-
end
|
377
|
-
@shell_initialized = true
|
378
|
-
end
|
379
|
-
|
380
|
-
def execute(command, _ = {})
|
381
|
-
result_output = Bolt::Node::Output.new
|
382
|
-
|
383
|
-
@logger.debug { "Executing command: #{command}" }
|
384
|
-
|
385
|
-
output = @session.run(command) do |stdout, stderr|
|
386
|
-
result_output.stdout << stdout
|
387
|
-
@logger.debug { "stdout: #{stdout}" }
|
388
|
-
result_output.stderr << stderr
|
389
|
-
@logger.debug { "stderr: #{stderr}" }
|
390
|
-
end
|
391
|
-
result_output.exit_code = output.exitcode
|
392
|
-
if output.exitcode.zero?
|
393
|
-
@logger.debug { "Command returned successfully" }
|
394
|
-
else
|
395
|
-
@logger.info { "Command failed with exit code #{output.exitcode}" }
|
396
|
-
end
|
397
|
-
result_output
|
398
|
-
end
|
399
|
-
private :execute
|
400
|
-
|
401
|
-
# 10 minutes in seconds
|
402
|
-
DEFAULT_EXECUTION_TIMEOUT = 10 * 60
|
403
|
-
|
404
|
-
def execute_process(path = '', arguments = [], stdin = nil,
|
405
|
-
timeout = DEFAULT_EXECUTION_TIMEOUT)
|
406
|
-
quoted_args = arguments.map do |arg|
|
407
|
-
"'" + arg.gsub("'", "''") + "'"
|
408
|
-
end.join(',')
|
409
|
-
|
410
|
-
execute(<<-PS)
|
411
|
-
$quoted_array = @(
|
412
|
-
#{quoted_args}
|
413
|
-
)
|
414
|
-
|
415
|
-
$invokeArgs = @{
|
416
|
-
Path = "#{path}"
|
417
|
-
Arguments = $quoted_array -Join ' '
|
418
|
-
Timeout = #{timeout}
|
419
|
-
#{stdin.nil? ? '' : "StdinInput = @'\n" + stdin + "\n'@"}
|
420
|
-
}
|
421
|
-
|
422
|
-
# winrm gem checks $? prior to using $LASTEXITCODE
|
423
|
-
# making it necessary to exit with the desired code to propagate status properly
|
424
|
-
exit $(Invoke-Interpreter @invokeArgs)
|
425
|
-
PS
|
426
|
-
end
|
427
|
-
|
428
|
-
DEFAULT_EXTENSIONS = ['.ps1', '.rb', '.pp'].freeze
|
429
|
-
|
430
|
-
PS_ARGS = %w[
|
431
|
-
-NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass
|
432
|
-
].freeze
|
433
|
-
|
434
|
-
def powershell_file?(path)
|
435
|
-
Pathname(path).extname.casecmp('.ps1').zero?
|
436
|
-
end
|
437
|
-
|
438
|
-
def process_from_extension(path)
|
439
|
-
case Pathname(path).extname.downcase
|
440
|
-
when '.rb'
|
441
|
-
[
|
442
|
-
'ruby.exe',
|
443
|
-
['-S', "\"#{path}\""]
|
444
|
-
]
|
445
|
-
when '.ps1'
|
446
|
-
[
|
447
|
-
'powershell.exe',
|
448
|
-
[*PS_ARGS, '-File', "\"#{path}\""]
|
449
|
-
]
|
450
|
-
when '.pp'
|
451
|
-
[
|
452
|
-
'puppet.bat',
|
453
|
-
['apply', "\"#{path}\""]
|
454
|
-
]
|
455
|
-
else
|
456
|
-
# Run the script via cmd, letting Windows extension handling determine how
|
457
|
-
[
|
458
|
-
'cmd.exe',
|
459
|
-
['/c', "\"#{path}\""]
|
460
|
-
]
|
461
|
-
end
|
462
|
-
end
|
463
|
-
|
464
|
-
def upload(source, destination, _options = nil)
|
465
|
-
write_remote_file(source, destination)
|
466
|
-
Bolt::Result.for_upload(@target, source, destination)
|
467
|
-
end
|
468
|
-
|
469
|
-
def write_remote_file(source, destination)
|
470
|
-
fs = ::WinRM::FS::FileManager.new(@connection)
|
471
|
-
# TODO: raise FileError here if this fails
|
472
|
-
fs.upload(source, destination)
|
473
|
-
end
|
474
|
-
|
475
|
-
def make_tempdir
|
476
|
-
find_parent = @tmpdir ? "\"#{@tmpdir}\"" : '[System.IO.Path]::GetTempPath()'
|
477
|
-
result = execute(<<-PS)
|
478
|
-
$parent = #{find_parent}
|
479
|
-
$name = [System.IO.Path]::GetRandomFileName()
|
480
|
-
$path = Join-Path $parent $name
|
481
|
-
New-Item -ItemType Directory -Path $path | Out-Null
|
482
|
-
$path
|
483
|
-
PS
|
484
|
-
if result.exit_code != 0
|
485
|
-
raise FileError.new("Could not make tempdir: #{result.stderr}", 'TEMPDIR_ERROR')
|
486
|
-
end
|
487
|
-
result.stdout.string.chomp
|
488
|
-
end
|
489
|
-
|
490
|
-
def with_remote_file(file)
|
491
|
-
ext = File.extname(file)
|
492
|
-
unless @extensions.include?(ext)
|
493
|
-
raise FileError.new("File extension #{ext} is not enabled, "\
|
494
|
-
"to run it please add to 'winrm: extensions'", 'FILETYPE_ERROR')
|
495
|
-
end
|
496
|
-
file_base = File.basename(file)
|
497
|
-
dir = make_tempdir
|
498
|
-
dest = "#{dir}\\#{file_base}"
|
499
|
-
begin
|
500
|
-
write_remote_file(file, dest)
|
501
|
-
shell_init
|
502
|
-
yield dest
|
503
|
-
ensure
|
504
|
-
execute(<<-PS)
|
505
|
-
Remove-Item -Force "#{dest}"
|
506
|
-
Remove-Item -Force "#{dir}"
|
507
|
-
PS
|
508
|
-
end
|
509
|
-
end
|
510
|
-
|
511
|
-
def run_command(command, _options = nil)
|
512
|
-
output = execute(command)
|
513
|
-
Bolt::Result.for_command(@target, output.stdout.string, output.stderr.string, output.exit_code)
|
514
|
-
end
|
515
|
-
|
516
|
-
def run_script(script, arguments, _options = nil)
|
517
|
-
with_remote_file(script) do |remote_path|
|
518
|
-
if powershell_file?(remote_path)
|
519
|
-
mapped_args = arguments.map do |a|
|
520
|
-
"$invokeArgs.ArgumentList += @'\n#{a}\n'@"
|
521
|
-
end.join("\n")
|
522
|
-
output = execute(<<-PS)
|
523
|
-
$invokeArgs = @{
|
524
|
-
ScriptBlock = (Get-Command "#{remote_path}").ScriptBlock
|
525
|
-
ArgumentList = @()
|
526
|
-
}
|
527
|
-
#{mapped_args}
|
528
|
-
|
529
|
-
try
|
530
|
-
{
|
531
|
-
Invoke-Command @invokeArgs
|
532
|
-
}
|
533
|
-
catch
|
534
|
-
{
|
535
|
-
exit 1
|
536
|
-
}
|
537
|
-
PS
|
538
|
-
else
|
539
|
-
path, args = *process_from_extension(remote_path)
|
540
|
-
args += escape_arguments(arguments)
|
541
|
-
output = execute_process(path, args)
|
542
|
-
end
|
543
|
-
Bolt::Result.for_command(@target, output.stdout.string, output.stderr.string, output.exit_code)
|
544
|
-
end
|
545
|
-
end
|
546
|
-
|
547
|
-
def run_task(task, input_method, arguments, _options = nil)
|
548
|
-
if STDIN_METHODS.include?(input_method)
|
549
|
-
stdin = JSON.dump(arguments)
|
550
|
-
end
|
551
|
-
|
552
|
-
if ENVIRONMENT_METHODS.include?(input_method)
|
553
|
-
arguments.each do |(arg, val)|
|
554
|
-
cmd = "[Environment]::SetEnvironmentVariable('PT_#{arg}', '#{val}')"
|
555
|
-
result = execute(cmd)
|
556
|
-
if result.exit_code != 0
|
557
|
-
raise EnvironmentVarError(var, value)
|
558
|
-
end
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
with_remote_file(task) do |remote_path|
|
563
|
-
output =
|
564
|
-
if powershell_file?(remote_path) && stdin.nil?
|
565
|
-
# NOTE: cannot redirect STDIN to a .ps1 script inside of PowerShell
|
566
|
-
# must create new powershell.exe process like other interpreters
|
567
|
-
# fortunately, using PS with stdin input_method should never happen
|
568
|
-
if input_method == 'powershell'
|
569
|
-
execute(<<-PS)
|
570
|
-
$private:taskArgs = Get-ContentAsJson (
|
571
|
-
$utf8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
|
572
|
-
)
|
573
|
-
try { & "#{remote_path}" @taskArgs } catch { exit 1 }
|
574
|
-
PS
|
575
|
-
else
|
576
|
-
execute(%(try { & "#{remote_path}" } catch { exit 1 }))
|
577
|
-
end
|
578
|
-
else
|
579
|
-
path, args = *process_from_extension(remote_path)
|
580
|
-
execute_process(path, args, stdin)
|
581
|
-
end
|
582
|
-
Bolt::Result.for_task(@target, output.stdout.string,
|
583
|
-
output.stderr.string,
|
584
|
-
output.exit_code)
|
585
|
-
end
|
586
|
-
end
|
587
|
-
|
588
|
-
def escape_arguments(arguments)
|
589
|
-
arguments.map do |arg|
|
590
|
-
if arg =~ / /
|
591
|
-
"\"#{arg}\""
|
592
|
-
else
|
593
|
-
arg
|
594
|
-
end
|
595
|
-
end
|
596
|
-
end
|
597
|
-
end
|
598
|
-
end
|