kitchen-hyperv 0.10.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Author:: Steven Murawski <smurawski@chef.io>
3
5
  # Copyright:: Copyright (c) 2020 Chef Software, Inc.
@@ -15,41 +17,135 @@
15
17
  # See the License for the specific language governing permissions and
16
18
  # limitations under the License.
17
19
 
18
- require "mixlib/shellout" unless defined?(Mixlib::ShellOut)
20
+ require "base64" unless defined?(Base64)
19
21
  require "benchmark" unless defined?(Benchmark)
22
+ require "rbconfig/sizeof" unless defined?(RbConfig::SIZEOF)
20
23
  require "fileutils" unless defined?(FileUtils)
21
24
  require "json" unless defined?(JSON)
22
25
 
23
26
  module Kitchen
24
27
  module Driver
28
+ # PowerShell generation and execution for {Kitchen::Driver::Hyperv}.
29
+ #
30
+ # Every method here is either a script generator -- a `*_ps` method
31
+ # returning PowerShell source -- or part of the pipeline that runs one:
32
+ # {#run_ps} wraps the script so it dot-sources `support/hyperv.ps1`,
33
+ # {#encode_command} encodes it for `powershell.exe -encodedcommand`, and
34
+ # {#execute_command} runs it over the Train connection and parses the JSON
35
+ # that comes back.
36
+ #
37
+ # Encoding sidesteps every layer of quoting between Ruby and PowerShell,
38
+ # which matters because these scripts embed Windows paths and user-supplied
39
+ # strings.
40
+ #
41
+ # The module reads `config`, `instance` and `@state` from the driver it is
42
+ # mixed into, so it is not usable standalone.
43
+ #
44
+ # @see Kitchen::Driver::Hyperv
25
45
  module PowerShellScripts
46
+ # Values Windows reports in PROCESSOR_ARCHITECTURE for a 64-bit OS.
47
+ #
48
+ # ARM64 matters for Windows on ARM devices, which run Hyper-V: matching
49
+ # only AMD64 there made both width checks false and sent the driver to
50
+ # the Sysnative path, which does not exist for a native 64-bit process.
51
+ SIXTY_FOUR_BIT_ARCHITECTURES = %w{AMD64 ARM64 IA64}.freeze
52
+
53
+ # Encode a script the way `powershell.exe -encodedcommand` expects it:
54
+ # UTF-16LE, then Base64.
55
+ #
56
+ # @param script [String] UTF-8 PowerShell source
57
+ # @return [String] strict Base64, with no line breaks
58
+ # @api private
26
59
  def encode_command(script)
27
60
  encoded_script = script.encode("UTF-16LE", "UTF-8")
28
61
  Base64.strict_encode64(encoded_script)
29
62
  end
30
63
 
31
- def is_64bit?
64
+ # The OS architecture, seeing through WOW64.
65
+ #
66
+ # A 32-bit process on 64-bit Windows reads its own architecture from
67
+ # PROCESSOR_ARCHITECTURE; PROCESSOR_ARCHITEW6432 is what reveals the real
68
+ # one, and is only set in that case.
69
+ #
70
+ # @return [String, nil]
71
+ # @api private
72
+ def os_architecture
73
+ ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
74
+ end
75
+
76
+ # Pointer width of the running Ruby, in bits.
77
+ #
78
+ # @return [Integer] 32 or 64
79
+ # @api private
80
+ def ruby_architecture_bits
81
+ RbConfig::SIZEOF.fetch("void*", 8) * 8
82
+ end
83
+
84
+ # Whether a 64-bit PowerShell is directly reachable.
85
+ #
86
+ # Always true for a remote host, where the local architecture is
87
+ # irrelevant.
88
+ #
89
+ # @return [Boolean]
90
+ # @api private
91
+ def sixty_four_bit?
32
92
  return true if remote_hyperv
33
93
 
34
- os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
35
- ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
36
- os_arch == "AMD64" && ruby_arch == 64
94
+ SIXTY_FOUR_BIT_ARCHITECTURES.include?(os_architecture) &&
95
+ ruby_architecture_bits == 64
96
+ end
97
+
98
+ # Whether both the OS and Ruby are 32-bit, so no WOW64 redirection is in
99
+ # play.
100
+ #
101
+ # @return [Boolean]
102
+ # @api private
103
+ def thirty_two_bit?
104
+ !SIXTY_FOUR_BIT_ARCHITECTURES.include?(os_architecture) &&
105
+ ruby_architecture_bits == 32
106
+ end
107
+
108
+ # @deprecated Use {#sixty_four_bit?}. Kept because this module is mixed
109
+ # into a published driver class.
110
+ # @return [Boolean]
111
+ # @api private
112
+ def is_64bit?
113
+ sixty_four_bit?
37
114
  end
38
115
 
116
+ # @deprecated Use {#thirty_two_bit?}. Kept because this module is mixed
117
+ # into a published driver class.
118
+ # @return [Boolean]
119
+ # @api private
39
120
  def is_32bit?
40
- os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
41
- ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
42
- os_arch != "AMD64" && ruby_arch == 32
121
+ thirty_two_bit?
43
122
  end
44
123
 
124
+ # Path to a PowerShell that can see the Hyper-V cmdlets.
125
+ #
126
+ # When a 32-bit Ruby runs on 64-bit Windows the WOW64 filesystem
127
+ # redirector rewrites `system32` to `SysWOW64`, which would launch a
128
+ # 32-bit PowerShell with no Hyper-V module. `sysnative` is the virtual
129
+ # path that escapes redirection.
130
+ #
131
+ # @return [String]
132
+ # @api private
45
133
  def powershell_64_bit
46
- if is_64bit? || is_32bit?
134
+ if sixty_four_bit? || thirty_two_bit?
47
135
  'c:\windows\system32\windowspowershell\v1.0\powershell.exe'
48
136
  else
49
137
  'c:\windows\sysnative\windowspowershell\v1.0\powershell.exe'
50
138
  end
51
139
  end
52
140
 
141
+ # Turn a script into a full `powershell.exe` command line.
142
+ #
143
+ # Prepends a dot-source of the support script so the helper functions are
144
+ # defined, then encodes the result.
145
+ #
146
+ # @param script [String] PowerShell source
147
+ # @return [String] the command line to hand to the connection
148
+ # @api private
53
149
  def wrap_command(script)
54
150
  debug("Loading functions from #{base_script_path}")
55
151
  new_script = [ ". #{base_script_path}", "#{script}" ].join(";\n")
@@ -58,11 +154,16 @@ module Kitchen
58
154
  " -encodedcommand #{encode_command new_script} -outputformat Text"
59
155
  end
60
156
 
61
- # Convenience method to run a powershell command locally.
157
+ # Run a PowerShell script on the Hyper-V host.
62
158
  #
63
- # @param cmd [String] command to run locally
64
- # @param options [Hash] options hash
65
- # @see Kitchen::ShellOut.run_command
159
+ # With `dry_run` set the script is echoed rather than executed, which is
160
+ # the quickest way to see exactly what the driver would have run.
161
+ #
162
+ # @param cmd [String] PowerShell source
163
+ # @param options [Hash] options passed through to the Train connection
164
+ # @return [Hash, Array, nil] the parsed JSON output, or nil when the
165
+ # script produced none
166
+ # @raise [RuntimeError] if the script exits non-zero
66
167
  # @api private
67
168
  def run_ps(cmd, options = {})
68
169
  cmd = "echo #{cmd}" if config[:dry_run]
@@ -72,6 +173,14 @@ module Kitchen
72
173
  execute_command wrapped_command, options
73
174
  end
74
175
 
176
+ # Run a prepared command line and parse its output.
177
+ #
178
+ # @param cmd [String] the full command line from {#wrap_command}
179
+ # @param options [Hash] options passed through to the Train connection
180
+ # @return [Hash, Array, nil] the parsed JSON output, or nil when the
181
+ # script produced none
182
+ # @raise [RuntimeError] if the command exits non-zero
183
+ # @api private
75
184
  def execute_command(cmd, options = {})
76
185
  debug("#Command BEGIN (#{cmd})")
77
186
 
@@ -87,10 +196,21 @@ module Kitchen
87
196
  JSON.parse(stdout) if stdout.length > 2
88
197
  end
89
198
 
199
+ # Strip the interactive prompt lines PowerShell interleaves with output,
200
+ # which would otherwise make the result invalid JSON.
201
+ #
202
+ # @param stdout [String] raw stdout from the host
203
+ # @return [String] stdout with prompt lines removed
204
+ # @api private
90
205
  def sanitize_stdout(stdout)
91
206
  stdout.split("\n").select { |s| !s.start_with?("PS") }.join("\n")
92
207
  end
93
208
 
209
+ # Script that clones the parent VHD into this instance's differencing
210
+ # disk.
211
+ #
212
+ # @return [String] PowerShell source
213
+ # @api private
94
214
  def new_differencing_disk_ps
95
215
  <<-DIFF
96
216
 
@@ -98,6 +218,12 @@ module Kitchen
98
218
  DIFF
99
219
  end
100
220
 
221
+ # Script that creates one additional data disk.
222
+ #
223
+ # @param disk_path [String] full path of the disk to create
224
+ # @param disk_size [Integer] size in gigabytes
225
+ # @return [String] PowerShell source
226
+ # @api private
101
227
  def new_additional_disk_ps(disk_path, disk_size)
102
228
  <<-ADDDISK
103
229
 
@@ -105,6 +231,10 @@ module Kitchen
105
231
  ADDDISK
106
232
  end
107
233
 
234
+ # Script that confirms the VM exists and starts it if it is stopped.
235
+ #
236
+ # @return [String] PowerShell source
237
+ # @api private
108
238
  def ensure_vm_running_ps
109
239
  <<-RUNNING
110
240
 
@@ -112,6 +242,10 @@ module Kitchen
112
242
  RUNNING
113
243
  end
114
244
 
245
+ # Script that creates the VM from the current configuration.
246
+ #
247
+ # @return [String] PowerShell source
248
+ # @api private
115
249
  def new_vm_ps
116
250
  <<-NEWVM
117
251
 
@@ -137,6 +271,14 @@ module Kitchen
137
271
  NEWVM
138
272
  end
139
273
 
274
+ # The `AdditionalDisks` entry spliced into {#new_vm_ps}.
275
+ #
276
+ # Reads the paths {Hyperv#create_additional_disks} recorded, so it is only
277
+ # meaningful after that has run.
278
+ #
279
+ # @return [String, nil] the parameter line, or nil when no additional
280
+ # disks are configured
281
+ # @api private
140
282
  def additional_disks
141
283
  return if config[:additional_disks].nil?
142
284
 
@@ -145,7 +287,11 @@ module Kitchen
145
287
  EOH
146
288
  end
147
289
 
148
- # TODO: Report if VM has no IP address instead of silently waiting forever
290
+ # Script that reads the VM's name, id and IP address.
291
+ #
292
+ # @return [String] PowerShell source
293
+ # @api private
294
+ # @todo Report if VM has no IP address instead of silently waiting forever
149
295
  def vm_details_ps
150
296
  <<-DETAILS
151
297
 
@@ -153,6 +299,38 @@ module Kitchen
153
299
  DETAILS
154
300
  end
155
301
 
302
+ # Script that reads the VM's current power state without changing it.
303
+ #
304
+ # Unlike {#ensure_vm_running_ps}, this never starts a stopped VM, so it is
305
+ # safe for `kitchen list --probe`.
306
+ #
307
+ # @return [String] PowerShell source
308
+ # @api private
309
+ def vm_status_ps
310
+ <<-STATUS
311
+
312
+ Get-VmStatus -Id "#{@state[:id]}" | ConvertTo-Json
313
+ STATUS
314
+ end
315
+
316
+ # Script that reports whether the Hyper-V PowerShell module is installed.
317
+ #
318
+ # @return [String] PowerShell source
319
+ # @api private
320
+ def hyperv_module_ps
321
+ <<-MODULE
322
+
323
+ Get-Module -ListAvailable -Name Hyper-V |
324
+ Select-Object -First 1 |
325
+ ForEach-Object { [pscustomobject]@{ Name = $_.Name; Version = [string]$_.Version } } |
326
+ ConvertTo-Json
327
+ MODULE
328
+ end
329
+
330
+ # Script that forces the VM off and removes it.
331
+ #
332
+ # @return [String] PowerShell source
333
+ # @api private
156
334
  def delete_vm_ps
157
335
  <<-REMOVE
158
336
 
@@ -162,6 +340,10 @@ module Kitchen
162
340
  REMOVE
163
341
  end
164
342
 
343
+ # Script that assigns the VM a static address once its adapter is up.
344
+ #
345
+ # @return [String] PowerShell source
346
+ # @api private
165
347
  def set_vm_ipaddress_ps
166
348
  <<-VMIP
167
349
 
@@ -178,30 +360,55 @@ module Kitchen
178
360
  VMIP
179
361
  end
180
362
 
363
+ # Script that resolves the virtual switch to attach the VM to.
364
+ #
365
+ # @return [String] PowerShell source
366
+ # @api private
181
367
  def vm_default_switch_ps
182
368
  <<-VMSWITCH
183
369
  Get-DefaultVMSwitch "#{config[:vm_switch]}" | ConvertTo-Json
184
370
  VMSWITCH
185
371
  end
186
372
 
373
+ # Script that attaches the configured ISO to the VM's DVD drive.
374
+ #
375
+ # @return [String] PowerShell source
376
+ # @api private
187
377
  def mount_vm_iso
188
378
  <<-MOUNTISO
189
379
  mount-vmiso -id "#{@state[:id]}" -Path #{config[:iso_path]}
190
380
  MOUNTISO
191
381
  end
192
382
 
383
+ # Script that grows the parent VHD to the configured size.
384
+ #
385
+ # @return [String] PowerShell source
386
+ # @api private
193
387
  def resize_vhd
194
388
  <<-VMNOTE
195
389
  Resize-VHD -Path "#{parent_vhd_path}" -SizeBytes #{config[:resize_vhd]}
196
390
  VMNOTE
197
391
  end
198
392
 
393
+ # Script that writes the configured note onto the VM.
394
+ #
395
+ # @return [String] PowerShell source
396
+ # @api private
199
397
  def set_vm_note
200
398
  <<-VMNOTE
201
399
  Set-VM -Name (Get-VM | Where-Object{ $_.ID -eq "#{@state[:id]}"}).Name -Note "#{config[:vm_note]}"
202
400
  VMNOTE
203
401
  end
204
402
 
403
+ # Script that copies a file or directory into the running guest.
404
+ #
405
+ # Enables the guest service interface first if it is off, and walks a
406
+ # directory source file by file since `Copy-VMFile` handles only files.
407
+ #
408
+ # @param source [String] path on the Hyper-V host
409
+ # @param dest [String] path inside the guest
410
+ # @return [String] PowerShell source
411
+ # @api private
205
412
  def copy_vm_file_ps(source, dest)
206
413
  <<-FILECOPY
207
414
  Function CopyFile ($VM, [string]$SourcePath, [string]$DestPath) {
@@ -240,6 +447,11 @@ module Kitchen
240
447
 
241
448
  private
242
449
 
450
+ # Render a Ruby array as a PowerShell array literal.
451
+ #
452
+ # @param list [Array<String>, nil] the values
453
+ # @return [String] e.g. `@("8.8.8.8", "8.8.4.4")`, or `@()` when empty
454
+ # @api private
243
455
  def ruby_array_to_ps_array(list)
244
456
  return "@()" if list.nil? || list.empty?
245
457