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.

@@ -0,0 +1,472 @@
1
+ require 'bolt/node/errors'
2
+ require 'bolt/node/output'
3
+
4
+ module Bolt
5
+ module Transport
6
+ class WinRM < Base
7
+ class Connection
8
+ attr_reader :logger, :target
9
+
10
+ def initialize(target)
11
+ @target = target
12
+
13
+ default_port = target.options[:ssl] ? HTTPS_PORT : HTTP_PORT
14
+ @port = @target.port || default_port
15
+ @user = @target.user
16
+ @extensions = DEFAULT_EXTENSIONS.to_set.merge(target.options[:extensions] || [])
17
+
18
+ @logger = Logging.logger[@target.host]
19
+ end
20
+
21
+ HTTP_PORT = 5985
22
+ HTTPS_PORT = 5986
23
+
24
+ def connect
25
+ if target.options[:ssl]
26
+ scheme = 'https'
27
+ transport = :ssl
28
+ else
29
+ scheme = 'http'
30
+ transport = :negotiate
31
+ end
32
+ endpoint = "#{scheme}://#{target.host}:#{@port}/wsman"
33
+ options = { endpoint: endpoint,
34
+ user: @user,
35
+ password: target.password,
36
+ retry_limit: 1,
37
+ transport: transport,
38
+ ca_trust_path: target.options[:cacert] }
39
+
40
+ Timeout.timeout(target.options[:connect_timeout]) do
41
+ @connection = ::WinRM::Connection.new(options)
42
+ transport_logger = Logging.logger[::WinRM]
43
+ transport_logger.level = :warn
44
+ @connection.logger = transport_logger
45
+
46
+ @session = @connection.shell(:powershell)
47
+ @session.run('$PSVersionTable.PSVersion')
48
+ @logger.debug { "Opened session" }
49
+ end
50
+ rescue Timeout::Error
51
+ # If we're using the default port with SSL, a timeout probably means the
52
+ # host doesn't support SSL.
53
+ if target.options[:ssl] && @port == HTTPS_PORT
54
+ theres_your_problem = "\nUse --no-ssl if this host isn't configured to use SSL for WinRM"
55
+ end
56
+ raise Bolt::Node::ConnectError.new(
57
+ "Timeout after #{target.options[:connect_timeout]} seconds connecting to #{endpoint}#{theres_your_problem}",
58
+ 'CONNECT_ERROR'
59
+ )
60
+ rescue ::WinRM::WinRMAuthorizationError
61
+ raise Bolt::Node::ConnectError.new(
62
+ "Authentication failed for #{endpoint}",
63
+ 'AUTH_ERROR'
64
+ )
65
+ rescue OpenSSL::SSL::SSLError => e
66
+ # If we're using SSL with the default non-SSL port, mention that as a likely problem
67
+ if target.options[:ssl] && @port == HTTP_PORT
68
+ theres_your_problem = "\nAre you using SSL to connect to a non-SSL port?"
69
+ end
70
+ raise Bolt::Node::ConnectError.new(
71
+ "Failed to connect to #{endpoint}: #{e.message}#{theres_your_problem}",
72
+ "CONNECT_ERROR"
73
+ )
74
+ rescue StandardError => e
75
+ raise Bolt::Node::ConnectError.new(
76
+ "Failed to connect to #{endpoint}: #{e.message}",
77
+ 'CONNECT_ERROR'
78
+ )
79
+ end
80
+
81
+ def disconnect
82
+ @session.close if @session
83
+ @logger.debug { "Closed session" }
84
+ end
85
+
86
+ def shell_init
87
+ return nil if @shell_initialized
88
+ result = execute(<<-PS)
89
+ $ENV:PATH += ";${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\bin\\;" +
90
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\sys\\ruby\\bin\\"
91
+ $ENV:RUBYLIB = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\lib;" +
92
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\facter\\lib;" +
93
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\hiera\\lib;" +
94
+ $ENV:RUBYLIB
95
+
96
+ Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
97
+ $utf8 = [System.Text.Encoding]::UTF8
98
+
99
+ function Invoke-Interpreter
100
+ {
101
+ [CmdletBinding()]
102
+ Param (
103
+ [Parameter()]
104
+ [String]
105
+ $Path,
106
+
107
+ [Parameter()]
108
+ [String]
109
+ $Arguments,
110
+
111
+ [Parameter()]
112
+ [Int32]
113
+ $Timeout,
114
+
115
+ [Parameter()]
116
+ [String]
117
+ $StdinInput = $Null
118
+ )
119
+
120
+ try
121
+ {
122
+ if (-not (Get-Command $Path -ErrorAction SilentlyContinue))
123
+ {
124
+ throw "Could not find executable '$Path' in ${ENV:PATH} on target node"
125
+ }
126
+
127
+ $startInfo = New-Object System.Diagnostics.ProcessStartInfo($Path, $Arguments)
128
+ $startInfo.UseShellExecute = $false
129
+ $startInfo.WorkingDirectory = Split-Path -Parent (Get-Command $Path).Path
130
+ $startInfo.CreateNoWindow = $true
131
+ if ($StdinInput) { $startInfo.RedirectStandardInput = $true }
132
+ $startInfo.RedirectStandardOutput = $true
133
+ $startInfo.RedirectStandardError = $true
134
+
135
+ $stdoutHandler = { if (-not ([String]::IsNullOrEmpty($EventArgs.Data))) { $Host.UI.WriteLine($EventArgs.Data) } }
136
+ $stderrHandler = { if (-not ([String]::IsNullOrEmpty($EventArgs.Data))) { $Host.UI.WriteErrorLine($EventArgs.Data) } }
137
+ $invocationId = [Guid]::NewGuid().ToString()
138
+
139
+ $process = New-Object System.Diagnostics.Process
140
+ $process.StartInfo = $startInfo
141
+ $process.EnableRaisingEvents = $true
142
+
143
+ # https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standarderror(v=vs.110).aspx#Anchor_2
144
+ $stdoutEvent = Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -Action $stdoutHandler
145
+ $stderrEvent = Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -Action $stderrHandler
146
+ $exitedEvent = Register-ObjectEvent -InputObject $process -EventName 'Exited' -SourceIdentifier $invocationId
147
+
148
+ $process.Start() | Out-Null
149
+
150
+ $process.BeginOutputReadLine()
151
+ $process.BeginErrorReadLine()
152
+
153
+ if ($StdinInput)
154
+ {
155
+ $process.StandardInput.WriteLine($StdinInput)
156
+ $process.StandardInput.Close()
157
+ }
158
+
159
+ # park current thread until the PS event is signaled upon process exit
160
+ # OR the timeout has elapsed
161
+ $waitResult = Wait-Event -SourceIdentifier $invocationId -Timeout $Timeout
162
+ if (! $process.HasExited)
163
+ {
164
+ $Host.UI.WriteErrorLine("Process $Path did not complete in $Timeout seconds")
165
+ return 1
166
+ }
167
+
168
+ return $process.ExitCode
169
+ }
170
+ catch
171
+ {
172
+ $Host.UI.WriteErrorLine($_)
173
+ return 1
174
+ }
175
+ finally
176
+ {
177
+ @($stdoutEvent, $stderrEvent, $exitedEvent) |
178
+ ? { $_ -ne $Null } |
179
+ % { Unregister-Event -SourceIdentifier $_.Name }
180
+
181
+ if ($process -ne $Null)
182
+ {
183
+ if (($process.Handle -ne $Null) -and (! $process.HasExited))
184
+ {
185
+ try { $process.Kill() } catch { $Host.UI.WriteErrorLine("Failed To Kill Process $Path") }
186
+ }
187
+ $process.Dispose()
188
+ }
189
+ }
190
+ }
191
+
192
+ function Write-Stream {
193
+ PARAM(
194
+ [Parameter(Position=0)] $stream,
195
+ [Parameter(ValueFromPipeline=$true)] $string
196
+ )
197
+ PROCESS {
198
+ $bytes = $utf8.GetBytes($string)
199
+ $stream.Write( $bytes, 0, $bytes.Length )
200
+ }
201
+ }
202
+
203
+ function Convert-JsonToXml {
204
+ PARAM([Parameter(ValueFromPipeline=$true)] [string[]] $json)
205
+ BEGIN {
206
+ $mStream = New-Object System.IO.MemoryStream
207
+ }
208
+ PROCESS {
209
+ $json | Write-Stream -Stream $mStream
210
+ }
211
+ END {
212
+ $mStream.Position = 0
213
+ try {
214
+ $jsonReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($mStream,[System.Xml.XmlDictionaryReaderQuotas]::Max)
215
+ $xml = New-Object Xml.XmlDocument
216
+ $xml.Load($jsonReader)
217
+ $xml
218
+ } finally {
219
+ $jsonReader.Close()
220
+ $mStream.Dispose()
221
+ }
222
+ }
223
+ }
224
+
225
+ Function ConvertFrom-Xml {
226
+ [CmdletBinding(DefaultParameterSetName="AutoType")]
227
+ PARAM(
228
+ [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [Xml.XmlNode] $xml,
229
+ [Parameter(Mandatory=$true,ParameterSetName="ManualType")] [Type] $Type,
230
+ [Switch] $ForceType
231
+ )
232
+ PROCESS{
233
+ if (Get-Member -InputObject $xml -Name root) {
234
+ return $xml.root.Objects | ConvertFrom-Xml
235
+ } elseif (Get-Member -InputObject $xml -Name Objects) {
236
+ return $xml.Objects | ConvertFrom-Xml
237
+ }
238
+ $propbag = @{}
239
+ foreach ($name in Get-Member -InputObject $xml -MemberType Properties | Where-Object{$_.Name -notmatch "^__|type"} | Select-Object -ExpandProperty name) {
240
+ Write-Debug "$Name Type: $($xml.$Name.type)" -Debug:$false
241
+ $propbag."$Name" = Convert-Properties $xml."$name"
242
+ }
243
+ if (!$Type -and $xml.HasAttribute("__type")) { $Type = $xml.__Type }
244
+ if ($ForceType -and $Type) {
245
+ try {
246
+ $output = New-Object $Type -Property $propbag
247
+ } catch {
248
+ $output = New-Object PSObject -Property $propbag
249
+ $output.PsTypeNames.Insert(0, $xml.__type)
250
+ }
251
+ } elseif ($propbag.Count -ne 0) {
252
+ $output = New-Object PSObject -Property $propbag
253
+ if ($Type) {
254
+ $output.PsTypeNames.Insert(0, $Type)
255
+ }
256
+ }
257
+ return $output
258
+ }
259
+ }
260
+
261
+ Function Convert-Properties {
262
+ PARAM($InputObject)
263
+ switch ($InputObject.type) {
264
+ "object" {
265
+ return (ConvertFrom-Xml -Xml $InputObject)
266
+ }
267
+ "string" {
268
+ $MightBeADate = $InputObject.get_InnerText() -as [DateTime]
269
+ ## Strings that are actually dates (*grumble* JSON is crap)
270
+ if ($MightBeADate -and $propbag."$Name" -eq $MightBeADate.ToString("G")) {
271
+ return $MightBeADate
272
+ } else {
273
+ return $InputObject.get_InnerText()
274
+ }
275
+ }
276
+ "number" {
277
+ $number = $InputObject.get_InnerText()
278
+ if ($number -eq ($number -as [int])) {
279
+ return $number -as [int]
280
+ } elseif ($number -eq ($number -as [double])) {
281
+ return $number -as [double]
282
+ } else {
283
+ return $number -as [decimal]
284
+ }
285
+ }
286
+ "boolean" {
287
+ return [bool]::parse($InputObject.get_InnerText())
288
+ }
289
+ "null" {
290
+ return $null
291
+ }
292
+ "array" {
293
+ [object[]]$Items = $(foreach( $item in $InputObject.GetEnumerator() ) {
294
+ Convert-Properties $item
295
+ })
296
+ return $Items
297
+ }
298
+ default {
299
+ return $InputObject
300
+ }
301
+ }
302
+ }
303
+
304
+ Function ConvertFrom-Json2 {
305
+ [CmdletBinding()]
306
+ PARAM(
307
+ [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [string] $InputObject,
308
+ [Parameter(Mandatory=$true)] [Type] $Type,
309
+ [Switch] $ForceType
310
+ )
311
+ PROCESS {
312
+ $null = $PSBoundParameters.Remove("InputObject")
313
+ [Xml.XmlElement]$xml = (Convert-JsonToXml $InputObject).Root
314
+ if ($xml) {
315
+ if ($xml.Objects) {
316
+ $xml.Objects.Item.GetEnumerator() | ConvertFrom-Xml @PSBoundParameters
317
+ } elseif ($xml.Item -and $xml.Item -isnot [System.Management.Automation.PSParameterizedProperty]) {
318
+ $xml.Item | ConvertFrom-Xml @PSBoundParameters
319
+ } else {
320
+ $xml | ConvertFrom-Xml @PSBoundParameters
321
+ }
322
+ } else {
323
+ Write-Error "Failed to parse JSON with JsonReader" -Debug:$false
324
+ }
325
+ }
326
+ }
327
+
328
+ function ConvertFrom-PSCustomObject
329
+ {
330
+ PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
331
+ PROCESS {
332
+ if ($null -eq $InputObject) { return $null }
333
+
334
+ if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
335
+ $collection = @(
336
+ foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
337
+ )
338
+
339
+ $collection
340
+ } elseif ($InputObject -is [System.Management.Automation.PSCustomObject]) {
341
+ $hash = @{}
342
+ foreach ($property in $InputObject.PSObject.Properties) {
343
+ $hash[$property.Name] = ConvertFrom-PSCustomObject $property.Value
344
+ }
345
+
346
+ $hash
347
+ } else {
348
+ $InputObject
349
+ }
350
+ }
351
+ }
352
+
353
+ function Get-ContentAsJson
354
+ {
355
+ [CmdletBinding()]
356
+ PARAM(
357
+ [Parameter(Mandatory = $true)] $Text,
358
+ [Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
359
+ )
360
+
361
+ # using polyfill cmdlet on PS2, so pass type info
362
+ if ($PSVersionTable.PSVersion -lt [Version]'3.0') {
363
+ $Text | ConvertFrom-Json2 -Type PSObject | ConvertFrom-PSCustomObject
364
+ } else {
365
+ $Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
366
+ }
367
+ }
368
+ PS
369
+ if result.exit_code != 0
370
+ raise BaseError.new("Could not initialize shell: #{result.stderr.string}", "SHELL_INIT_ERROR")
371
+ end
372
+ @shell_initialized = true
373
+ end
374
+
375
+ def execute(command, _ = {})
376
+ result_output = Bolt::Node::Output.new
377
+
378
+ @logger.debug { "Executing command: #{command}" }
379
+
380
+ output = @session.run(command) do |stdout, stderr|
381
+ result_output.stdout << stdout
382
+ @logger.debug { "stdout: #{stdout}" }
383
+ result_output.stderr << stderr
384
+ @logger.debug { "stderr: #{stderr}" }
385
+ end
386
+ result_output.exit_code = output.exitcode
387
+ if output.exitcode.zero?
388
+ @logger.debug { "Command returned successfully" }
389
+ else
390
+ @logger.info { "Command failed with exit code #{output.exitcode}" }
391
+ end
392
+ result_output
393
+ end
394
+
395
+ # 10 minutes in seconds
396
+ DEFAULT_EXECUTION_TIMEOUT = 10 * 60
397
+
398
+ def execute_process(path = '', arguments = [], stdin = nil,
399
+ timeout = DEFAULT_EXECUTION_TIMEOUT)
400
+ quoted_args = arguments.map do |arg|
401
+ "'" + arg.gsub("'", "''") + "'"
402
+ end.join(',')
403
+
404
+ execute(<<-PS)
405
+ $quoted_array = @(
406
+ #{quoted_args}
407
+ )
408
+
409
+ $invokeArgs = @{
410
+ Path = "#{path}"
411
+ Arguments = $quoted_array -Join ' '
412
+ Timeout = #{timeout}
413
+ #{stdin.nil? ? '' : "StdinInput = @'\n" + stdin + "\n'@"}
414
+ }
415
+
416
+ # winrm gem checks $? prior to using $LASTEXITCODE
417
+ # making it necessary to exit with the desired code to propagate status properly
418
+ exit $(Invoke-Interpreter @invokeArgs)
419
+ PS
420
+ end
421
+
422
+ DEFAULT_EXTENSIONS = ['.ps1', '.rb', '.pp'].freeze
423
+
424
+ PS_ARGS = %w[
425
+ -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass
426
+ ].freeze
427
+
428
+ def write_remote_file(source, destination)
429
+ fs = ::WinRM::FS::FileManager.new(@connection)
430
+ # TODO: raise FileError here if this fails
431
+ fs.upload(source, destination)
432
+ end
433
+
434
+ def make_tempdir
435
+ find_parent = target.options[:tmpdir] ? "\"#{target.options[:tmpdir]}\"" : '[System.IO.Path]::GetTempPath()'
436
+ result = execute(<<-PS)
437
+ $parent = #{find_parent}
438
+ $name = [System.IO.Path]::GetRandomFileName()
439
+ $path = Join-Path $parent $name
440
+ New-Item -ItemType Directory -Path $path | Out-Null
441
+ $path
442
+ PS
443
+ if result.exit_code != 0
444
+ raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr}", 'TEMPDIR_ERROR')
445
+ end
446
+ result.stdout.string.chomp
447
+ end
448
+
449
+ def with_remote_file(file)
450
+ ext = File.extname(file)
451
+ unless @extensions.include?(ext)
452
+ raise Bolt::Node::FileError.new("File extension #{ext} is not enabled, "\
453
+ "to run it please add to 'winrm: extensions'", 'FILETYPE_ERROR')
454
+ end
455
+ file_base = File.basename(file)
456
+ dir = make_tempdir
457
+ dest = "#{dir}\\#{file_base}"
458
+ begin
459
+ write_remote_file(file, dest)
460
+ shell_init
461
+ yield dest
462
+ ensure
463
+ execute(<<-PS)
464
+ Remove-Item -Force "#{dest}"
465
+ Remove-Item -Force "#{dir}"
466
+ PS
467
+ end
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end