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.

@@ -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
@@ -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