bolt 1.5.0 → 1.6.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.

@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ module Transport
5
+ module Powershell
6
+ class << self
7
+ PS_ARGS = %w[
8
+ -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass
9
+ ].freeze
10
+
11
+ def powershell_file?(path)
12
+ Pathname(path).extname.casecmp('.ps1').zero?
13
+ end
14
+
15
+ def process_from_extension(path)
16
+ case Pathname(path).extname.downcase
17
+ when '.rb'
18
+ [
19
+ 'ruby.exe',
20
+ ['-S', "\"#{path}\""]
21
+ ]
22
+ when '.ps1'
23
+ [
24
+ 'powershell.exe',
25
+ [*PS_ARGS, '-File', "\"#{path}\""]
26
+ ]
27
+ when '.pp'
28
+ [
29
+ 'puppet.bat',
30
+ ['apply', "\"#{path}\""]
31
+ ]
32
+ else
33
+ # Run the script via cmd, letting Windows extension handling determine how
34
+ [
35
+ 'cmd.exe',
36
+ ['/c', "\"#{path}\""]
37
+ ]
38
+ end
39
+ end
40
+
41
+ def escape_arguments(arguments)
42
+ arguments.map do |arg|
43
+ if arg =~ / /
44
+ "\"#{arg}\""
45
+ else
46
+ arg
47
+ end
48
+ end
49
+ end
50
+
51
+ def set_env(arg, val)
52
+ "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
53
+ end
54
+
55
+ def execute_process(path, arguments, stdin = nil)
56
+ quoted_args = arguments.map do |arg|
57
+ "'" + arg.gsub("'", "''") + "'"
58
+ end.join(' ')
59
+
60
+ exec_cmd =
61
+ if stdin.nil?
62
+ "& #{path} #{quoted_args}"
63
+ else
64
+ "@'\n#{stdin}\n'@ | & #{path} #{quoted_args}"
65
+ end
66
+ <<-PS
67
+ $OutputEncoding = [Console]::OutputEncoding
68
+ #{exec_cmd}
69
+ if (-not $? -and ($LASTEXITCODE -eq $null)) { exit 1 }
70
+ exit $LASTEXITCODE
71
+ PS
72
+ end
73
+
74
+ def mkdirs(dirs)
75
+ "mkdir -Force #{dirs.uniq.sort.join(',')}"
76
+ end
77
+
78
+ def make_tempdir(parent)
79
+ <<-PS
80
+ $parent = #{parent}
81
+ $name = [System.IO.Path]::GetRandomFileName()
82
+ $path = Join-Path $parent $name
83
+ New-Item -ItemType Directory -Path $path | Out-Null
84
+ $path
85
+ PS
86
+ end
87
+
88
+ def rmdir(dir)
89
+ <<-PS
90
+ Remove-Item -Force -Recurse -Path "#{dir}"
91
+ PS
92
+ end
93
+
94
+ def run_script(arguments, script_path)
95
+ mapped_args = arguments.map do |a|
96
+ "$invokeArgs.ArgumentList += @'\n#{a}\n'@"
97
+ end.join("\n")
98
+ <<-PS
99
+ $invokeArgs = @{
100
+ ScriptBlock = (Get-Command "#{script_path}").ScriptBlock
101
+ ArgumentList = @()
102
+ }
103
+ #{mapped_args}
104
+
105
+ try
106
+ {
107
+ Invoke-Command @invokeArgs
108
+ }
109
+ catch
110
+ {
111
+ Write-Error $_.Exception
112
+ exit 1
113
+ }
114
+ PS
115
+ end
116
+
117
+ def run_ps_task(arguments, task_path, input_method)
118
+ # NOTE: cannot redirect STDIN to a .ps1 script inside of PowerShell
119
+ # must create new powershell.exe process like other interpreters
120
+ # fortunately, using PS with stdin input_method should never happen
121
+ if input_method == 'powershell'
122
+ <<-PS
123
+ $private:tempArgs = Get-ContentAsJson (
124
+ $utf8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
125
+ )
126
+ $allowedArgs = (Get-Command "#{task_path}").Parameters.Keys
127
+ $private:taskArgs = @{}
128
+ $private:tempArgs.Keys | ? { $allowedArgs -contains $_ } | % { $private:taskArgs[$_] = $private:tempArgs[$_] }
129
+ try { & "#{task_path}" @taskArgs } catch { Write-Error $_.Exception; exit 1 }
130
+ PS
131
+ else
132
+ %(try { & "#{task_path}" } catch { Write-Error $_.Exception; exit 1 })
133
+ end
134
+ end
135
+
136
+ def shell_init
137
+ <<-PS
138
+ $ENV:PATH += ";${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\bin\\;" +
139
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\bin;" +
140
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\sys\\ruby\\bin\\"
141
+ $ENV:RUBYLIB = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\lib;" +
142
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\facter\\lib;" +
143
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\hiera\\lib;" +
144
+ $ENV:RUBYLIB
145
+
146
+ Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
147
+ $utf8 = [System.Text.Encoding]::UTF8
148
+
149
+ function Write-Stream {
150
+ PARAM(
151
+ [Parameter(Position=0)] $stream,
152
+ [Parameter(ValueFromPipeline=$true)] $string
153
+ )
154
+ PROCESS {
155
+ $bytes = $utf8.GetBytes($string)
156
+ $stream.Write( $bytes, 0, $bytes.Length )
157
+ }
158
+ }
159
+
160
+ function Convert-JsonToXml {
161
+ PARAM([Parameter(ValueFromPipeline=$true)] [string[]] $json)
162
+ BEGIN {
163
+ $mStream = New-Object System.IO.MemoryStream
164
+ }
165
+ PROCESS {
166
+ $json | Write-Stream -Stream $mStream
167
+ }
168
+ END {
169
+ $mStream.Position = 0
170
+ try {
171
+ $jsonReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($mStream,[System.Xml.XmlDictionaryReaderQuotas]::Max)
172
+ $xml = New-Object Xml.XmlDocument
173
+ $xml.Load($jsonReader)
174
+ $xml
175
+ } finally {
176
+ $jsonReader.Close()
177
+ $mStream.Dispose()
178
+ }
179
+ }
180
+ }
181
+
182
+ Function ConvertFrom-Xml {
183
+ [CmdletBinding(DefaultParameterSetName="AutoType")]
184
+ PARAM(
185
+ [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [Xml.XmlNode] $xml,
186
+ [Parameter(Mandatory=$true,ParameterSetName="ManualType")] [Type] $Type,
187
+ [Switch] $ForceType
188
+ )
189
+ PROCESS{
190
+ if (Get-Member -InputObject $xml -Name root) {
191
+ return $xml.root.Objects | ConvertFrom-Xml
192
+ } elseif (Get-Member -InputObject $xml -Name Objects) {
193
+ return $xml.Objects | ConvertFrom-Xml
194
+ }
195
+ $propbag = @{}
196
+ foreach ($name in Get-Member -InputObject $xml -MemberType Properties | Where-Object{$_.Name -notmatch "^__|type"} | Select-Object -ExpandProperty name) {
197
+ Write-Debug "$Name Type: $($xml.$Name.type)" -Debug:$false
198
+ $propbag."$Name" = Convert-Properties $xml."$name"
199
+ }
200
+ if (!$Type -and $xml.HasAttribute("__type")) { $Type = $xml.__Type }
201
+ if ($ForceType -and $Type) {
202
+ try {
203
+ $output = New-Object $Type -Property $propbag
204
+ } catch {
205
+ $output = New-Object PSObject -Property $propbag
206
+ $output.PsTypeNames.Insert(0, $xml.__type)
207
+ }
208
+ } elseif ($propbag.Count -ne 0) {
209
+ $output = New-Object PSObject -Property $propbag
210
+ if ($Type) {
211
+ $output.PsTypeNames.Insert(0, $Type)
212
+ }
213
+ }
214
+ return $output
215
+ }
216
+ }
217
+
218
+ Function Convert-Properties {
219
+ PARAM($InputObject)
220
+ switch ($InputObject.type) {
221
+ "object" {
222
+ return (ConvertFrom-Xml -Xml $InputObject)
223
+ }
224
+ "string" {
225
+ $MightBeADate = $InputObject.get_InnerText() -as [DateTime]
226
+ ## Strings that are actually dates (*grumble* JSON is crap)
227
+ if ($MightBeADate -and $propbag."$Name" -eq $MightBeADate.ToString("G")) {
228
+ return $MightBeADate
229
+ } else {
230
+ return $InputObject.get_InnerText()
231
+ }
232
+ }
233
+ "number" {
234
+ $number = $InputObject.get_InnerText()
235
+ if ($number -eq ($number -as [int])) {
236
+ return $number -as [int]
237
+ } elseif ($number -eq ($number -as [double])) {
238
+ return $number -as [double]
239
+ } else {
240
+ return $number -as [decimal]
241
+ }
242
+ }
243
+ "boolean" {
244
+ return [bool]::parse($InputObject.get_InnerText())
245
+ }
246
+ "null" {
247
+ return $null
248
+ }
249
+ "array" {
250
+ [object[]]$Items = $(foreach( $item in $InputObject.GetEnumerator() ) {
251
+ Convert-Properties $item
252
+ })
253
+ return $Items
254
+ }
255
+ default {
256
+ return $InputObject
257
+ }
258
+ }
259
+ }
260
+
261
+ Function ConvertFrom-Json2 {
262
+ [CmdletBinding()]
263
+ PARAM(
264
+ [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [string] $InputObject,
265
+ [Parameter(Mandatory=$true)] [Type] $Type,
266
+ [Switch] $ForceType
267
+ )
268
+ PROCESS {
269
+ $null = $PSBoundParameters.Remove("InputObject")
270
+ [Xml.XmlElement]$xml = (Convert-JsonToXml $InputObject).Root
271
+ if ($xml) {
272
+ if ($xml.Objects) {
273
+ $xml.Objects.Item.GetEnumerator() | ConvertFrom-Xml @PSBoundParameters
274
+ } elseif ($xml.Item -and $xml.Item -isnot [System.Management.Automation.PSParameterizedProperty]) {
275
+ $xml.Item | ConvertFrom-Xml @PSBoundParameters
276
+ } else {
277
+ $xml | ConvertFrom-Xml @PSBoundParameters
278
+ }
279
+ } else {
280
+ Write-Error "Failed to parse JSON with JsonReader" -Debug:$false
281
+ }
282
+ }
283
+ }
284
+
285
+ function ConvertFrom-PSCustomObject
286
+ {
287
+ PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
288
+ PROCESS {
289
+ if ($null -eq $InputObject) { return $null }
290
+
291
+ if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
292
+ $collection = @(
293
+ foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
294
+ )
295
+
296
+ $collection
297
+ } elseif ($InputObject -is [System.Management.Automation.PSCustomObject]) {
298
+ $hash = @{}
299
+ foreach ($property in $InputObject.PSObject.Properties) {
300
+ $hash[$property.Name] = ConvertFrom-PSCustomObject $property.Value
301
+ }
302
+
303
+ $hash
304
+ } else {
305
+ $InputObject
306
+ }
307
+ }
308
+ }
309
+
310
+ function Get-ContentAsJson
311
+ {
312
+ [CmdletBinding()]
313
+ PARAM(
314
+ [Parameter(Mandatory = $true)] $Text,
315
+ [Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
316
+ )
317
+
318
+ # using polyfill cmdlet on PS2, so pass type info
319
+ if ($PSVersionTable.PSVersion -lt [Version]'3.0') {
320
+ $Text | ConvertFrom-Json2 -Type PSObject | ConvertFrom-PSCustomObject
321
+ } else {
322
+ $Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
323
+ }
324
+ }
325
+ PS
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/task/remote'
4
+ require 'bolt/transport/base'
5
+
6
+ module Bolt
7
+ module Transport
8
+ class Remote < Base
9
+ # The options for the remote transport not defined.
10
+ def self.filter_options(unfiltered)
11
+ unfiltered
12
+ end
13
+
14
+ def self.validate(options)
15
+ # This will fail when validating global config
16
+ # unless options['device-type']
17
+ # raise Bolt::ValidationError, 'Must specify device-type for devices'
18
+ # end
19
+ end
20
+
21
+ # TODO: this should have access to inventory so target doesn't have to
22
+ def initialize(executor)
23
+ super()
24
+
25
+ @executor = executor
26
+ end
27
+
28
+ def get_proxy(target)
29
+ inventory = target.inventory
30
+ raise "Target was created without inventory? Not get_targets?" unless inventory
31
+ proxy = inventory.get_targets(target.options['run-on'] || 'localhost').first
32
+
33
+ if proxy.transport == 'remote'
34
+ msg = "#{proxy.name} is not a valid run-on target for #{target.name} since is also remote."
35
+ raise Bolt::Error.new(msg, 'bolt/invalid-remote-target')
36
+ end
37
+ proxy
38
+ end
39
+
40
+ # Cannot batch because arugments differ
41
+ def run_task(target, task, arguments, options = {})
42
+ proxy_target = get_proxy(target)
43
+ transport = @executor.transport(proxy_target.protocol)
44
+ arguments = arguments.merge('_target' => target.to_h.reject { |_, v| v.nil? })
45
+
46
+ remote_task = Bolt::Task::Remote.new(task.to_h)
47
+
48
+ result = transport.run_task(proxy_target, remote_task, arguments, options)
49
+ Bolt::Result.new(target, value: result.value)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -121,11 +121,10 @@ module Bolt
121
121
  end
122
122
 
123
123
  def run_task(target, task, arguments, options = {})
124
- implementation = task.select_implementation(target, provided_features)
124
+ implementation = select_implementation(target, task)
125
125
  executable = implementation['path']
126
126
  input_method = implementation['input_method']
127
127
  extra_files = implementation['files']
128
- input_method ||= 'both'
129
128
 
130
129
  # unpack any Sensitive data
131
130
  arguments = unwrap_sensitive_args(arguments)
@@ -2,14 +2,11 @@
2
2
 
3
3
  require 'bolt/node/errors'
4
4
  require 'bolt/transport/base'
5
+ require 'bolt/transport/powershell'
5
6
 
6
7
  module Bolt
7
8
  module Transport
8
9
  class WinRM < Base
9
- PS_ARGS = %w[
10
- -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass
11
- ].freeze
12
-
13
10
  def self.options
14
11
  %w[port user password connect-timeout ssl ssl-verify tmpdir cacert extensions]
15
12
  end
@@ -18,6 +15,11 @@ module Bolt
18
15
  ['powershell']
19
16
  end
20
17
 
18
+ def default_input_method(executable)
19
+ input_method ||= Powershell.powershell_file?(executable) ? 'powershell' : 'both'
20
+ input_method
21
+ end
22
+
21
23
  def self.validate(options)
22
24
  ssl_flag = options['ssl']
23
25
  unless !!ssl_flag == ssl_flag
@@ -74,34 +76,14 @@ module Bolt
74
76
  def run_script(target, script, arguments, _options = {})
75
77
  # unpack any Sensitive data
76
78
  arguments = unwrap_sensitive_args(arguments)
77
-
78
79
  with_connection(target) do |conn|
79
80
  conn.with_remote_tempdir do |dir|
80
81
  remote_path = conn.write_remote_executable(dir, script)
81
- if powershell_file?(remote_path)
82
- mapped_args = arguments.map do |a|
83
- "$invokeArgs.ArgumentList += @'\n#{a}\n'@"
84
- end.join("\n")
85
- output = conn.execute(<<-PS)
86
- $invokeArgs = @{
87
- ScriptBlock = (Get-Command "#{remote_path}").ScriptBlock
88
- ArgumentList = @()
89
- }
90
- #{mapped_args}
91
-
92
- try
93
- {
94
- Invoke-Command @invokeArgs
95
- }
96
- catch
97
- {
98
- Write-Error $_.Exception
99
- exit 1
100
- }
101
- PS
82
+ if Powershell.powershell_file?(remote_path)
83
+ output = conn.execute(Powershell.run_script(arguments, remote_path))
102
84
  else
103
- path, args = *process_from_extension(remote_path)
104
- args += escape_arguments(arguments)
85
+ path, args = *Powershell.process_from_extension(remote_path)
86
+ args += Powershell.escape_arguments(arguments)
105
87
  output = conn.execute_process(path, args)
106
88
  end
107
89
  Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
@@ -110,11 +92,11 @@ catch
110
92
  end
111
93
 
112
94
  def run_task(target, task, arguments, _options = {})
113
- implementation = task.select_implementation(target, provided_features)
95
+ implementation = select_implementation(target, task)
114
96
  executable = implementation['path']
115
97
  input_method = implementation['input_method']
116
98
  extra_files = implementation['files']
117
- input_method ||= powershell_file?(executable) ? 'powershell' : 'both'
99
+ input_method ||= Powershell.powershell_file?(executable) ? 'powershell' : 'both'
118
100
 
119
101
  # unpack any Sensitive data
120
102
  arguments = unwrap_sensitive_args(arguments)
@@ -140,7 +122,7 @@ catch
140
122
 
141
123
  if ENVIRONMENT_METHODS.include?(input_method)
142
124
  envify_params(arguments).each do |(arg, val)|
143
- cmd = "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
125
+ cmd = Powershell.set_env(arg, val)
144
126
  result = conn.execute(cmd)
145
127
  if result.exit_code != 0
146
128
  raise Bolt::Node::EnvironmentVarError.new(arg, val)
@@ -150,25 +132,10 @@ catch
150
132
 
151
133
  conn.shell_init
152
134
  output =
153
- if powershell_file?(remote_task_path) && stdin.nil?
154
- # NOTE: cannot redirect STDIN to a .ps1 script inside of PowerShell
155
- # must create new powershell.exe process like other interpreters
156
- # fortunately, using PS with stdin input_method should never happen
157
- if input_method == 'powershell'
158
- conn.execute(<<-PS)
159
- $private:tempArgs = Get-ContentAsJson (
160
- $utf8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
161
- )
162
- $allowedArgs = (Get-Command "#{remote_task_path}").Parameters.Keys
163
- $private:taskArgs = @{}
164
- $private:tempArgs.Keys | ? { $allowedArgs -contains $_ } | % { $private:taskArgs[$_] = $private:tempArgs[$_] }
165
- try { & "#{remote_task_path}" @taskArgs } catch { Write-Error $_.Exception; exit 1 }
166
- PS
167
- else
168
- conn.execute(%(try { & "#{remote_task_path}" } catch { Write-Error $_.Exception; exit 1 }))
169
- end
135
+ if Powershell.powershell_file?(remote_task_path) && stdin.nil?
136
+ conn.execute(Powershell.run_ps_task(arguments, remote_task_path, input_method))
170
137
  else
171
- path, args = *process_from_extension(remote_task_path)
138
+ path, args = *Powershell.process_from_extension(remote_task_path)
172
139
  conn.execute_process(path, args, stdin)
173
140
  end
174
141
 
@@ -179,46 +146,6 @@ try { & "#{remote_task_path}" @taskArgs } catch { Write-Error $_.Exception; exit
179
146
  end
180
147
  end
181
148
 
182
- def powershell_file?(path)
183
- Pathname(path).extname.casecmp('.ps1').zero?
184
- end
185
-
186
- def process_from_extension(path)
187
- case Pathname(path).extname.downcase
188
- when '.rb'
189
- [
190
- 'ruby.exe',
191
- ['-S', "\"#{path}\""]
192
- ]
193
- when '.ps1'
194
- [
195
- 'powershell.exe',
196
- [*PS_ARGS, '-File', "\"#{path}\""]
197
- ]
198
- when '.pp'
199
- [
200
- 'puppet.bat',
201
- ['apply', "\"#{path}\""]
202
- ]
203
- else
204
- # Run the script via cmd, letting Windows extension handling determine how
205
- [
206
- 'cmd.exe',
207
- ['/c', "\"#{path}\""]
208
- ]
209
- end
210
- end
211
-
212
- def escape_arguments(arguments)
213
- arguments.map do |arg|
214
- if arg =~ / /
215
- "\"#{arg}\""
216
- else
217
- arg
218
- end
219
- end
220
- end
221
-
222
149
  def connected?(target)
223
150
  with_connection(target) { true }
224
151
  rescue Bolt::Node::ConnectError