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.
|
|
@@ -20,17 +22,34 @@ require "kitchen"
|
|
|
20
22
|
require "kitchen/driver"
|
|
21
23
|
require_relative "hyperv_version"
|
|
22
24
|
require_relative "powershell"
|
|
23
|
-
require "mixlib/shellout" unless defined?(Mixlib::ShellOut)
|
|
24
25
|
require "fileutils" unless defined?(FileUtils)
|
|
25
26
|
require "json" unless defined?(JSON)
|
|
26
27
|
require "train" unless defined?(Train)
|
|
27
28
|
require "train-winrm" unless defined?(TrainPlugins::WinRM)
|
|
29
|
+
require "time" unless defined?(Time.zone_offset)
|
|
28
30
|
|
|
29
31
|
module Kitchen
|
|
30
32
|
|
|
33
|
+
# Test Kitchen driver plugins.
|
|
31
34
|
module Driver
|
|
32
35
|
|
|
33
|
-
#
|
|
36
|
+
# Test Kitchen driver that builds instances as Hyper-V virtual machines.
|
|
37
|
+
#
|
|
38
|
+
# The driver never talks to Hyper-V directly. It generates PowerShell that
|
|
39
|
+
# calls the helper functions in `support/hyperv.ps1` and runs that script
|
|
40
|
+
# through a Train connection -- a local one on a Hyper-V host, or WinRM
|
|
41
|
+
# when `hyperv_server` points at a remote host.
|
|
42
|
+
#
|
|
43
|
+
# Each instance gets a differencing disk cloned from a shared parent VHD,
|
|
44
|
+
# so creating an instance costs a few seconds and very little disk.
|
|
45
|
+
#
|
|
46
|
+
# @example Minimal kitchen.yml
|
|
47
|
+
# driver:
|
|
48
|
+
# name: hyperv
|
|
49
|
+
# parent_vhd_folder: C:\VHDs
|
|
50
|
+
# parent_vhd_name: windows-2022.vhdx
|
|
51
|
+
#
|
|
52
|
+
# @see https://github.com/test-kitchen/kitchen-hyperv
|
|
34
53
|
class Hyperv < Kitchen::Driver::Base
|
|
35
54
|
|
|
36
55
|
kitchen_driver_api_version 2
|
|
@@ -60,9 +79,12 @@ module Kitchen
|
|
|
60
79
|
default_config :disable_secureboot, false
|
|
61
80
|
default_config :static_mac_address
|
|
62
81
|
default_config :disk_type do |driver|
|
|
63
|
-
File.extname(driver[:parent_vhd_name])
|
|
82
|
+
File.extname(driver[:parent_vhd_name].to_s)
|
|
64
83
|
end
|
|
65
84
|
|
|
85
|
+
default_config :copy_vm_files
|
|
86
|
+
default_config :dry_run, false
|
|
87
|
+
|
|
66
88
|
default_config :hyperv_server, nil
|
|
67
89
|
default_config :hyperv_username, nil
|
|
68
90
|
default_config :hyperv_password, nil
|
|
@@ -72,8 +94,22 @@ module Kitchen
|
|
|
72
94
|
|
|
73
95
|
include Kitchen::Driver::PowerShellScripts
|
|
74
96
|
|
|
97
|
+
# Create the virtual machine and wait until it is reachable.
|
|
98
|
+
#
|
|
99
|
+
# Runs the full bring-up in order: validate the configuration, clone the
|
|
100
|
+
# parent VHD into a differencing disk, create any additional data disks,
|
|
101
|
+
# create and start the VM, then block on the transport until the guest
|
|
102
|
+
# accepts connections.
|
|
103
|
+
#
|
|
104
|
+
# @param state [Hash] the instance state hash, updated in place with
|
|
105
|
+
# `:id`, `:hostname` and `:vm_name`
|
|
106
|
+
# @return [void]
|
|
107
|
+
# @raise [RuntimeError] if validation fails or Hyper-V cannot create the VM
|
|
75
108
|
def create(state)
|
|
76
109
|
@state = state
|
|
110
|
+
# Kitchen::Driver::Base#create runs config[:pre_create_command].
|
|
111
|
+
# Without this the option is silently ignored.
|
|
112
|
+
super
|
|
77
113
|
validate_vm_settings
|
|
78
114
|
create_new_differencing_disk
|
|
79
115
|
create_additional_disks
|
|
@@ -86,12 +122,25 @@ module Kitchen
|
|
|
86
122
|
info("Hyper-V instance #{instance.to_str} created.")
|
|
87
123
|
end
|
|
88
124
|
|
|
125
|
+
# Destroy the virtual machine and the disks created alongside it.
|
|
126
|
+
#
|
|
127
|
+
# Safe to call repeatedly and safe to call when the VM was removed out of
|
|
128
|
+
# band: a differencing disk left behind by a partial create is cleaned up
|
|
129
|
+
# even when no VM exists.
|
|
130
|
+
#
|
|
131
|
+
# @param state [Hash] the instance state hash; `:id` is deleted from it
|
|
132
|
+
# @return [void]
|
|
89
133
|
def destroy(state)
|
|
90
134
|
@state = state
|
|
91
135
|
if differencing_disk_exists && !vm_exists_silent
|
|
92
136
|
remove_differencing_disk
|
|
93
137
|
end
|
|
94
|
-
|
|
138
|
+
unless vm_exists
|
|
139
|
+
# The VM is gone, but a stale id would make every later run believe
|
|
140
|
+
# otherwise, so clear it rather than returning with it still in place.
|
|
141
|
+
state.delete(:id)
|
|
142
|
+
return
|
|
143
|
+
end
|
|
95
144
|
|
|
96
145
|
instance.transport.connection(state).close
|
|
97
146
|
remove_virtual_machine
|
|
@@ -101,16 +150,127 @@ module Kitchen
|
|
|
101
150
|
state.delete(:id)
|
|
102
151
|
end
|
|
103
152
|
|
|
153
|
+
# Report whether Hyper-V still has this instance's virtual machine.
|
|
154
|
+
#
|
|
155
|
+
# Backs `kitchen list --probe`. Deliberately read-only: unlike the check
|
|
156
|
+
# {#create} makes, this never starts a stopped VM.
|
|
157
|
+
#
|
|
158
|
+
# @param state [Hash] the instance state hash
|
|
159
|
+
# @return [Hash] normalized status data for Test Kitchen
|
|
160
|
+
def status(state)
|
|
161
|
+
@state = state
|
|
162
|
+
if state[:id].nil?
|
|
163
|
+
return status_report(
|
|
164
|
+
live: false,
|
|
165
|
+
state: "not_created",
|
|
166
|
+
message: "No virtual machine id recorded for this instance."
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
vm = run_ps vm_status_ps
|
|
171
|
+
if vm.nil? || vm["Id"].nil?
|
|
172
|
+
status_report(live: false, state: "not_created", resource_id: state[:id],
|
|
173
|
+
message: "Hyper-V has no virtual machine with id #{state[:id]}.")
|
|
174
|
+
else
|
|
175
|
+
running = vm["State"].to_s.casecmp?("running")
|
|
176
|
+
status_report(live: running, state: running ? "running" : "stopped",
|
|
177
|
+
resource_id: vm["Id"],
|
|
178
|
+
message: "Hyper-V reports the virtual machine as #{vm["State"]}.")
|
|
179
|
+
end
|
|
180
|
+
rescue => e
|
|
181
|
+
status_report(live: nil, state: "unknown", resource_id: state[:id], message: e.message)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Check for the common reasons this driver cannot build an instance.
|
|
185
|
+
#
|
|
186
|
+
# Backs `kitchen doctor`. Reports every problem it finds rather than
|
|
187
|
+
# stopping at the first, since they are usually related.
|
|
188
|
+
#
|
|
189
|
+
# @param state [Hash] the instance state hash
|
|
190
|
+
# @return [Boolean] true if at least one problem was found
|
|
191
|
+
def doctor(state)
|
|
192
|
+
@state = state
|
|
193
|
+
problems = hyperv_problems + parent_vhd_problems
|
|
194
|
+
problems.each { |problem| warn(problem) }
|
|
195
|
+
!problems.empty?
|
|
196
|
+
end
|
|
197
|
+
|
|
104
198
|
private
|
|
105
199
|
|
|
200
|
+
# Build the status hash Test Kitchen normalizes.
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash]
|
|
203
|
+
# @api private
|
|
204
|
+
def status_report(live:, state:, message:, resource_id: nil)
|
|
205
|
+
{
|
|
206
|
+
live: live,
|
|
207
|
+
state: state,
|
|
208
|
+
source: "driver",
|
|
209
|
+
resource_id: resource_id,
|
|
210
|
+
message: message,
|
|
211
|
+
checked_at: Time.now.utc.iso8601,
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Problems reaching the Hyper-V host itself.
|
|
216
|
+
#
|
|
217
|
+
# @return [Array<String>]
|
|
218
|
+
# @api private
|
|
219
|
+
def hyperv_problems
|
|
220
|
+
return [] unless run_ps(hyperv_module_ps).nil?
|
|
221
|
+
|
|
222
|
+
["The Hyper-V PowerShell module is not installed on #{hyperv_host_description}."]
|
|
223
|
+
rescue => e
|
|
224
|
+
["Could not run PowerShell on #{hyperv_host_description}: #{e.message}"]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Problems with the parent VHD the instance is cloned from.
|
|
228
|
+
#
|
|
229
|
+
# Only checked locally: the paths refer to the remote host's filesystem
|
|
230
|
+
# when hyperv_server is set, so this machine cannot see them.
|
|
231
|
+
#
|
|
232
|
+
# @return [Array<String>]
|
|
233
|
+
# @api private
|
|
234
|
+
def parent_vhd_problems
|
|
235
|
+
return [] if remote_hyperv
|
|
236
|
+
|
|
237
|
+
problems = []
|
|
238
|
+
unless vhd_folder?
|
|
239
|
+
problems << "parent_vhd_folder #{config[:parent_vhd_folder].inspect} does not exist."
|
|
240
|
+
end
|
|
241
|
+
unless vhd?
|
|
242
|
+
problems << "parent_vhd_name #{config[:parent_vhd_name].inspect} was not found in " \
|
|
243
|
+
"#{config[:parent_vhd_folder].inspect}."
|
|
244
|
+
end
|
|
245
|
+
problems
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# How to refer to the Hyper-V host in a message.
|
|
249
|
+
#
|
|
250
|
+
# @return [String]
|
|
251
|
+
# @api private
|
|
252
|
+
def hyperv_host_description
|
|
253
|
+
remote_hyperv ? config[:hyperv_server] : "this machine"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Check the configuration before anything is created.
|
|
257
|
+
#
|
|
258
|
+
# Also resolves `vm_switch`, which requires a round trip to the host and
|
|
259
|
+
# so cannot be handled by a plain `default_config` block.
|
|
260
|
+
#
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @raise [RuntimeError] if the parent VHD is missing, the startup memory
|
|
263
|
+
# falls outside the dynamic memory range, or the VLAN id is not a valid
|
|
264
|
+
# 802.1Q id
|
|
265
|
+
# @api private
|
|
106
266
|
def validate_vm_settings
|
|
107
267
|
raise "Missing parent_vhd_folder" unless vhd_folder? || remote_hyperv
|
|
108
268
|
raise "Missing parent_vhd_name" unless vhd? || remote_hyperv
|
|
109
269
|
|
|
110
270
|
if config[:dynamic_memory]
|
|
111
|
-
startup_bytes =
|
|
112
|
-
min =
|
|
113
|
-
max =
|
|
271
|
+
startup_bytes = integer_config(:memory_startup_bytes)
|
|
272
|
+
min = integer_config(:dynamic_memory_min_bytes)
|
|
273
|
+
max = integer_config(:dynamic_memory_max_bytes)
|
|
114
274
|
memory_valid = startup_bytes.between?(min, max)
|
|
115
275
|
warning = "memory_startup_bytes (#{startup_bytes}) must" \
|
|
116
276
|
" fall within dynamic memory range (#{min}-#{max})"
|
|
@@ -118,7 +278,7 @@ module Kitchen
|
|
|
118
278
|
end
|
|
119
279
|
config[:vm_switch] = vm_switch
|
|
120
280
|
if config[:vm_vlan_id]
|
|
121
|
-
vm_vlan_id =
|
|
281
|
+
vm_vlan_id = integer_config(:vm_vlan_id)
|
|
122
282
|
vm_vlan_id_min = 1
|
|
123
283
|
vm_vlan_id_max = 4094
|
|
124
284
|
vm_vlan_id_valid = vm_vlan_id.between?(vm_vlan_id_min, vm_vlan_id_max)
|
|
@@ -128,6 +288,10 @@ module Kitchen
|
|
|
128
288
|
end
|
|
129
289
|
end
|
|
130
290
|
|
|
291
|
+
# Clone the parent VHD into this instance's differencing disk.
|
|
292
|
+
#
|
|
293
|
+
# @return [void]
|
|
294
|
+
# @api private
|
|
131
295
|
def create_new_differencing_disk
|
|
132
296
|
info("Creating differencing disk for #{instance.name}.")
|
|
133
297
|
run_ps new_differencing_disk_ps
|
|
@@ -135,6 +299,15 @@ module Kitchen
|
|
|
135
299
|
set_new_vhd_size
|
|
136
300
|
end
|
|
137
301
|
|
|
302
|
+
# Create each disk described by the `additional_disks` config.
|
|
303
|
+
#
|
|
304
|
+
# Records the created paths in `@additional_disk_objects` so
|
|
305
|
+
# {PowerShellScripts#new_vm_ps} can attach them to the new VM.
|
|
306
|
+
#
|
|
307
|
+
# @return [void]
|
|
308
|
+
# @raise [RuntimeError] if a disk entry has no name, or the target file
|
|
309
|
+
# already exists
|
|
310
|
+
# @api private
|
|
138
311
|
def create_additional_disks
|
|
139
312
|
return if config[:additional_disks].nil?
|
|
140
313
|
|
|
@@ -154,6 +327,14 @@ module Kitchen
|
|
|
154
327
|
end
|
|
155
328
|
end
|
|
156
329
|
|
|
330
|
+
# Resolve the virtual switch to attach the VM to.
|
|
331
|
+
#
|
|
332
|
+
# With `vm_switch` unset the host picks its first switch; with it set the
|
|
333
|
+
# host confirms that switch exists.
|
|
334
|
+
#
|
|
335
|
+
# @return [String] the switch name
|
|
336
|
+
# @raise [RuntimeError] if the host reports no usable switch
|
|
337
|
+
# @api private
|
|
157
338
|
def vm_switch
|
|
158
339
|
default_switch_object = run_ps vm_default_switch_ps
|
|
159
340
|
if default_switch_object.nil? ||
|
|
@@ -165,6 +346,11 @@ module Kitchen
|
|
|
165
346
|
default_switch_object["Name"]
|
|
166
347
|
end
|
|
167
348
|
|
|
349
|
+
# Create and start the VM, unless one already exists for this instance.
|
|
350
|
+
#
|
|
351
|
+
# @return [void]
|
|
352
|
+
# @raise [RuntimeError] if the host returns no VM
|
|
353
|
+
# @api private
|
|
168
354
|
def create_virtual_machine
|
|
169
355
|
return if vm_exists
|
|
170
356
|
|
|
@@ -176,18 +362,34 @@ module Kitchen
|
|
|
176
362
|
info("Created virtual machine for #{instance.name}.")
|
|
177
363
|
end
|
|
178
364
|
|
|
365
|
+
# Copy the VM's id, address and name into the instance state.
|
|
366
|
+
#
|
|
367
|
+
# @return [void]
|
|
368
|
+
# @raise [RuntimeError] if the host reports no detail for the VM
|
|
369
|
+
# @api private
|
|
179
370
|
def update_state
|
|
180
371
|
vm_details
|
|
372
|
+
raise "Unable to fetch details for virtual machine #{instance.name}." if @vm.nil?
|
|
373
|
+
|
|
181
374
|
@state[:id] = @vm["Id"]
|
|
182
375
|
@state[:hostname] = @vm["IpAddress"]
|
|
183
376
|
@state[:vm_name] = @vm["Name"]
|
|
184
377
|
end
|
|
185
378
|
|
|
379
|
+
# Fetch the VM's details from the host, applying a static IP first if one
|
|
380
|
+
# is configured.
|
|
381
|
+
#
|
|
382
|
+
# @return [Hash, nil] the parsed `Get-VmDetail` payload
|
|
383
|
+
# @api private
|
|
186
384
|
def vm_details
|
|
187
385
|
run_ps set_vm_ipaddress_ps if config[:ip_address]
|
|
188
386
|
@vm = run_ps vm_details_ps
|
|
189
387
|
end
|
|
190
388
|
|
|
389
|
+
# Attach the configured ISO to the VM's DVD drive.
|
|
390
|
+
#
|
|
391
|
+
# @return [void]
|
|
392
|
+
# @api private
|
|
191
393
|
def mount_virtual_machine_iso
|
|
192
394
|
return unless config[:iso_path]
|
|
193
395
|
|
|
@@ -196,6 +398,10 @@ module Kitchen
|
|
|
196
398
|
info("Done mounting #{config[:iso_path]}")
|
|
197
399
|
end
|
|
198
400
|
|
|
401
|
+
# Grow the disk to `resize_vhd` bytes, when configured.
|
|
402
|
+
#
|
|
403
|
+
# @return [void]
|
|
404
|
+
# @api private
|
|
199
405
|
def set_new_vhd_size
|
|
200
406
|
return unless config[:resize_vhd]
|
|
201
407
|
|
|
@@ -204,6 +410,11 @@ module Kitchen
|
|
|
204
410
|
info("Resized differencing disk for #{instance.name}.")
|
|
205
411
|
end
|
|
206
412
|
|
|
413
|
+
# Write the configured note onto the VM, so it is identifiable in the
|
|
414
|
+
# Hyper-V manager.
|
|
415
|
+
#
|
|
416
|
+
# @return [void]
|
|
417
|
+
# @api private
|
|
207
418
|
def set_virtual_machine_note
|
|
208
419
|
return unless config[:vm_note]
|
|
209
420
|
|
|
@@ -211,6 +422,13 @@ module Kitchen
|
|
|
211
422
|
run_ps set_vm_note
|
|
212
423
|
end
|
|
213
424
|
|
|
425
|
+
# Copy the configured files into the running guest.
|
|
426
|
+
#
|
|
427
|
+
# Requires the guest service interface, which `enable_guest_services`
|
|
428
|
+
# turns on.
|
|
429
|
+
#
|
|
430
|
+
# @return [void]
|
|
431
|
+
# @api private
|
|
214
432
|
def copy_vm_files
|
|
215
433
|
return if config[:copy_vm_files].nil?
|
|
216
434
|
|
|
@@ -221,6 +439,10 @@ module Kitchen
|
|
|
221
439
|
info("Copied files to virtual machine")
|
|
222
440
|
end
|
|
223
441
|
|
|
442
|
+
# Whether a VM for this instance exists, starting it if it is stopped.
|
|
443
|
+
#
|
|
444
|
+
# @return [Boolean]
|
|
445
|
+
# @api private
|
|
224
446
|
def vm_exists
|
|
225
447
|
info("Checking for existing virtual machine.")
|
|
226
448
|
return false unless @state.key?(:id) && !@state[:id].nil?
|
|
@@ -228,12 +450,18 @@ module Kitchen
|
|
|
228
450
|
existing_vm = run_ps ensure_vm_running_ps
|
|
229
451
|
return false if existing_vm.nil? || existing_vm["Id"].nil?
|
|
230
452
|
|
|
231
|
-
info("Found an
|
|
453
|
+
info("Found an existing VM with an ID: #{existing_vm["Id"]}")
|
|
232
454
|
true
|
|
233
455
|
end
|
|
234
456
|
|
|
235
|
-
#
|
|
236
|
-
#
|
|
457
|
+
# {#vm_exists} without the logging.
|
|
458
|
+
#
|
|
459
|
+
# `destroy` checks for a VM twice -- once to decide whether a leftover
|
|
460
|
+
# differencing disk is stale, once to decide whether to remove the VM --
|
|
461
|
+
# and logging both checks makes it look like the driver ran twice.
|
|
462
|
+
#
|
|
463
|
+
# @return [Boolean]
|
|
464
|
+
# @api private
|
|
237
465
|
def vm_exists_silent
|
|
238
466
|
return false unless @state.key?(:id) && !@state[:id].nil?
|
|
239
467
|
|
|
@@ -243,18 +471,45 @@ module Kitchen
|
|
|
243
471
|
true
|
|
244
472
|
end
|
|
245
473
|
|
|
474
|
+
# Whether this instance's differencing disk is on disk.
|
|
475
|
+
#
|
|
476
|
+
# @return [Boolean]
|
|
477
|
+
# @api private
|
|
246
478
|
def differencing_disk_exists
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
479
|
+
File.exist?(differencing_disk_path)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Read a config value that must be numeric.
|
|
483
|
+
#
|
|
484
|
+
# Values coming from `kitchen.yml` may be quoted, and comparing a String
|
|
485
|
+
# against an Integer raises deep inside Comparable rather than reporting
|
|
486
|
+
# anything a user can act on.
|
|
487
|
+
#
|
|
488
|
+
# @param key [Symbol] the config key
|
|
489
|
+
# @return [Integer]
|
|
490
|
+
# @raise [RuntimeError] if the value is not coercible to an Integer
|
|
491
|
+
# @api private
|
|
492
|
+
def integer_config(key)
|
|
493
|
+
value = config[key]
|
|
494
|
+
Integer(value)
|
|
495
|
+
rescue ArgumentError, TypeError
|
|
496
|
+
raise "#{key} (#{value.inspect}) must be an integer"
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Force the VM off and remove it.
|
|
500
|
+
#
|
|
501
|
+
# @return [void]
|
|
502
|
+
# @api private
|
|
252
503
|
def remove_virtual_machine
|
|
253
504
|
info("Deleting virtual machine for #{instance.name}")
|
|
254
505
|
run_ps delete_vm_ps
|
|
255
506
|
info("Deleted virtual machine for #{instance.name}")
|
|
256
507
|
end
|
|
257
508
|
|
|
509
|
+
# Delete this instance's differencing disk, if present.
|
|
510
|
+
#
|
|
511
|
+
# @return [void]
|
|
512
|
+
# @api private
|
|
258
513
|
def remove_differencing_disk
|
|
259
514
|
return unless differencing_disk_exists
|
|
260
515
|
|
|
@@ -263,6 +518,11 @@ module Kitchen
|
|
|
263
518
|
info("Removed the differencing disk for #{instance.name}.")
|
|
264
519
|
end
|
|
265
520
|
|
|
521
|
+
# Delete every configured additional disk that exists.
|
|
522
|
+
#
|
|
523
|
+
# @return [void]
|
|
524
|
+
# @raise [RuntimeError] if a disk entry has no name
|
|
525
|
+
# @api private
|
|
266
526
|
def remove_additional_disks
|
|
267
527
|
return if config[:additional_disks].nil?
|
|
268
528
|
|
|
@@ -279,74 +539,141 @@ module Kitchen
|
|
|
279
539
|
end
|
|
280
540
|
end
|
|
281
541
|
|
|
542
|
+
# Where this instance's files live under the local kitchen root.
|
|
543
|
+
#
|
|
544
|
+
# @return [String]
|
|
545
|
+
# @api private
|
|
282
546
|
def kitchen_vm_path
|
|
283
547
|
@kitchen_vm_path ||= File.join(config[:kitchen_root], ".kitchen/#{instance.name}")
|
|
284
548
|
end
|
|
285
549
|
|
|
550
|
+
# Where this instance's files live on a remote Hyper-V host.
|
|
551
|
+
#
|
|
552
|
+
# @return [String]
|
|
553
|
+
# @api private
|
|
286
554
|
def remote_kitchen_vm_path
|
|
287
555
|
config[:remote_vm_path]
|
|
288
556
|
end
|
|
289
557
|
|
|
558
|
+
# Directory holding every disk belonging to this instance.
|
|
559
|
+
#
|
|
560
|
+
# A remote host cannot see the local kitchen root, so a remote run keeps
|
|
561
|
+
# its disks under `remote_vm_path` instead.
|
|
562
|
+
#
|
|
563
|
+
# @return [String]
|
|
564
|
+
# @api private
|
|
565
|
+
def vm_base_path
|
|
566
|
+
remote_hyperv ? remote_kitchen_vm_path : kitchen_vm_path
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# ISO to boot the VM from, for building an instance from install media.
|
|
570
|
+
#
|
|
571
|
+
# @return [String, nil]
|
|
572
|
+
# @api private
|
|
290
573
|
def boot_iso_path
|
|
291
574
|
@boot_iso_path ||= config[:boot_iso_path]
|
|
292
575
|
end
|
|
293
576
|
|
|
577
|
+
# Full path to this instance's differencing disk.
|
|
578
|
+
#
|
|
579
|
+
# @return [String]
|
|
580
|
+
# @api private
|
|
294
581
|
def differencing_disk_path
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
@differencing_disk_path ||= File.join(kitchen_vm_base, "diff" + "#{config[:disk_type]}")
|
|
582
|
+
@differencing_disk_path ||= File.join(vm_base_path, "diff#{config[:disk_type]}")
|
|
298
583
|
end
|
|
299
584
|
|
|
585
|
+
# Full path to one additional data disk.
|
|
586
|
+
#
|
|
587
|
+
# @param disk_name [String] the disk's configured name
|
|
588
|
+
# @param disk_type [String] the file extension, e.g. `".vhdx"`
|
|
589
|
+
# @return [String]
|
|
590
|
+
# @api private
|
|
300
591
|
def additional_disk_path(disk_name, disk_type)
|
|
301
|
-
File.join(
|
|
592
|
+
File.join(vm_base_path, disk_name + disk_type)
|
|
302
593
|
end
|
|
303
594
|
|
|
595
|
+
# Full path to the parent VHD every instance is cloned from.
|
|
596
|
+
#
|
|
597
|
+
# @return [String]
|
|
598
|
+
# @api private
|
|
304
599
|
def parent_vhd_path
|
|
305
600
|
@parent_vhd_path ||= File.join(config[:parent_vhd_folder], config[:parent_vhd_name])
|
|
306
601
|
end
|
|
307
602
|
|
|
603
|
+
# Whether the configured parent VHD folder exists locally.
|
|
604
|
+
#
|
|
605
|
+
# @return [Boolean]
|
|
606
|
+
# @api private
|
|
308
607
|
def vhd_folder?
|
|
309
608
|
config[:parent_vhd_folder] && Dir.exist?(config[:parent_vhd_folder])
|
|
310
609
|
end
|
|
311
610
|
|
|
611
|
+
# Whether the configured parent VHD file exists locally.
|
|
612
|
+
#
|
|
613
|
+
# @return [Boolean]
|
|
614
|
+
# @api private
|
|
312
615
|
def vhd?
|
|
313
616
|
config[:parent_vhd_name] && File.exist?(parent_vhd_path)
|
|
314
617
|
end
|
|
315
618
|
|
|
619
|
+
# Whether the driver is targeting a remote Hyper-V host over WinRM.
|
|
620
|
+
#
|
|
621
|
+
# @return [Boolean]
|
|
622
|
+
# @api private
|
|
316
623
|
def remote_hyperv
|
|
317
624
|
!!config[:hyperv_server]
|
|
318
625
|
end
|
|
319
626
|
|
|
627
|
+
# The Train connection commands run over, opened on first use.
|
|
628
|
+
#
|
|
629
|
+
# Uses the `local` backend on a Hyper-V host and `winrm` when
|
|
630
|
+
# `hyperv_server` is set, uploading the support script in that case.
|
|
631
|
+
#
|
|
632
|
+
# @return [Train::Plugins::Transport::BaseConnection]
|
|
633
|
+
# @api private
|
|
320
634
|
def connection
|
|
321
635
|
return @connection if @connection
|
|
322
636
|
|
|
323
637
|
backend = remote_hyperv ? "winrm" : "local"
|
|
324
638
|
|
|
325
639
|
train = Train.create(backend, {
|
|
326
|
-
host:
|
|
327
|
-
user:
|
|
328
|
-
password:
|
|
329
|
-
ssl:
|
|
640
|
+
host: config[:hyperv_server],
|
|
641
|
+
user: config[:hyperv_username],
|
|
642
|
+
password: config[:hyperv_password],
|
|
643
|
+
ssl: config[:hyperv_ssl],
|
|
330
644
|
self_signed: config[:hyperv_insecure],
|
|
331
645
|
})
|
|
332
646
|
@connection = train.connection
|
|
333
647
|
|
|
334
|
-
#
|
|
335
|
-
|
|
648
|
+
# Only the remote backend dot-sources the uploaded copy; locally the
|
|
649
|
+
# driver reads the script straight out of the gem.
|
|
650
|
+
@connection.upload(local_script_path, remote_script_path) if remote_hyperv
|
|
336
651
|
|
|
337
652
|
@connection
|
|
338
653
|
end
|
|
339
654
|
|
|
655
|
+
# Path every generated script dot-sources to load the helper functions.
|
|
656
|
+
#
|
|
657
|
+
# @return [String]
|
|
658
|
+
# @api private
|
|
340
659
|
def base_script_path
|
|
341
660
|
return remote_script_path if remote_hyperv
|
|
342
661
|
|
|
343
662
|
local_script_path
|
|
344
663
|
end
|
|
345
664
|
|
|
665
|
+
# Path to `support/hyperv.ps1` inside the installed gem.
|
|
666
|
+
#
|
|
667
|
+
# @return [String]
|
|
668
|
+
# @api private
|
|
346
669
|
def local_script_path
|
|
347
|
-
File.
|
|
670
|
+
File.expand_path("../../../support/hyperv.ps1", __dir__)
|
|
348
671
|
end
|
|
349
672
|
|
|
673
|
+
# Path the support script is uploaded to on a remote host.
|
|
674
|
+
#
|
|
675
|
+
# @return [String]
|
|
676
|
+
# @api private
|
|
350
677
|
def remote_script_path
|
|
351
678
|
File.join(config[:kitchen_root], "kitchen-hyperv", "hyperv.ps1")
|
|
352
679
|
end
|
|
@@ -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) 2015-2020 Chef Software, Inc.
|
|
@@ -17,6 +19,13 @@
|
|
|
17
19
|
|
|
18
20
|
module Kitchen
|
|
19
21
|
module Driver
|
|
20
|
-
|
|
22
|
+
# Version of the kitchen-hyperv gem, reported by `kitchen diagnose` as the
|
|
23
|
+
# driver's plugin version.
|
|
24
|
+
#
|
|
25
|
+
# Kept in its own file so the gemspec can read it without loading the
|
|
26
|
+
# driver, and therefore without loading test-kitchen.
|
|
27
|
+
#
|
|
28
|
+
# @return [String] a frozen semantic version
|
|
29
|
+
HYPERV_VERSION = "0.12.0"
|
|
21
30
|
end
|
|
22
31
|
end
|