kitchen-hyperv 0.10.2 → 0.11.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 +5 -2
- data/lib/kitchen/driver/hyperv.rb +243 -25
- data/lib/kitchen/driver/hyperv_version.rb +8 -1
- data/lib/kitchen/driver/powershell.rb +146 -5
- metadata +32 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c1c78c069680ce6179d290ba250302102bd29da553842ecc3f00e7ddb4114c5
|
|
4
|
+
data.tar.gz: 05413cbc517eebed2176b0ab9a20f86e742ee2dcf14a58e8babf0a774a64fedd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 723997d9f09f593d941e7ee46cca5a9ec19577142cb99148e7cebc6f9839395ddb06e6f681ed470522753aff12c088ea903440d186f6cd201d7f41616633331f
|
|
7
|
+
data.tar.gz: f87621dfb332510fa30f6c8fee49a4fa23186c6bf1ae9597fce4cca8467afa8e1387bc5c4a2a3931ad28a20e15475fb6755f747137688a9bd3263b944b8ac48a
|
data/Gemfile
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
source "https://rubygems.org"
|
|
2
2
|
|
|
3
|
-
gemspec
|
|
3
|
+
gemspec development_group: :test
|
|
4
4
|
|
|
5
|
+
# Everything needed to run `rake spec` and `rake style`. Deliberately small:
|
|
6
|
+
# the unit suite fakes the Train connection, so it needs neither Hyper-V nor
|
|
7
|
+
# any other Test Kitchen plugin to run on any platform.
|
|
5
8
|
group :test do
|
|
9
|
+
gem "rake"
|
|
10
|
+
gem "rspec", "~> 3.13"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Only needed to drive a real VM via `kitchen test` against a Hyper-V host.
|
|
14
|
+
# Skip with: bundle config set --local without integration
|
|
15
|
+
group :integration do
|
|
6
16
|
gem "berkshelf"
|
|
7
|
-
gem "kitchen-inspec"
|
|
8
17
|
gem "kitchen-dokken"
|
|
18
|
+
gem "kitchen-inspec"
|
|
9
19
|
gem "kitchen-vagrant"
|
|
10
|
-
gem "rake"
|
|
11
|
-
gem "minitest", "~> 6.0", "< 6.1"
|
|
12
|
-
gem "minitest-stub-const"
|
|
13
|
-
gem "mocha", "~> 3.0"
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
group :development do
|
|
@@ -19,11 +25,10 @@ group :development do
|
|
|
19
25
|
gem "pry-stack_explorer"
|
|
20
26
|
end
|
|
21
27
|
|
|
22
|
-
group :chefstyle do
|
|
23
|
-
gem "chefstyle"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
28
|
group :docs do
|
|
27
29
|
gem "yard"
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
group :cookstyle do
|
|
33
|
+
gem "cookstyle"
|
|
34
|
+
end
|
data/Rakefile
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
require "bundler/gem_tasks"
|
|
2
|
-
require "kitchen/driver/hyperv_version"
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
t.verbose = true
|
|
9
|
-
end
|
|
3
|
+
begin
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
10
7
|
|
|
11
|
-
desc "Run
|
|
12
|
-
task
|
|
8
|
+
desc "Run the unit tests"
|
|
9
|
+
task unit: :spec
|
|
10
|
+
|
|
11
|
+
desc "Run all test suites"
|
|
12
|
+
task test: :spec
|
|
13
|
+
rescue LoadError
|
|
14
|
+
puts "rspec is not available. (sudo) gem install rspec to run the unit tests."
|
|
15
|
+
end
|
|
13
16
|
|
|
14
17
|
begin
|
|
15
|
-
require "chefstyle"
|
|
18
|
+
require "cookstyle/chefstyle"
|
|
16
19
|
require "rubocop/rake_task"
|
|
17
20
|
RuboCop::RakeTask.new(:style) do |task|
|
|
18
21
|
task.options += ["--display-cop-names", "--no-color"]
|
|
19
22
|
end
|
|
20
23
|
rescue LoadError
|
|
21
|
-
puts "chefstyle is not available. (sudo) gem install
|
|
24
|
+
puts "cookstyle/chefstyle is not available. (sudo) gem install cookstyle to do style checking."
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
desc "Run all quality tasks"
|
|
@@ -26,23 +29,37 @@ task quality: :style
|
|
|
26
29
|
|
|
27
30
|
begin
|
|
28
31
|
require "yard" unless defined?(YARD)
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
YARD::Rake::YardocTask.new(:yard) do |task|
|
|
34
|
+
task.stats_options = ["--list-undoc"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
namespace :yard do
|
|
38
|
+
desc "Report documentation coverage and list undocumented objects"
|
|
39
|
+
task :stats do
|
|
40
|
+
sh "yard stats --list-undoc"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc "Serve the documentation at http://localhost:8808, reloading on change"
|
|
44
|
+
task :server do
|
|
45
|
+
sh "yard server --reload"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
30
48
|
rescue LoadError
|
|
31
49
|
puts "yard is not available. (sudo) gem install yard to generate yard documentation."
|
|
32
50
|
end
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
|
|
39
|
-
config.future_release = "v#{Kitchen::Driver::HYPERV_VERSION}"
|
|
40
|
-
config.issues = false
|
|
41
|
-
config.pulls = true
|
|
42
|
-
config.user = "test-kitchen"
|
|
43
|
-
config.project = "kitchen-hyperv"
|
|
52
|
+
desc "Run the PowerShell (Pester) tests for support/hyperv.ps1"
|
|
53
|
+
task :pester do
|
|
54
|
+
pwsh = %w{pwsh powershell}.find do |candidate|
|
|
55
|
+
system("which #{candidate} > /dev/null 2>&1") || system("where #{candidate} > NUL 2>&1")
|
|
44
56
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
|
|
58
|
+
abort "PowerShell was not found on PATH; install PowerShell 7+ to run the Pester tests." unless pwsh
|
|
59
|
+
|
|
60
|
+
sh pwsh, "-NoProfile", "-NonInteractive", "-File", File.join(__dir__, "spec", "powershell", "run_tests.ps1")
|
|
48
61
|
end
|
|
62
|
+
|
|
63
|
+
# `yard` is deliberately not a prerequisite of `default`: documentation coverage
|
|
64
|
+
# should never be able to fail a build.
|
|
65
|
+
task default: %i{test quality}
|
data/kitchen-hyperv.gemspec
CHANGED
|
@@ -18,7 +18,10 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
|
|
19
19
|
spec.required_ruby_version = ">= 3.1"
|
|
20
20
|
|
|
21
|
+
# Required directly by the PowerShell command encoder. base64 is a bundled
|
|
22
|
+
# gem from Ruby 3.4 on, so it has to be declared rather than assumed.
|
|
23
|
+
spec.add_dependency "base64", "~> 0.2"
|
|
21
24
|
spec.add_dependency "test-kitchen", ">= 1.4", "< 5"
|
|
22
|
-
spec.add_dependency "train", "
|
|
23
|
-
spec.add_dependency "train-winrm", "
|
|
25
|
+
spec.add_dependency "train", ">= 3.5", "< 4.0"
|
|
26
|
+
spec.add_dependency "train-winrm", ">= 0.2", "< 1.0"
|
|
24
27
|
end
|
|
@@ -28,9 +28,26 @@ require "train-winrm" unless defined?(TrainPlugins::WinRM)
|
|
|
28
28
|
|
|
29
29
|
module Kitchen
|
|
30
30
|
|
|
31
|
+
# Test Kitchen driver plugins.
|
|
31
32
|
module Driver
|
|
32
33
|
|
|
33
|
-
#
|
|
34
|
+
# Test Kitchen driver that builds instances as Hyper-V virtual machines.
|
|
35
|
+
#
|
|
36
|
+
# The driver never talks to Hyper-V directly. It generates PowerShell that
|
|
37
|
+
# calls the helper functions in `support/hyperv.ps1` and runs that script
|
|
38
|
+
# through a Train connection -- a local one on a Hyper-V host, or WinRM
|
|
39
|
+
# when `hyperv_server` points at a remote host.
|
|
40
|
+
#
|
|
41
|
+
# Each instance gets a differencing disk cloned from a shared parent VHD,
|
|
42
|
+
# so creating an instance costs a few seconds and very little disk.
|
|
43
|
+
#
|
|
44
|
+
# @example Minimal kitchen.yml
|
|
45
|
+
# driver:
|
|
46
|
+
# name: hyperv
|
|
47
|
+
# parent_vhd_folder: C:\VHDs
|
|
48
|
+
# parent_vhd_name: windows-2022.vhdx
|
|
49
|
+
#
|
|
50
|
+
# @see https://github.com/test-kitchen/kitchen-hyperv
|
|
34
51
|
class Hyperv < Kitchen::Driver::Base
|
|
35
52
|
|
|
36
53
|
kitchen_driver_api_version 2
|
|
@@ -72,6 +89,17 @@ module Kitchen
|
|
|
72
89
|
|
|
73
90
|
include Kitchen::Driver::PowerShellScripts
|
|
74
91
|
|
|
92
|
+
# Create the virtual machine and wait until it is reachable.
|
|
93
|
+
#
|
|
94
|
+
# Runs the full bring-up in order: validate the configuration, clone the
|
|
95
|
+
# parent VHD into a differencing disk, create any additional data disks,
|
|
96
|
+
# create and start the VM, then block on the transport until the guest
|
|
97
|
+
# accepts connections.
|
|
98
|
+
#
|
|
99
|
+
# @param state [Hash] the instance state hash, updated in place with
|
|
100
|
+
# `:id`, `:hostname` and `:vm_name`
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @raise [RuntimeError] if validation fails or Hyper-V cannot create the VM
|
|
75
103
|
def create(state)
|
|
76
104
|
@state = state
|
|
77
105
|
validate_vm_settings
|
|
@@ -86,12 +114,25 @@ module Kitchen
|
|
|
86
114
|
info("Hyper-V instance #{instance.to_str} created.")
|
|
87
115
|
end
|
|
88
116
|
|
|
117
|
+
# Destroy the virtual machine and the disks created alongside it.
|
|
118
|
+
#
|
|
119
|
+
# Safe to call repeatedly and safe to call when the VM was removed out of
|
|
120
|
+
# band: a differencing disk left behind by a partial create is cleaned up
|
|
121
|
+
# even when no VM exists.
|
|
122
|
+
#
|
|
123
|
+
# @param state [Hash] the instance state hash; `:id` is deleted from it
|
|
124
|
+
# @return [void]
|
|
89
125
|
def destroy(state)
|
|
90
126
|
@state = state
|
|
91
127
|
if differencing_disk_exists && !vm_exists_silent
|
|
92
128
|
remove_differencing_disk
|
|
93
129
|
end
|
|
94
|
-
|
|
130
|
+
unless vm_exists
|
|
131
|
+
# The VM is gone, but a stale id would make every later run believe
|
|
132
|
+
# otherwise, so clear it rather than returning with it still in place.
|
|
133
|
+
state.delete(:id)
|
|
134
|
+
return
|
|
135
|
+
end
|
|
95
136
|
|
|
96
137
|
instance.transport.connection(state).close
|
|
97
138
|
remove_virtual_machine
|
|
@@ -103,14 +144,24 @@ module Kitchen
|
|
|
103
144
|
|
|
104
145
|
private
|
|
105
146
|
|
|
147
|
+
# Check the configuration before anything is created.
|
|
148
|
+
#
|
|
149
|
+
# Also resolves `vm_switch`, which requires a round trip to the host and
|
|
150
|
+
# so cannot be handled by a plain `default_config` block.
|
|
151
|
+
#
|
|
152
|
+
# @return [void]
|
|
153
|
+
# @raise [RuntimeError] if the parent VHD is missing, the startup memory
|
|
154
|
+
# falls outside the dynamic memory range, or the VLAN id is not a valid
|
|
155
|
+
# 802.1Q id
|
|
156
|
+
# @api private
|
|
106
157
|
def validate_vm_settings
|
|
107
158
|
raise "Missing parent_vhd_folder" unless vhd_folder? || remote_hyperv
|
|
108
159
|
raise "Missing parent_vhd_name" unless vhd? || remote_hyperv
|
|
109
160
|
|
|
110
161
|
if config[:dynamic_memory]
|
|
111
|
-
startup_bytes =
|
|
112
|
-
min =
|
|
113
|
-
max =
|
|
162
|
+
startup_bytes = integer_config(:memory_startup_bytes)
|
|
163
|
+
min = integer_config(:dynamic_memory_min_bytes)
|
|
164
|
+
max = integer_config(:dynamic_memory_max_bytes)
|
|
114
165
|
memory_valid = startup_bytes.between?(min, max)
|
|
115
166
|
warning = "memory_startup_bytes (#{startup_bytes}) must" \
|
|
116
167
|
" fall within dynamic memory range (#{min}-#{max})"
|
|
@@ -118,7 +169,7 @@ module Kitchen
|
|
|
118
169
|
end
|
|
119
170
|
config[:vm_switch] = vm_switch
|
|
120
171
|
if config[:vm_vlan_id]
|
|
121
|
-
vm_vlan_id =
|
|
172
|
+
vm_vlan_id = integer_config(:vm_vlan_id)
|
|
122
173
|
vm_vlan_id_min = 1
|
|
123
174
|
vm_vlan_id_max = 4094
|
|
124
175
|
vm_vlan_id_valid = vm_vlan_id.between?(vm_vlan_id_min, vm_vlan_id_max)
|
|
@@ -128,6 +179,10 @@ module Kitchen
|
|
|
128
179
|
end
|
|
129
180
|
end
|
|
130
181
|
|
|
182
|
+
# Clone the parent VHD into this instance's differencing disk.
|
|
183
|
+
#
|
|
184
|
+
# @return [void]
|
|
185
|
+
# @api private
|
|
131
186
|
def create_new_differencing_disk
|
|
132
187
|
info("Creating differencing disk for #{instance.name}.")
|
|
133
188
|
run_ps new_differencing_disk_ps
|
|
@@ -135,6 +190,15 @@ module Kitchen
|
|
|
135
190
|
set_new_vhd_size
|
|
136
191
|
end
|
|
137
192
|
|
|
193
|
+
# Create each disk described by the `additional_disks` config.
|
|
194
|
+
#
|
|
195
|
+
# Records the created paths in `@additional_disk_objects` so
|
|
196
|
+
# {PowerShellScripts#new_vm_ps} can attach them to the new VM.
|
|
197
|
+
#
|
|
198
|
+
# @return [void]
|
|
199
|
+
# @raise [RuntimeError] if a disk entry has no name, or the target file
|
|
200
|
+
# already exists
|
|
201
|
+
# @api private
|
|
138
202
|
def create_additional_disks
|
|
139
203
|
return if config[:additional_disks].nil?
|
|
140
204
|
|
|
@@ -154,6 +218,14 @@ module Kitchen
|
|
|
154
218
|
end
|
|
155
219
|
end
|
|
156
220
|
|
|
221
|
+
# Resolve the virtual switch to attach the VM to.
|
|
222
|
+
#
|
|
223
|
+
# With `vm_switch` unset the host picks its first switch; with it set the
|
|
224
|
+
# host confirms that switch exists.
|
|
225
|
+
#
|
|
226
|
+
# @return [String] the switch name
|
|
227
|
+
# @raise [RuntimeError] if the host reports no usable switch
|
|
228
|
+
# @api private
|
|
157
229
|
def vm_switch
|
|
158
230
|
default_switch_object = run_ps vm_default_switch_ps
|
|
159
231
|
if default_switch_object.nil? ||
|
|
@@ -165,6 +237,11 @@ module Kitchen
|
|
|
165
237
|
default_switch_object["Name"]
|
|
166
238
|
end
|
|
167
239
|
|
|
240
|
+
# Create and start the VM, unless one already exists for this instance.
|
|
241
|
+
#
|
|
242
|
+
# @return [void]
|
|
243
|
+
# @raise [RuntimeError] if the host returns no VM
|
|
244
|
+
# @api private
|
|
168
245
|
def create_virtual_machine
|
|
169
246
|
return if vm_exists
|
|
170
247
|
|
|
@@ -176,18 +253,34 @@ module Kitchen
|
|
|
176
253
|
info("Created virtual machine for #{instance.name}.")
|
|
177
254
|
end
|
|
178
255
|
|
|
256
|
+
# Copy the VM's id, address and name into the instance state.
|
|
257
|
+
#
|
|
258
|
+
# @return [void]
|
|
259
|
+
# @raise [RuntimeError] if the host reports no detail for the VM
|
|
260
|
+
# @api private
|
|
179
261
|
def update_state
|
|
180
262
|
vm_details
|
|
263
|
+
raise "Unable to fetch details for virtual machine #{instance.name}." if @vm.nil?
|
|
264
|
+
|
|
181
265
|
@state[:id] = @vm["Id"]
|
|
182
266
|
@state[:hostname] = @vm["IpAddress"]
|
|
183
267
|
@state[:vm_name] = @vm["Name"]
|
|
184
268
|
end
|
|
185
269
|
|
|
270
|
+
# Fetch the VM's details from the host, applying a static IP first if one
|
|
271
|
+
# is configured.
|
|
272
|
+
#
|
|
273
|
+
# @return [Hash, nil] the parsed `Get-VmDetail` payload
|
|
274
|
+
# @api private
|
|
186
275
|
def vm_details
|
|
187
276
|
run_ps set_vm_ipaddress_ps if config[:ip_address]
|
|
188
277
|
@vm = run_ps vm_details_ps
|
|
189
278
|
end
|
|
190
279
|
|
|
280
|
+
# Attach the configured ISO to the VM's DVD drive.
|
|
281
|
+
#
|
|
282
|
+
# @return [void]
|
|
283
|
+
# @api private
|
|
191
284
|
def mount_virtual_machine_iso
|
|
192
285
|
return unless config[:iso_path]
|
|
193
286
|
|
|
@@ -196,6 +289,10 @@ module Kitchen
|
|
|
196
289
|
info("Done mounting #{config[:iso_path]}")
|
|
197
290
|
end
|
|
198
291
|
|
|
292
|
+
# Grow the disk to `resize_vhd` bytes, when configured.
|
|
293
|
+
#
|
|
294
|
+
# @return [void]
|
|
295
|
+
# @api private
|
|
199
296
|
def set_new_vhd_size
|
|
200
297
|
return unless config[:resize_vhd]
|
|
201
298
|
|
|
@@ -204,6 +301,11 @@ module Kitchen
|
|
|
204
301
|
info("Resized differencing disk for #{instance.name}.")
|
|
205
302
|
end
|
|
206
303
|
|
|
304
|
+
# Write the configured note onto the VM, so it is identifiable in the
|
|
305
|
+
# Hyper-V manager.
|
|
306
|
+
#
|
|
307
|
+
# @return [void]
|
|
308
|
+
# @api private
|
|
207
309
|
def set_virtual_machine_note
|
|
208
310
|
return unless config[:vm_note]
|
|
209
311
|
|
|
@@ -211,6 +313,13 @@ module Kitchen
|
|
|
211
313
|
run_ps set_vm_note
|
|
212
314
|
end
|
|
213
315
|
|
|
316
|
+
# Copy the configured files into the running guest.
|
|
317
|
+
#
|
|
318
|
+
# Requires the guest service interface, which `enable_guest_services`
|
|
319
|
+
# turns on.
|
|
320
|
+
#
|
|
321
|
+
# @return [void]
|
|
322
|
+
# @api private
|
|
214
323
|
def copy_vm_files
|
|
215
324
|
return if config[:copy_vm_files].nil?
|
|
216
325
|
|
|
@@ -221,6 +330,10 @@ module Kitchen
|
|
|
221
330
|
info("Copied files to virtual machine")
|
|
222
331
|
end
|
|
223
332
|
|
|
333
|
+
# Whether a VM for this instance exists, starting it if it is stopped.
|
|
334
|
+
#
|
|
335
|
+
# @return [Boolean]
|
|
336
|
+
# @api private
|
|
224
337
|
def vm_exists
|
|
225
338
|
info("Checking for existing virtual machine.")
|
|
226
339
|
return false unless @state.key?(:id) && !@state[:id].nil?
|
|
@@ -228,12 +341,18 @@ module Kitchen
|
|
|
228
341
|
existing_vm = run_ps ensure_vm_running_ps
|
|
229
342
|
return false if existing_vm.nil? || existing_vm["Id"].nil?
|
|
230
343
|
|
|
231
|
-
info("Found an
|
|
344
|
+
info("Found an existing VM with an ID: #{existing_vm["Id"]}")
|
|
232
345
|
true
|
|
233
346
|
end
|
|
234
347
|
|
|
235
|
-
#
|
|
236
|
-
#
|
|
348
|
+
# {#vm_exists} without the logging.
|
|
349
|
+
#
|
|
350
|
+
# `destroy` checks for a VM twice -- once to decide whether a leftover
|
|
351
|
+
# differencing disk is stale, once to decide whether to remove the VM --
|
|
352
|
+
# and logging both checks makes it look like the driver ran twice.
|
|
353
|
+
#
|
|
354
|
+
# @return [Boolean]
|
|
355
|
+
# @api private
|
|
237
356
|
def vm_exists_silent
|
|
238
357
|
return false unless @state.key?(:id) && !@state[:id].nil?
|
|
239
358
|
|
|
@@ -243,18 +362,45 @@ module Kitchen
|
|
|
243
362
|
true
|
|
244
363
|
end
|
|
245
364
|
|
|
365
|
+
# Whether this instance's differencing disk is on disk.
|
|
366
|
+
#
|
|
367
|
+
# @return [Boolean]
|
|
368
|
+
# @api private
|
|
246
369
|
def differencing_disk_exists
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
370
|
+
File.exist?(differencing_disk_path)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Read a config value that must be numeric.
|
|
374
|
+
#
|
|
375
|
+
# Values coming from `kitchen.yml` may be quoted, and comparing a String
|
|
376
|
+
# against an Integer raises deep inside Comparable rather than reporting
|
|
377
|
+
# anything a user can act on.
|
|
378
|
+
#
|
|
379
|
+
# @param key [Symbol] the config key
|
|
380
|
+
# @return [Integer]
|
|
381
|
+
# @raise [RuntimeError] if the value is not coercible to an Integer
|
|
382
|
+
# @api private
|
|
383
|
+
def integer_config(key)
|
|
384
|
+
value = config[key]
|
|
385
|
+
Integer(value)
|
|
386
|
+
rescue ArgumentError, TypeError
|
|
387
|
+
raise "#{key} (#{value.inspect}) must be an integer"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Force the VM off and remove it.
|
|
391
|
+
#
|
|
392
|
+
# @return [void]
|
|
393
|
+
# @api private
|
|
252
394
|
def remove_virtual_machine
|
|
253
395
|
info("Deleting virtual machine for #{instance.name}")
|
|
254
396
|
run_ps delete_vm_ps
|
|
255
397
|
info("Deleted virtual machine for #{instance.name}")
|
|
256
398
|
end
|
|
257
399
|
|
|
400
|
+
# Delete this instance's differencing disk, if present.
|
|
401
|
+
#
|
|
402
|
+
# @return [void]
|
|
403
|
+
# @api private
|
|
258
404
|
def remove_differencing_disk
|
|
259
405
|
return unless differencing_disk_exists
|
|
260
406
|
|
|
@@ -263,6 +409,11 @@ module Kitchen
|
|
|
263
409
|
info("Removed the differencing disk for #{instance.name}.")
|
|
264
410
|
end
|
|
265
411
|
|
|
412
|
+
# Delete every configured additional disk that exists.
|
|
413
|
+
#
|
|
414
|
+
# @return [void]
|
|
415
|
+
# @raise [RuntimeError] if a disk entry has no name
|
|
416
|
+
# @api private
|
|
266
417
|
def remove_additional_disks
|
|
267
418
|
return if config[:additional_disks].nil?
|
|
268
419
|
|
|
@@ -279,74 +430,141 @@ module Kitchen
|
|
|
279
430
|
end
|
|
280
431
|
end
|
|
281
432
|
|
|
433
|
+
# Where this instance's files live under the local kitchen root.
|
|
434
|
+
#
|
|
435
|
+
# @return [String]
|
|
436
|
+
# @api private
|
|
282
437
|
def kitchen_vm_path
|
|
283
438
|
@kitchen_vm_path ||= File.join(config[:kitchen_root], ".kitchen/#{instance.name}")
|
|
284
439
|
end
|
|
285
440
|
|
|
441
|
+
# Where this instance's files live on a remote Hyper-V host.
|
|
442
|
+
#
|
|
443
|
+
# @return [String]
|
|
444
|
+
# @api private
|
|
286
445
|
def remote_kitchen_vm_path
|
|
287
446
|
config[:remote_vm_path]
|
|
288
447
|
end
|
|
289
448
|
|
|
449
|
+
# Directory holding every disk belonging to this instance.
|
|
450
|
+
#
|
|
451
|
+
# A remote host cannot see the local kitchen root, so a remote run keeps
|
|
452
|
+
# its disks under `remote_vm_path` instead.
|
|
453
|
+
#
|
|
454
|
+
# @return [String]
|
|
455
|
+
# @api private
|
|
456
|
+
def vm_base_path
|
|
457
|
+
remote_hyperv ? remote_kitchen_vm_path : kitchen_vm_path
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# ISO to boot the VM from, for building an instance from install media.
|
|
461
|
+
#
|
|
462
|
+
# @return [String, nil]
|
|
463
|
+
# @api private
|
|
290
464
|
def boot_iso_path
|
|
291
465
|
@boot_iso_path ||= config[:boot_iso_path]
|
|
292
466
|
end
|
|
293
467
|
|
|
468
|
+
# Full path to this instance's differencing disk.
|
|
469
|
+
#
|
|
470
|
+
# @return [String]
|
|
471
|
+
# @api private
|
|
294
472
|
def differencing_disk_path
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
@differencing_disk_path ||= File.join(kitchen_vm_base, "diff" + "#{config[:disk_type]}")
|
|
473
|
+
@differencing_disk_path ||= File.join(vm_base_path, "diff#{config[:disk_type]}")
|
|
298
474
|
end
|
|
299
475
|
|
|
476
|
+
# Full path to one additional data disk.
|
|
477
|
+
#
|
|
478
|
+
# @param disk_name [String] the disk's configured name
|
|
479
|
+
# @param disk_type [String] the file extension, e.g. `".vhdx"`
|
|
480
|
+
# @return [String]
|
|
481
|
+
# @api private
|
|
300
482
|
def additional_disk_path(disk_name, disk_type)
|
|
301
|
-
File.join(
|
|
483
|
+
File.join(vm_base_path, disk_name + disk_type)
|
|
302
484
|
end
|
|
303
485
|
|
|
486
|
+
# Full path to the parent VHD every instance is cloned from.
|
|
487
|
+
#
|
|
488
|
+
# @return [String]
|
|
489
|
+
# @api private
|
|
304
490
|
def parent_vhd_path
|
|
305
491
|
@parent_vhd_path ||= File.join(config[:parent_vhd_folder], config[:parent_vhd_name])
|
|
306
492
|
end
|
|
307
493
|
|
|
494
|
+
# Whether the configured parent VHD folder exists locally.
|
|
495
|
+
#
|
|
496
|
+
# @return [Boolean]
|
|
497
|
+
# @api private
|
|
308
498
|
def vhd_folder?
|
|
309
499
|
config[:parent_vhd_folder] && Dir.exist?(config[:parent_vhd_folder])
|
|
310
500
|
end
|
|
311
501
|
|
|
502
|
+
# Whether the configured parent VHD file exists locally.
|
|
503
|
+
#
|
|
504
|
+
# @return [Boolean]
|
|
505
|
+
# @api private
|
|
312
506
|
def vhd?
|
|
313
507
|
config[:parent_vhd_name] && File.exist?(parent_vhd_path)
|
|
314
508
|
end
|
|
315
509
|
|
|
510
|
+
# Whether the driver is targeting a remote Hyper-V host over WinRM.
|
|
511
|
+
#
|
|
512
|
+
# @return [Boolean]
|
|
513
|
+
# @api private
|
|
316
514
|
def remote_hyperv
|
|
317
515
|
!!config[:hyperv_server]
|
|
318
516
|
end
|
|
319
517
|
|
|
518
|
+
# The Train connection commands run over, opened on first use.
|
|
519
|
+
#
|
|
520
|
+
# Uses the `local` backend on a Hyper-V host and `winrm` when
|
|
521
|
+
# `hyperv_server` is set, uploading the support script in that case.
|
|
522
|
+
#
|
|
523
|
+
# @return [Train::Plugins::Transport::BaseConnection]
|
|
524
|
+
# @api private
|
|
320
525
|
def connection
|
|
321
526
|
return @connection if @connection
|
|
322
527
|
|
|
323
528
|
backend = remote_hyperv ? "winrm" : "local"
|
|
324
529
|
|
|
325
530
|
train = Train.create(backend, {
|
|
326
|
-
host:
|
|
327
|
-
user:
|
|
328
|
-
password:
|
|
329
|
-
ssl:
|
|
531
|
+
host: config[:hyperv_server],
|
|
532
|
+
user: config[:hyperv_username],
|
|
533
|
+
password: config[:hyperv_password],
|
|
534
|
+
ssl: config[:hyperv_ssl],
|
|
330
535
|
self_signed: config[:hyperv_insecure],
|
|
331
536
|
})
|
|
332
537
|
@connection = train.connection
|
|
333
538
|
|
|
334
|
-
#
|
|
335
|
-
|
|
539
|
+
# Only the remote backend dot-sources the uploaded copy; locally the
|
|
540
|
+
# driver reads the script straight out of the gem.
|
|
541
|
+
@connection.upload(local_script_path, remote_script_path) if remote_hyperv
|
|
336
542
|
|
|
337
543
|
@connection
|
|
338
544
|
end
|
|
339
545
|
|
|
546
|
+
# Path every generated script dot-sources to load the helper functions.
|
|
547
|
+
#
|
|
548
|
+
# @return [String]
|
|
549
|
+
# @api private
|
|
340
550
|
def base_script_path
|
|
341
551
|
return remote_script_path if remote_hyperv
|
|
342
552
|
|
|
343
553
|
local_script_path
|
|
344
554
|
end
|
|
345
555
|
|
|
556
|
+
# Path to `support/hyperv.ps1` inside the installed gem.
|
|
557
|
+
#
|
|
558
|
+
# @return [String]
|
|
559
|
+
# @api private
|
|
346
560
|
def local_script_path
|
|
347
|
-
File.
|
|
561
|
+
File.expand_path("../../../support/hyperv.ps1", __dir__)
|
|
348
562
|
end
|
|
349
563
|
|
|
564
|
+
# Path the support script is uploaded to on a remote host.
|
|
565
|
+
#
|
|
566
|
+
# @return [String]
|
|
567
|
+
# @api private
|
|
350
568
|
def remote_script_path
|
|
351
569
|
File.join(config[:kitchen_root], "kitchen-hyperv", "hyperv.ps1")
|
|
352
570
|
end
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
module Kitchen
|
|
19
19
|
module Driver
|
|
20
|
-
|
|
20
|
+
# Version of the kitchen-hyperv gem, reported by `kitchen diagnose` as the
|
|
21
|
+
# driver's plugin version.
|
|
22
|
+
#
|
|
23
|
+
# Kept in its own file so the gemspec can read it without loading the
|
|
24
|
+
# driver, and therefore without loading test-kitchen.
|
|
25
|
+
#
|
|
26
|
+
# @return [String] a frozen semantic version
|
|
27
|
+
HYPERV_VERSION = "0.11.0".freeze
|
|
21
28
|
end
|
|
22
29
|
end
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# See the License for the specific language governing permissions and
|
|
16
16
|
# limitations under the License.
|
|
17
17
|
|
|
18
|
+
require "base64" unless defined?(Base64)
|
|
18
19
|
require "mixlib/shellout" unless defined?(Mixlib::ShellOut)
|
|
19
20
|
require "benchmark" unless defined?(Benchmark)
|
|
20
21
|
require "fileutils" unless defined?(FileUtils)
|
|
@@ -22,12 +23,42 @@ require "json" unless defined?(JSON)
|
|
|
22
23
|
|
|
23
24
|
module Kitchen
|
|
24
25
|
module Driver
|
|
26
|
+
# PowerShell generation and execution for {Kitchen::Driver::Hyperv}.
|
|
27
|
+
#
|
|
28
|
+
# Every method here is either a script generator -- a `*_ps` method
|
|
29
|
+
# returning PowerShell source -- or part of the pipeline that runs one:
|
|
30
|
+
# {#run_ps} wraps the script so it dot-sources `support/hyperv.ps1`,
|
|
31
|
+
# {#encode_command} encodes it for `powershell.exe -encodedcommand`, and
|
|
32
|
+
# {#execute_command} runs it over the Train connection and parses the JSON
|
|
33
|
+
# that comes back.
|
|
34
|
+
#
|
|
35
|
+
# Encoding sidesteps every layer of quoting between Ruby and PowerShell,
|
|
36
|
+
# which matters because these scripts embed Windows paths and user-supplied
|
|
37
|
+
# strings.
|
|
38
|
+
#
|
|
39
|
+
# The module reads `config`, `instance` and `@state` from the driver it is
|
|
40
|
+
# mixed into, so it is not usable standalone.
|
|
41
|
+
#
|
|
42
|
+
# @see Kitchen::Driver::Hyperv
|
|
25
43
|
module PowerShellScripts
|
|
44
|
+
# Encode a script the way `powershell.exe -encodedcommand` expects it:
|
|
45
|
+
# UTF-16LE, then Base64.
|
|
46
|
+
#
|
|
47
|
+
# @param script [String] UTF-8 PowerShell source
|
|
48
|
+
# @return [String] strict Base64, with no line breaks
|
|
49
|
+
# @api private
|
|
26
50
|
def encode_command(script)
|
|
27
51
|
encoded_script = script.encode("UTF-16LE", "UTF-8")
|
|
28
52
|
Base64.strict_encode64(encoded_script)
|
|
29
53
|
end
|
|
30
54
|
|
|
55
|
+
# Whether a 64-bit PowerShell is directly reachable.
|
|
56
|
+
#
|
|
57
|
+
# Always true for a remote host, where the local architecture is
|
|
58
|
+
# irrelevant.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
# @api private
|
|
31
62
|
def is_64bit?
|
|
32
63
|
return true if remote_hyperv
|
|
33
64
|
|
|
@@ -36,12 +67,26 @@ module Kitchen
|
|
|
36
67
|
os_arch == "AMD64" && ruby_arch == 64
|
|
37
68
|
end
|
|
38
69
|
|
|
70
|
+
# Whether both the OS and Ruby are 32-bit, so no WOW64 redirection is in
|
|
71
|
+
# play.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
# @api private
|
|
39
75
|
def is_32bit?
|
|
40
76
|
os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
|
|
41
77
|
ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
|
|
42
78
|
os_arch != "AMD64" && ruby_arch == 32
|
|
43
79
|
end
|
|
44
80
|
|
|
81
|
+
# Path to a PowerShell that can see the Hyper-V cmdlets.
|
|
82
|
+
#
|
|
83
|
+
# When a 32-bit Ruby runs on 64-bit Windows the WOW64 filesystem
|
|
84
|
+
# redirector rewrites `system32` to `SysWOW64`, which would launch a
|
|
85
|
+
# 32-bit PowerShell with no Hyper-V module. `sysnative` is the virtual
|
|
86
|
+
# path that escapes redirection.
|
|
87
|
+
#
|
|
88
|
+
# @return [String]
|
|
89
|
+
# @api private
|
|
45
90
|
def powershell_64_bit
|
|
46
91
|
if is_64bit? || is_32bit?
|
|
47
92
|
'c:\windows\system32\windowspowershell\v1.0\powershell.exe'
|
|
@@ -50,6 +95,14 @@ module Kitchen
|
|
|
50
95
|
end
|
|
51
96
|
end
|
|
52
97
|
|
|
98
|
+
# Turn a script into a full `powershell.exe` command line.
|
|
99
|
+
#
|
|
100
|
+
# Prepends a dot-source of the support script so the helper functions are
|
|
101
|
+
# defined, then encodes the result.
|
|
102
|
+
#
|
|
103
|
+
# @param script [String] PowerShell source
|
|
104
|
+
# @return [String] the command line to hand to the connection
|
|
105
|
+
# @api private
|
|
53
106
|
def wrap_command(script)
|
|
54
107
|
debug("Loading functions from #{base_script_path}")
|
|
55
108
|
new_script = [ ". #{base_script_path}", "#{script}" ].join(";\n")
|
|
@@ -58,11 +111,16 @@ module Kitchen
|
|
|
58
111
|
" -encodedcommand #{encode_command new_script} -outputformat Text"
|
|
59
112
|
end
|
|
60
113
|
|
|
61
|
-
#
|
|
114
|
+
# Run a PowerShell script on the Hyper-V host.
|
|
115
|
+
#
|
|
116
|
+
# With `dry_run` set the script is echoed rather than executed, which is
|
|
117
|
+
# the quickest way to see exactly what the driver would have run.
|
|
62
118
|
#
|
|
63
|
-
# @param cmd [String]
|
|
64
|
-
# @param options [Hash] options
|
|
65
|
-
# @
|
|
119
|
+
# @param cmd [String] PowerShell source
|
|
120
|
+
# @param options [Hash] options passed through to the Train connection
|
|
121
|
+
# @return [Hash, Array, nil] the parsed JSON output, or nil when the
|
|
122
|
+
# script produced none
|
|
123
|
+
# @raise [RuntimeError] if the script exits non-zero
|
|
66
124
|
# @api private
|
|
67
125
|
def run_ps(cmd, options = {})
|
|
68
126
|
cmd = "echo #{cmd}" if config[:dry_run]
|
|
@@ -72,6 +130,14 @@ module Kitchen
|
|
|
72
130
|
execute_command wrapped_command, options
|
|
73
131
|
end
|
|
74
132
|
|
|
133
|
+
# Run a prepared command line and parse its output.
|
|
134
|
+
#
|
|
135
|
+
# @param cmd [String] the full command line from {#wrap_command}
|
|
136
|
+
# @param options [Hash] options passed through to the Train connection
|
|
137
|
+
# @return [Hash, Array, nil] the parsed JSON output, or nil when the
|
|
138
|
+
# script produced none
|
|
139
|
+
# @raise [RuntimeError] if the command exits non-zero
|
|
140
|
+
# @api private
|
|
75
141
|
def execute_command(cmd, options = {})
|
|
76
142
|
debug("#Command BEGIN (#{cmd})")
|
|
77
143
|
|
|
@@ -87,10 +153,21 @@ module Kitchen
|
|
|
87
153
|
JSON.parse(stdout) if stdout.length > 2
|
|
88
154
|
end
|
|
89
155
|
|
|
156
|
+
# Strip the interactive prompt lines PowerShell interleaves with output,
|
|
157
|
+
# which would otherwise make the result invalid JSON.
|
|
158
|
+
#
|
|
159
|
+
# @param stdout [String] raw stdout from the host
|
|
160
|
+
# @return [String] stdout with prompt lines removed
|
|
161
|
+
# @api private
|
|
90
162
|
def sanitize_stdout(stdout)
|
|
91
163
|
stdout.split("\n").select { |s| !s.start_with?("PS") }.join("\n")
|
|
92
164
|
end
|
|
93
165
|
|
|
166
|
+
# Script that clones the parent VHD into this instance's differencing
|
|
167
|
+
# disk.
|
|
168
|
+
#
|
|
169
|
+
# @return [String] PowerShell source
|
|
170
|
+
# @api private
|
|
94
171
|
def new_differencing_disk_ps
|
|
95
172
|
<<-DIFF
|
|
96
173
|
|
|
@@ -98,6 +175,12 @@ module Kitchen
|
|
|
98
175
|
DIFF
|
|
99
176
|
end
|
|
100
177
|
|
|
178
|
+
# Script that creates one additional data disk.
|
|
179
|
+
#
|
|
180
|
+
# @param disk_path [String] full path of the disk to create
|
|
181
|
+
# @param disk_size [Integer] size in gigabytes
|
|
182
|
+
# @return [String] PowerShell source
|
|
183
|
+
# @api private
|
|
101
184
|
def new_additional_disk_ps(disk_path, disk_size)
|
|
102
185
|
<<-ADDDISK
|
|
103
186
|
|
|
@@ -105,6 +188,10 @@ module Kitchen
|
|
|
105
188
|
ADDDISK
|
|
106
189
|
end
|
|
107
190
|
|
|
191
|
+
# Script that confirms the VM exists and starts it if it is stopped.
|
|
192
|
+
#
|
|
193
|
+
# @return [String] PowerShell source
|
|
194
|
+
# @api private
|
|
108
195
|
def ensure_vm_running_ps
|
|
109
196
|
<<-RUNNING
|
|
110
197
|
|
|
@@ -112,6 +199,10 @@ module Kitchen
|
|
|
112
199
|
RUNNING
|
|
113
200
|
end
|
|
114
201
|
|
|
202
|
+
# Script that creates the VM from the current configuration.
|
|
203
|
+
#
|
|
204
|
+
# @return [String] PowerShell source
|
|
205
|
+
# @api private
|
|
115
206
|
def new_vm_ps
|
|
116
207
|
<<-NEWVM
|
|
117
208
|
|
|
@@ -137,6 +228,14 @@ module Kitchen
|
|
|
137
228
|
NEWVM
|
|
138
229
|
end
|
|
139
230
|
|
|
231
|
+
# The `AdditionalDisks` entry spliced into {#new_vm_ps}.
|
|
232
|
+
#
|
|
233
|
+
# Reads the paths {Hyperv#create_additional_disks} recorded, so it is only
|
|
234
|
+
# meaningful after that has run.
|
|
235
|
+
#
|
|
236
|
+
# @return [String, nil] the parameter line, or nil when no additional
|
|
237
|
+
# disks are configured
|
|
238
|
+
# @api private
|
|
140
239
|
def additional_disks
|
|
141
240
|
return if config[:additional_disks].nil?
|
|
142
241
|
|
|
@@ -145,7 +244,11 @@ module Kitchen
|
|
|
145
244
|
EOH
|
|
146
245
|
end
|
|
147
246
|
|
|
148
|
-
#
|
|
247
|
+
# Script that reads the VM's name, id and IP address.
|
|
248
|
+
#
|
|
249
|
+
# @return [String] PowerShell source
|
|
250
|
+
# @api private
|
|
251
|
+
# @todo Report if VM has no IP address instead of silently waiting forever
|
|
149
252
|
def vm_details_ps
|
|
150
253
|
<<-DETAILS
|
|
151
254
|
|
|
@@ -153,6 +256,10 @@ module Kitchen
|
|
|
153
256
|
DETAILS
|
|
154
257
|
end
|
|
155
258
|
|
|
259
|
+
# Script that forces the VM off and removes it.
|
|
260
|
+
#
|
|
261
|
+
# @return [String] PowerShell source
|
|
262
|
+
# @api private
|
|
156
263
|
def delete_vm_ps
|
|
157
264
|
<<-REMOVE
|
|
158
265
|
|
|
@@ -162,6 +269,10 @@ module Kitchen
|
|
|
162
269
|
REMOVE
|
|
163
270
|
end
|
|
164
271
|
|
|
272
|
+
# Script that assigns the VM a static address once its adapter is up.
|
|
273
|
+
#
|
|
274
|
+
# @return [String] PowerShell source
|
|
275
|
+
# @api private
|
|
165
276
|
def set_vm_ipaddress_ps
|
|
166
277
|
<<-VMIP
|
|
167
278
|
|
|
@@ -178,30 +289,55 @@ module Kitchen
|
|
|
178
289
|
VMIP
|
|
179
290
|
end
|
|
180
291
|
|
|
292
|
+
# Script that resolves the virtual switch to attach the VM to.
|
|
293
|
+
#
|
|
294
|
+
# @return [String] PowerShell source
|
|
295
|
+
# @api private
|
|
181
296
|
def vm_default_switch_ps
|
|
182
297
|
<<-VMSWITCH
|
|
183
298
|
Get-DefaultVMSwitch "#{config[:vm_switch]}" | ConvertTo-Json
|
|
184
299
|
VMSWITCH
|
|
185
300
|
end
|
|
186
301
|
|
|
302
|
+
# Script that attaches the configured ISO to the VM's DVD drive.
|
|
303
|
+
#
|
|
304
|
+
# @return [String] PowerShell source
|
|
305
|
+
# @api private
|
|
187
306
|
def mount_vm_iso
|
|
188
307
|
<<-MOUNTISO
|
|
189
308
|
mount-vmiso -id "#{@state[:id]}" -Path #{config[:iso_path]}
|
|
190
309
|
MOUNTISO
|
|
191
310
|
end
|
|
192
311
|
|
|
312
|
+
# Script that grows the parent VHD to the configured size.
|
|
313
|
+
#
|
|
314
|
+
# @return [String] PowerShell source
|
|
315
|
+
# @api private
|
|
193
316
|
def resize_vhd
|
|
194
317
|
<<-VMNOTE
|
|
195
318
|
Resize-VHD -Path "#{parent_vhd_path}" -SizeBytes #{config[:resize_vhd]}
|
|
196
319
|
VMNOTE
|
|
197
320
|
end
|
|
198
321
|
|
|
322
|
+
# Script that writes the configured note onto the VM.
|
|
323
|
+
#
|
|
324
|
+
# @return [String] PowerShell source
|
|
325
|
+
# @api private
|
|
199
326
|
def set_vm_note
|
|
200
327
|
<<-VMNOTE
|
|
201
328
|
Set-VM -Name (Get-VM | Where-Object{ $_.ID -eq "#{@state[:id]}"}).Name -Note "#{config[:vm_note]}"
|
|
202
329
|
VMNOTE
|
|
203
330
|
end
|
|
204
331
|
|
|
332
|
+
# Script that copies a file or directory into the running guest.
|
|
333
|
+
#
|
|
334
|
+
# Enables the guest service interface first if it is off, and walks a
|
|
335
|
+
# directory source file by file since `Copy-VMFile` handles only files.
|
|
336
|
+
#
|
|
337
|
+
# @param source [String] path on the Hyper-V host
|
|
338
|
+
# @param dest [String] path inside the guest
|
|
339
|
+
# @return [String] PowerShell source
|
|
340
|
+
# @api private
|
|
205
341
|
def copy_vm_file_ps(source, dest)
|
|
206
342
|
<<-FILECOPY
|
|
207
343
|
Function CopyFile ($VM, [string]$SourcePath, [string]$DestPath) {
|
|
@@ -240,6 +376,11 @@ module Kitchen
|
|
|
240
376
|
|
|
241
377
|
private
|
|
242
378
|
|
|
379
|
+
# Render a Ruby array as a PowerShell array literal.
|
|
380
|
+
#
|
|
381
|
+
# @param list [Array<String>, nil] the values
|
|
382
|
+
# @return [String] e.g. `@("8.8.8.8", "8.8.4.4")`, or `@()` when empty
|
|
383
|
+
# @api private
|
|
243
384
|
def ruby_array_to_ps_array(list)
|
|
244
385
|
return "@()" if list.nil? || list.empty?
|
|
245
386
|
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-hyperv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steven Murawski
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: test-kitchen
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -34,30 +48,42 @@ dependencies:
|
|
|
34
48
|
name: train
|
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
|
36
50
|
requirements:
|
|
37
|
-
- - "
|
|
51
|
+
- - ">="
|
|
38
52
|
- !ruby/object:Gem::Version
|
|
39
53
|
version: '3.5'
|
|
54
|
+
- - "<"
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '4.0'
|
|
40
57
|
type: :runtime
|
|
41
58
|
prerelease: false
|
|
42
59
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
60
|
requirements:
|
|
44
|
-
- - "
|
|
61
|
+
- - ">="
|
|
45
62
|
- !ruby/object:Gem::Version
|
|
46
63
|
version: '3.5'
|
|
64
|
+
- - "<"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '4.0'
|
|
47
67
|
- !ruby/object:Gem::Dependency
|
|
48
68
|
name: train-winrm
|
|
49
69
|
requirement: !ruby/object:Gem::Requirement
|
|
50
70
|
requirements:
|
|
51
|
-
- - "
|
|
71
|
+
- - ">="
|
|
52
72
|
- !ruby/object:Gem::Version
|
|
53
73
|
version: '0.2'
|
|
74
|
+
- - "<"
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '1.0'
|
|
54
77
|
type: :runtime
|
|
55
78
|
prerelease: false
|
|
56
79
|
version_requirements: !ruby/object:Gem::Requirement
|
|
57
80
|
requirements:
|
|
58
|
-
- - "
|
|
81
|
+
- - ">="
|
|
59
82
|
- !ruby/object:Gem::Version
|
|
60
83
|
version: '0.2'
|
|
84
|
+
- - "<"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '1.0'
|
|
61
87
|
description: Hyper-V Driver for Test-Kitchen
|
|
62
88
|
email:
|
|
63
89
|
- steven.murawski@gmail.com
|