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.
- checksums.yaml +4 -4
- data/Gemfile +15 -10
- data/Rakefile +42 -25
- data/kitchen-hyperv.gemspec +4 -1
- data/lib/kitchen/driver/hyperv.rb +354 -27
- data/lib/kitchen/driver/hyperv_version.rb +10 -1
- data/lib/kitchen/driver/powershell.rb +226 -14
- data/support/hyperv.ps1 +264 -233
- metadata +18 -4
|
@@ -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 "
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
157
|
+
# Run a PowerShell script on the Hyper-V host.
|
|
62
158
|
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
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
|
-
#
|
|
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
|
|