bolt 2.5.0 → 2.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,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Shell
5
+ class Powershell < Shell
6
+ module Snippets
7
+ class << self
8
+ def execute_process(command)
9
+ <<~PS
10
+ if ([Console]::InputEncoding -eq [System.Text.Encoding]::UTF8) {
11
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding $False
12
+ }
13
+ if ([Console]::OutputEncoding -eq [System.Text.Encoding]::UTF8) {
14
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $False
15
+ }
16
+ $OutputEncoding = [Console]::OutputEncoding
17
+ #{command}
18
+ if (-not $? -and ($LASTEXITCODE -eq $null)) { exit 1 }
19
+ exit $LASTEXITCODE
20
+ PS
21
+ end
22
+
23
+ def make_tempdir(parent)
24
+ <<~PS
25
+ $parent = #{parent}
26
+ $name = [System.IO.Path]::GetRandomFileName()
27
+ $path = Join-Path $parent $name -ErrorAction Stop
28
+ New-Item -ItemType Directory -Path $path -ErrorAction Stop | Out-Null
29
+ $path
30
+ PS
31
+ end
32
+
33
+ def rmdir(dir)
34
+ <<~PS
35
+ Remove-Item -Force -Recurse -Path "#{dir}"
36
+ PS
37
+ end
38
+
39
+ def run_script(arguments, script_path)
40
+ build_arg_list = arguments.map do |a|
41
+ "$invokeArgs.ArgumentList += @'\n#{a}\n'@"
42
+ end.join("\n")
43
+ <<~PS
44
+ $invokeArgs = @{
45
+ ScriptBlock = (Get-Command "#{script_path}").ScriptBlock
46
+ ArgumentList = @()
47
+ }
48
+ #{build_arg_list}
49
+
50
+ try
51
+ {
52
+ Invoke-Command @invokeArgs
53
+ }
54
+ catch
55
+ {
56
+ Write-Error $_.Exception
57
+ exit 1
58
+ }
59
+ PS
60
+ end
61
+
62
+ def ps_task(path, arguments)
63
+ <<~PS
64
+ $private:tempArgs = Get-ContentAsJson (
65
+ $utf8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
66
+ )
67
+ $allowedArgs = (Get-Command "#{path}").Parameters.Keys
68
+ $private:taskArgs = @{}
69
+ $private:tempArgs.Keys | ? { $allowedArgs -contains $_ } | % { $private:taskArgs[$_] = $private:tempArgs[$_] }
70
+ try { & "#{path}" @taskArgs } catch { Write-Error $_.Exception; exit 1 }
71
+ PS
72
+ end
73
+
74
+ def try_catch(command)
75
+ %(try { & "#{command}" } catch { Write-Error $_.Exception; exit 1 })
76
+ end
77
+
78
+ def shell_init
79
+ <<~PS
80
+ $ENV:PATH += ";${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\bin\\;" +
81
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\bin;" +
82
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\sys\\ruby\\bin\\"
83
+ $ENV:RUBYLIB = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\lib;" +
84
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\facter\\lib;" +
85
+ "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\hiera\\lib;" +
86
+ $ENV:RUBYLIB
87
+
88
+ Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
89
+ $utf8 = [System.Text.Encoding]::UTF8
90
+
91
+ function Write-Stream {
92
+ PARAM(
93
+ [Parameter(Position=0)] $stream,
94
+ [Parameter(ValueFromPipeline=$true)] $string
95
+ )
96
+ PROCESS {
97
+ $bytes = $utf8.GetBytes($string)
98
+ $stream.Write( $bytes, 0, $bytes.Length )
99
+ }
100
+ }
101
+
102
+ function Convert-JsonToXml {
103
+ PARAM([Parameter(ValueFromPipeline=$true)] [string[]] $json)
104
+ BEGIN {
105
+ $mStream = New-Object System.IO.MemoryStream
106
+ }
107
+ PROCESS {
108
+ $json | Write-Stream -Stream $mStream
109
+ }
110
+ END {
111
+ $mStream.Position = 0
112
+ try {
113
+ $jsonReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($mStream,[System.Xml.XmlDictionaryReaderQuotas]::Max)
114
+ $xml = New-Object Xml.XmlDocument
115
+ $xml.Load($jsonReader)
116
+ $xml
117
+ } finally {
118
+ $jsonReader.Close()
119
+ $mStream.Dispose()
120
+ }
121
+ }
122
+ }
123
+
124
+ Function ConvertFrom-Xml {
125
+ [CmdletBinding(DefaultParameterSetName="AutoType")]
126
+ PARAM(
127
+ [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [Xml.XmlNode] $xml,
128
+ [Parameter(Mandatory=$true,ParameterSetName="ManualType")] [Type] $Type,
129
+ [Switch] $ForceType
130
+ )
131
+ PROCESS{
132
+ if (Get-Member -InputObject $xml -Name root) {
133
+ return $xml.root.Objects | ConvertFrom-Xml
134
+ } elseif (Get-Member -InputObject $xml -Name Objects) {
135
+ return $xml.Objects | ConvertFrom-Xml
136
+ }
137
+ $propbag = @{}
138
+ foreach ($name in Get-Member -InputObject $xml -MemberType Properties | Where-Object{$_.Name -notmatch "^(__.*|type)$"} | Select-Object -ExpandProperty name) {
139
+ Write-Debug "$Name Type: $($xml.$Name.type)" -Debug:$false
140
+ $propbag."$Name" = Convert-Properties $xml."$name"
141
+ }
142
+ if (!$Type -and $xml.HasAttribute("__type")) { $Type = $xml.__Type }
143
+ if ($ForceType -and $Type) {
144
+ try {
145
+ $output = New-Object $Type -Property $propbag
146
+ } catch {
147
+ $output = New-Object PSObject -Property $propbag
148
+ $output.PsTypeNames.Insert(0, $xml.__type)
149
+ }
150
+ } elseif ($propbag.Count -ne 0) {
151
+ $output = New-Object PSObject -Property $propbag
152
+ if ($Type) {
153
+ $output.PsTypeNames.Insert(0, $Type)
154
+ }
155
+ }
156
+ return $output
157
+ }
158
+ }
159
+
160
+ Function Convert-Properties {
161
+ PARAM($InputObject)
162
+ switch ($InputObject.type) {
163
+ "object" {
164
+ return (ConvertFrom-Xml -Xml $InputObject)
165
+ }
166
+ "string" {
167
+ $MightBeADate = $InputObject.get_InnerText() -as [DateTime]
168
+ ## Strings that are actually dates (*grumble* JSON is crap)
169
+ if ($MightBeADate -and $propbag."$Name" -eq $MightBeADate.ToString("G")) {
170
+ return $MightBeADate
171
+ } else {
172
+ return $InputObject.get_InnerText()
173
+ }
174
+ }
175
+ "number" {
176
+ $number = $InputObject.get_InnerText()
177
+ if ($number -eq ($number -as [int])) {
178
+ return $number -as [int]
179
+ } elseif ($number -eq ($number -as [double])) {
180
+ return $number -as [double]
181
+ } else {
182
+ return $number -as [decimal]
183
+ }
184
+ }
185
+ "boolean" {
186
+ return [bool]::parse($InputObject.get_InnerText())
187
+ }
188
+ "null" {
189
+ return $null
190
+ }
191
+ "array" {
192
+ [object[]]$Items = $(foreach( $item in $InputObject.GetEnumerator() ) {
193
+ Convert-Properties $item
194
+ })
195
+ return $Items
196
+ }
197
+ default {
198
+ return $InputObject
199
+ }
200
+ }
201
+ }
202
+
203
+ Function ConvertFrom-Json2 {
204
+ [CmdletBinding()]
205
+ PARAM(
206
+ [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [string] $InputObject,
207
+ [Parameter(Mandatory=$true)] [Type] $Type,
208
+ [Switch] $ForceType
209
+ )
210
+ PROCESS {
211
+ $null = $PSBoundParameters.Remove("InputObject")
212
+ [Xml.XmlElement]$xml = (Convert-JsonToXml $InputObject).Root
213
+ if ($xml) {
214
+ if ($xml.Objects) {
215
+ $xml.Objects.Item.GetEnumerator() | ConvertFrom-Xml @PSBoundParameters
216
+ } elseif ($xml.Item -and $xml.Item -isnot [System.Management.Automation.PSParameterizedProperty]) {
217
+ $xml.Item | ConvertFrom-Xml @PSBoundParameters
218
+ } else {
219
+ $xml | ConvertFrom-Xml @PSBoundParameters
220
+ }
221
+ } else {
222
+ Write-Error "Failed to parse JSON with JsonReader" -Debug:$false
223
+ }
224
+ }
225
+ }
226
+
227
+ function ConvertFrom-PSCustomObject
228
+ {
229
+ PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
230
+ PROCESS {
231
+ if ($null -eq $InputObject) { return $null }
232
+
233
+ if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
234
+ $collection = @(
235
+ foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
236
+ )
237
+
238
+ $collection
239
+ } elseif ($InputObject -is [System.Management.Automation.PSCustomObject]) {
240
+ $hash = @{}
241
+ foreach ($property in $InputObject.PSObject.Properties) {
242
+ $hash[$property.Name] = ConvertFrom-PSCustomObject $property.Value
243
+ }
244
+
245
+ $hash
246
+ } else {
247
+ $InputObject
248
+ }
249
+ }
250
+ }
251
+
252
+ function Get-ContentAsJson
253
+ {
254
+ [CmdletBinding()]
255
+ PARAM(
256
+ [Parameter(Mandatory = $true)] $Text,
257
+ [Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
258
+ )
259
+
260
+ # using polyfill cmdlet on PS2, so pass type info
261
+ if ($PSVersionTable.PSVersion -lt [Version]'3.0') {
262
+ $Text | ConvertFrom-Json2 -Type PSObject | ConvertFrom-PSCustomObject
263
+ } else {
264
+ $Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
265
+ }
266
+ }
267
+ PS
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'fileutils'
5
+ require 'tempfile'
5
6
  require 'bolt/node/output'
6
7
  require 'bolt/util'
7
8
 
@@ -44,7 +45,16 @@ module Bolt
44
45
  end
45
46
 
46
47
  def execute(command)
47
- Open3.popen3(command)
48
+ if Bolt::Util.windows?
49
+ command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
50
+ script_file = Tempfile.new(['wrapper', '.ps1'], target.options['tmpdir'])
51
+ File.write(script_file, command)
52
+ script_file.close
53
+
54
+ command = ['powershell.exe', *Bolt::Shell::Powershell::PS_ARGS, script_file.path]
55
+ end
56
+
57
+ Open3.popen3(*command)
48
58
  end
49
59
 
50
60
  # This is used by the Bash shell to decide whether to `cd` before
@@ -2,20 +2,10 @@
2
2
 
3
3
  require 'bolt/node/errors'
4
4
  require 'bolt/transport/base'
5
- require 'bolt/transport/powershell'
6
5
 
7
6
  module Bolt
8
7
  module Transport
9
- class WinRM < Base
10
- def provided_features
11
- ['powershell']
12
- end
13
-
14
- def default_input_method(executable)
15
- input_method ||= Powershell.powershell_file?(executable) ? 'powershell' : 'both'
16
- input_method
17
- end
18
-
8
+ class WinRM < Simple
19
9
  def initialize
20
10
  super
21
11
  require 'winrm'
@@ -36,113 +26,6 @@ module Bolt
36
26
  logger.info("Failed to close connection to #{target.safe_name} : #{e.message}")
37
27
  end
38
28
  end
39
-
40
- def upload(target, source, destination, _options = {})
41
- with_connection(target) do |conn|
42
- conn.write_remote_file(source, destination)
43
- Bolt::Result.for_upload(target, source, destination)
44
- end
45
- end
46
-
47
- def run_command(target, command, _options = {})
48
- with_connection(target) do |conn|
49
- output = conn.execute(command)
50
- Bolt::Result.for_command(target,
51
- output.stdout.string,
52
- output.stderr.string,
53
- output.exit_code,
54
- 'command', command)
55
- end
56
- end
57
-
58
- def run_script(target, script, arguments, _options = {})
59
- # unpack any Sensitive data
60
- arguments = unwrap_sensitive_args(arguments)
61
- with_connection(target) do |conn|
62
- conn.with_remote_tempdir do |dir|
63
- remote_path = conn.write_remote_executable(dir, script)
64
- if Powershell.powershell_file?(remote_path)
65
- output = conn.execute(Powershell.run_script(arguments, remote_path))
66
- else
67
- path, args = *Powershell.process_from_extension(remote_path)
68
- args += Powershell.escape_arguments(arguments)
69
- output = conn.execute_process(path, args)
70
- end
71
- Bolt::Result.for_command(target,
72
- output.stdout.string,
73
- output.stderr.string,
74
- output.exit_code,
75
- 'script', script)
76
- end
77
- end
78
- end
79
-
80
- def run_task(target, task, arguments, _options = {})
81
- implementation = select_implementation(target, task)
82
- executable = implementation['path']
83
- input_method = implementation['input_method']
84
- extra_files = implementation['files']
85
- input_method ||= Powershell.powershell_file?(executable) ? 'powershell' : 'both'
86
-
87
- # unpack any Sensitive data
88
- arguments = unwrap_sensitive_args(arguments)
89
- with_connection(target) do |conn|
90
- conn.with_remote_tempdir do |dir|
91
- if extra_files.empty?
92
- task_dir = dir
93
- else
94
- # TODO: optimize upload of directories
95
- arguments['_installdir'] = dir
96
- task_dir = File.join(dir, task.tasks_dir)
97
- conn.mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
98
- extra_files.each do |file|
99
- conn.write_remote_file(file['path'], File.join(dir, file['name']))
100
- end
101
- end
102
-
103
- remote_task_path = conn.write_remote_executable(task_dir, executable)
104
-
105
- if Bolt::Task::STDIN_METHODS.include?(input_method)
106
- stdin = JSON.dump(arguments)
107
- end
108
-
109
- if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
110
- envify_params(arguments).each do |(arg, val)|
111
- cmd = Powershell.set_env(arg, val)
112
- result = conn.execute(cmd)
113
- if result.exit_code != 0
114
- raise Bolt::Node::EnvironmentVarError.new(arg, val)
115
- end
116
- end
117
- end
118
-
119
- conn.shell_init
120
- output =
121
- if Powershell.powershell_file?(remote_task_path) && stdin.nil?
122
- conn.execute(Powershell.run_ps_task(arguments, remote_task_path, input_method))
123
- else
124
- if (interpreter = select_interpreter(remote_task_path, target.options['interpreters']))
125
- path = interpreter
126
- args = [remote_task_path]
127
- else
128
- path, args = *Powershell.process_from_extension(remote_task_path)
129
- end
130
- conn.execute_process(path, args, stdin)
131
- end
132
-
133
- Bolt::Result.for_task(target, output.stdout.string,
134
- output.stderr.string,
135
- output.exit_code,
136
- task.name)
137
- end
138
- end
139
- end
140
-
141
- def connected?(target)
142
- with_connection(target) { true }
143
- rescue Bolt::Node::ConnectError
144
- false
145
- end
146
29
  end
147
30
  end
148
31
  end
@@ -5,12 +5,10 @@ require 'bolt/node/output'
5
5
 
6
6
  module Bolt
7
7
  module Transport
8
- class WinRM < Base
8
+ class WinRM < Simple
9
9
  class Connection
10
10
  attr_reader :logger, :target
11
11
 
12
- DEFAULT_EXTENSIONS = ['.ps1', '.rb', '.pp'].freeze
13
-
14
12
  def initialize(target, transport_logger)
15
13
  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
16
14
  @target = target
@@ -19,9 +17,6 @@ module Bolt
19
17
  @port = @target.port || default_port
20
18
  @user = @target.user
21
19
  # Build set of extensions from extensions config as well as interpreters
22
- extensions = [target.options['extensions'] || []].flatten.map { |ext| ext[0] != '.' ? '.' + ext : ext }
23
- extensions += target.options['interpreters'].keys if target.options['interpreters']
24
- @extensions = DEFAULT_EXTENSIONS.to_set.merge(extensions)
25
20
 
26
21
  @logger = Logging.logger[@target.safe_name]
27
22
  logger.debug("Initializing winrm connection to #{@target.safe_name}")
@@ -105,67 +100,48 @@ module Bolt
105
100
  @logger.debug { "Closed session" }
106
101
  end
107
102
 
108
- def shell_init
109
- return nil if @shell_initialized
110
- result = execute(Powershell.shell_init)
111
- if result.exit_code != 0
112
- raise BaseError.new("Could not initialize shell: #{result.stderr.string}", "SHELL_INIT_ERROR")
113
- end
114
- @shell_initialized = true
115
- end
116
-
117
103
  def execute(command)
118
- result_output = Bolt::Node::Output.new
119
-
120
104
  @logger.debug { "Executing command: #{command}" }
121
105
 
122
- output = @session.run(command) do |stdout, stderr|
123
- result_output.stdout << stdout
124
- @logger.debug { "stdout: #{stdout}" }
125
- result_output.stderr << stderr
126
- @logger.debug { "stderr: #{stderr}" }
127
- end
128
- result_output.exit_code = output.exitcode
129
- if output.exitcode.zero?
130
- @logger.debug { "Command returned successfully" }
131
- else
132
- @logger.info { "Command failed with exit code #{output.exitcode}" }
106
+ inp = StringIO.new
107
+ # This transport doesn't accept stdin, so close the stream to ensure
108
+ # it will fail if the shell attempts to provide stdin
109
+ inp.close
110
+
111
+ out_rd, out_wr = IO.pipe
112
+ err_rd, err_wr = IO.pipe
113
+ th = Thread.new do
114
+ result = @session.run(command)
115
+ out_wr << result.stdout
116
+ err_wr << result.stderr
117
+ out_wr.close
118
+ err_wr.close
119
+ result.exitcode
133
120
  end
134
- result_output
121
+
122
+ [inp, out_rd, err_rd, th]
135
123
  rescue StandardError
136
124
  @logger.debug { "Command aborted" }
137
125
  raise
138
126
  end
139
127
 
140
- def execute_process(path = '', arguments = [], stdin = nil)
141
- execute(Powershell.execute_process(path, arguments, stdin))
142
- end
143
-
144
- def mkdirs(dirs)
145
- result = execute(Powershell.mkdirs(dirs))
146
- if result.exit_code != 0
147
- message = "Could not create directories: #{result.stderr}"
148
- raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
149
- end
150
- end
151
-
152
- def write_remote_file(source, destination)
128
+ def copy_file(source, destination)
153
129
  @logger.debug { "Uploading #{source}, to #{destination}" }
154
130
  if target.options['file-protocol'] == 'smb'
155
- write_remote_file_smb(source, destination)
131
+ copy_file_smb(source, destination)
156
132
  else
157
- write_remote_file_winrm(source, destination)
133
+ copy_file_winrm(source, destination)
158
134
  end
159
135
  end
160
136
 
161
- def write_remote_file_winrm(source, destination)
137
+ def copy_file_winrm(source, destination)
162
138
  fs = ::WinRM::FS::FileManager.new(@connection)
163
139
  fs.upload(source, destination)
164
140
  rescue StandardError => e
165
141
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
166
142
  end
167
143
 
168
- def write_remote_file_smb(source, destination)
144
+ def copy_file_smb(source, destination)
169
145
  # lazy-load expensive gem code
170
146
  require 'ruby_smb'
171
147
 
@@ -185,7 +161,7 @@ module Bolt
185
161
  client = smb_client_login
186
162
  tree = client.tree_connect(path)
187
163
  begin
188
- write_remote_file_smb_recursive(tree, source, dest)
164
+ copy_file_smb_recursive(tree, source, dest)
189
165
  ensure
190
166
  tree.disconnect!
191
167
  end
@@ -195,35 +171,8 @@ module Bolt
195
171
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
196
172
  end
197
173
 
198
- def make_tempdir
199
- find_parent = target.options['tmpdir'] ? "\"#{target.options['tmpdir']}\"" : '[System.IO.Path]::GetTempPath()'
200
- result = execute(Powershell.make_tempdir(find_parent))
201
- if result.exit_code != 0
202
- raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr}", 'TEMPDIR_ERROR')
203
- end
204
- result.stdout.string.chomp
205
- end
206
-
207
- def with_remote_tempdir
208
- dir = make_tempdir
209
- yield dir
210
- ensure
211
- execute(Powershell.rmdir(dir))
212
- end
213
-
214
- def validate_extensions(ext)
215
- unless @extensions.include?(ext)
216
- raise Bolt::Node::FileError.new("File extension #{ext} is not enabled, "\
217
- "to run it please add to 'winrm: extensions'", 'FILETYPE_ERROR')
218
- end
219
- end
220
-
221
- def write_remote_executable(dir, file, filename = nil)
222
- filename ||= File.basename(file)
223
- validate_extensions(File.extname(filename))
224
- remote_path = "#{dir}\\#{filename}"
225
- write_remote_file(file, remote_path)
226
- remote_path
174
+ def shell
175
+ @shell ||= Bolt::Shell::Powershell.new(target, self)
227
176
  end
228
177
 
229
178
  private
@@ -273,13 +222,13 @@ module Bolt
273
222
  )
274
223
  end
275
224
 
276
- def write_remote_file_smb_recursive(tree, source, dest)
225
+ def copy_file_smb_recursive(tree, source, dest)
277
226
  if Dir.exist?(source)
278
227
  tree.open_directory(directory: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OPEN_IF)
279
228
 
280
229
  Dir.children(source).each do |child|
281
230
  child_dest = dest + '\\' + child
282
- write_remote_file_smb_recursive(tree, File.join(source, child), child_dest)
231
+ copy_file_smb_recursive(tree, File.join(source, child), child_dest)
283
232
  end
284
233
  return
285
234
  end