kitchen-hyperv 0.11.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c1c78c069680ce6179d290ba250302102bd29da553842ecc3f00e7ddb4114c5
4
- data.tar.gz: 05413cbc517eebed2176b0ab9a20f86e742ee2dcf14a58e8babf0a774a64fedd
3
+ metadata.gz: d5b1645576764131a29498745a319db7a3e3fa166de15481ec0744acb3dbfe42
4
+ data.tar.gz: d82e688587b75d0156c71f3b79200b76e7f4d6601561d73a7445bd7236cdf005
5
5
  SHA512:
6
- metadata.gz: 723997d9f09f593d941e7ee46cca5a9ec19577142cb99148e7cebc6f9839395ddb06e6f681ed470522753aff12c088ea903440d186f6cd201d7f41616633331f
7
- data.tar.gz: f87621dfb332510fa30f6c8fee49a4fa23186c6bf1ae9597fce4cca8467afa8e1387bc5c4a2a3931ad28a20e15475fb6755f747137688a9bd3263b944b8ac48a
6
+ metadata.gz: c82372e025d904e35a675987e01a1bd4805d653d090bb35814d9345c3437a854b87c49db795646afe019c536ab6f6696dd07de834ac279dc62aed31a3a85f543
7
+ data.tar.gz: 16c10a436d35cd4ca05fbad919ad3026f9eabcf403b8ffca5d55573cca445b15583b989d1c42517762151162d7fb5207006d67acd637791f8ad540ac13379064
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  # Required directly by the PowerShell command encoder. base64 is a bundled
22
22
  # gem from Ruby 3.4 on, so it has to be declared rather than assumed.
23
23
  spec.add_dependency "base64", "~> 0.2"
24
- spec.add_dependency "test-kitchen", ">= 1.4", "< 5"
24
+ spec.add_dependency "test-kitchen", ">= 3.0", "< 5"
25
25
  spec.add_dependency "train", ">= 3.5", "< 4.0"
26
26
  spec.add_dependency "train-winrm", ">= 0.2", "< 1.0"
27
27
  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) 2020 Chef Software, Inc.
@@ -20,11 +22,11 @@ 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
 
@@ -77,9 +79,12 @@ module Kitchen
77
79
  default_config :disable_secureboot, false
78
80
  default_config :static_mac_address
79
81
  default_config :disk_type do |driver|
80
- File.extname(driver[:parent_vhd_name])
82
+ File.extname(driver[:parent_vhd_name].to_s)
81
83
  end
82
84
 
85
+ default_config :copy_vm_files
86
+ default_config :dry_run, false
87
+
83
88
  default_config :hyperv_server, nil
84
89
  default_config :hyperv_username, nil
85
90
  default_config :hyperv_password, nil
@@ -102,6 +107,9 @@ module Kitchen
102
107
  # @raise [RuntimeError] if validation fails or Hyper-V cannot create the VM
103
108
  def create(state)
104
109
  @state = state
110
+ # Kitchen::Driver::Base#create runs config[:pre_create_command].
111
+ # Without this the option is silently ignored.
112
+ super
105
113
  validate_vm_settings
106
114
  create_new_differencing_disk
107
115
  create_additional_disks
@@ -142,8 +150,109 @@ module Kitchen
142
150
  state.delete(:id)
143
151
  end
144
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
+
145
198
  private
146
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
+
147
256
  # Check the configuration before anything is created.
148
257
  #
149
258
  # Also resolves `vm_switch`, which requires a round trip to the host and
@@ -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.
@@ -24,6 +26,6 @@ module Kitchen
24
26
  # driver, and therefore without loading test-kitchen.
25
27
  #
26
28
  # @return [String] a frozen semantic version
27
- HYPERV_VERSION = "0.11.0".freeze
29
+ HYPERV_VERSION = "0.12.0"
28
30
  end
29
31
  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) 2020 Chef Software, Inc.
@@ -16,8 +18,8 @@
16
18
  # limitations under the License.
17
19
 
18
20
  require "base64" unless defined?(Base64)
19
- require "mixlib/shellout" unless defined?(Mixlib::ShellOut)
20
21
  require "benchmark" unless defined?(Benchmark)
22
+ require "rbconfig/sizeof" unless defined?(RbConfig::SIZEOF)
21
23
  require "fileutils" unless defined?(FileUtils)
22
24
  require "json" unless defined?(JSON)
23
25
 
@@ -41,6 +43,13 @@ module Kitchen
41
43
  #
42
44
  # @see Kitchen::Driver::Hyperv
43
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
+
44
53
  # Encode a script the way `powershell.exe -encodedcommand` expects it:
45
54
  # UTF-16LE, then Base64.
46
55
  #
@@ -52,6 +61,26 @@ module Kitchen
52
61
  Base64.strict_encode64(encoded_script)
53
62
  end
54
63
 
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
+
55
84
  # Whether a 64-bit PowerShell is directly reachable.
56
85
  #
57
86
  # Always true for a remote host, where the local architecture is
@@ -59,12 +88,11 @@ module Kitchen
59
88
  #
60
89
  # @return [Boolean]
61
90
  # @api private
62
- def is_64bit?
91
+ def sixty_four_bit?
63
92
  return true if remote_hyperv
64
93
 
65
- os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
66
- ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
67
- os_arch == "AMD64" && ruby_arch == 64
94
+ SIXTY_FOUR_BIT_ARCHITECTURES.include?(os_architecture) &&
95
+ ruby_architecture_bits == 64
68
96
  end
69
97
 
70
98
  # Whether both the OS and Ruby are 32-bit, so no WOW64 redirection is in
@@ -72,10 +100,25 @@ module Kitchen
72
100
  #
73
101
  # @return [Boolean]
74
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?
114
+ end
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
75
120
  def is_32bit?
76
- os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
77
- ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
78
- os_arch != "AMD64" && ruby_arch == 32
121
+ thirty_two_bit?
79
122
  end
80
123
 
81
124
  # Path to a PowerShell that can see the Hyper-V cmdlets.
@@ -88,7 +131,7 @@ module Kitchen
88
131
  # @return [String]
89
132
  # @api private
90
133
  def powershell_64_bit
91
- if is_64bit? || is_32bit?
134
+ if sixty_four_bit? || thirty_two_bit?
92
135
  'c:\windows\system32\windowspowershell\v1.0\powershell.exe'
93
136
  else
94
137
  'c:\windows\sysnative\windowspowershell\v1.0\powershell.exe'
@@ -256,6 +299,34 @@ module Kitchen
256
299
  DETAILS
257
300
  end
258
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
+
259
330
  # Script that forces the VM off and removes it.
260
331
  #
261
332
  # @return [String] PowerShell source
data/support/hyperv.ps1 CHANGED
@@ -1,233 +1,264 @@
1
- #requires -Version 2 -Modules Hyper-V
2
-
3
- #implicitly import hyperv module to avoid powercli cmdlets
4
- if ((Get-Module -Name 'hyper-v') -ne $null) {
5
- Remove-Module -Name hyper-v
6
- Import-Module -Name hyper-v
7
- }
8
- else {
9
- Import-Module -Name hyper-v
10
- }
11
-
12
- $ProgressPreference = 'SilentlyContinue'
13
-
14
-
15
- function New-DifferencingDisk {
16
- [cmdletbinding()]
17
- param (
18
- [parameter(Mandatory)]
19
- [ValidateNotNullOrEmpty()]
20
- [string[]]$Path,
21
- [parameter(Mandatory)]
22
- [ValidateNotNullOrEmpty()]
23
- [string]$ParentPath
24
- )
25
- if (-not (Test-Path $Path)) {
26
- $null = new-vhd @psboundparameters -Differencing
27
- }
28
- }
29
-
30
- function Assert-VmRunning {
31
- [cmdletbinding()]
32
- param([string]$Id)
33
-
34
- if ([string]::IsNullOrEmpty($Id)) {
35
- $Output = [pscustomobject]@{
36
- Name = ''
37
- State = ''
38
- }
39
- }
40
- else {
41
- $Output = Get-VM -Id $Id |
42
- ForEach-Object -Process {
43
- if ($_.State -notlike 'Running') {
44
- $_ |
45
- Start-VM -passthru
46
- }
47
- else {
48
- $_
49
- }
50
- } |
51
- Select-Object -Property Name, Id, State
52
- }
53
- $Output
54
- }
55
-
56
- function New-KitchenVM {
57
- [cmdletbinding()]
58
- param (
59
- $Generation = 1,
60
- $DisableSecureBoot,
61
- $MemoryStartupBytes,
62
- $StaticMacAddress,
63
- $Name,
64
- $Path,
65
- $VHDPath,
66
- $SwitchName,
67
- $VlanId,
68
- $ProcessorCount,
69
- $UseDynamicMemory,
70
- $DynamicMemoryMinBytes,
71
- $DynamicMemoryMaxBytes,
72
- $boot_iso_path,
73
- $EnableGuestServices,
74
- $AdditionalDisks
75
- )
76
- $null = $psboundparameters.remove('DisableSecureBoot')
77
- $null = $psboundparameters.remove('ProcessorCount')
78
- $null = $psboundparameters.remove('StaticMacAddress')
79
- $null = $psboundparameters.remove('UseDynamicMemory')
80
- $null = $psboundparameters.remove('DynamicMemoryMinBytes')
81
- $null = $psboundparameters.remove('DynamicMemoryMaxBytes')
82
- $null = $psboundparameters.remove('boot_iso_path')
83
- $null = $psboundparameters.remove('EnableGuestServices')
84
- $null = $psboundparameters.remove('VlanId')
85
- $null = $psboundparameters.remove('AdditionalDisks')
86
- $DisableSecureBoot = [Convert]::ToBoolean($DisableSecureBoot)
87
- $UseDynamicMemory = [Convert]::ToBoolean($UseDynamicMemory)
88
- $null = [bool]::TryParse($EnableGuestServices, [ref]$EnableGuestServices)
89
-
90
- $vm = new-vm @psboundparameters |
91
- Set-Vm -ProcessorCount $ProcessorCount -passthru
92
-
93
- if ($UseDynamicMemory) {
94
- $vm | Set-VMMemory -DynamicMemoryEnabled $true -MinimumBytes $DynamicMemoryMinBytes -MaximumBytes $DynamicMemoryMaxBytes
95
- }
96
- else {
97
- $vm | Set-VMMemory -DynamicMemoryEnabled $false
98
- }
99
- if (-not [string]::IsNullOrEmpty($boot_iso_path)) {
100
- Mount-VMISO -Id $vm.Id -Path $boot_iso_path
101
- }
102
- if (-not [string]::IsNullOrEmpty($StaticMacAddress)) {
103
- Set-VMNetworkAdapter -VMName $vm.VMName -StaticMacAddress $StaticMacAddress
104
- }
105
- if ($EnableGuestServices -and (Get-command Enable-VMIntegrationService -ErrorAction SilentlyContinue)) {
106
- Enable-VMIntegrationService -VM $vm -Name 'Guest Service Interface'
107
- }
108
- if (($VlanId -ne $null) -and (Get-command Set-VMNetworkAdapterVlan -ErrorAction SilentlyContinue)) {
109
- Set-VMNetworkAdapterVlan -VM $vm -Access -VlanId $VlanId
110
- }
111
- if ($AdditionalDisks -and (Get-command Add-VMHardDiskDrive -ErrorAction SilentlyContinue)) {
112
- foreach ($AdditionalDisk in $AdditionalDisks) {
113
- Add-VMHardDiskDrive -VM $vm -Path $AdditionalDisk
114
- }
115
- }
116
- if ($DisableSecureBoot -and ($Generation -eq 2) -and (Get-command Set-VMFirmware -ErrorAction SilentlyContinue)) {
117
- Set-VMFirmware -VM $vm -EnableSecureBoot Off
118
- }
119
- if ((Get-Command -Name Set-Vm).Parameters["AutomaticCheckpointsEnabled"]) {
120
- Set-VM -Name $vm.VMName -AutomaticCheckpointsEnabled $false
121
- }
122
- $vm | Start-Vm -passthru |
123
- foreach {
124
- $vm = $_
125
- do {
126
- start-sleep -seconds 2
127
- }
128
- while ($vm.state -notlike 'Running')
129
- $vm
130
- } |
131
- select Name, Id, State
132
- }
133
-
134
- function Get-VmIP($vm) {
135
- start-sleep -seconds 10
136
- $vm.networkadapters.ipaddresses |
137
- Where-Object {
138
- $_ -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
139
- } |
140
- Select-Object -First 1
141
- }
142
-
143
- Function Set-VMNetworkConfiguration {
144
- [CmdletBinding()]
145
- Param (
146
- [parameter(valuefrompipeline)]
147
- [object]$NetworkAdapter,
148
- [String[]]$IPAddress = @(),
149
- [String[]]$Gateway = @(),
150
- [String[]]$DNSServers = @(),
151
- [String[]]$Subnet = @()
152
- )
153
-
154
- $vm = Get-WmiObject -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' | Where-Object {
155
- $_.ElementName -eq $NetworkAdapter.VMName
156
- }
157
- $VMSettings = $vm.GetRelated('Msvm_VirtualSystemSettingData') | Where-Object {
158
- $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized'
159
- }
160
- $VMNetAdapters = $VMSettings.GetRelated('Msvm_SyntheticEthernetPortSettingData')
161
-
162
- $NetworkSettings = @()
163
- foreach ($NetAdapter in $VMNetAdapters) {
164
- if ($NetAdapter.Address -eq $NetworkAdapter.MacAddress) {
165
- $NetworkSettings = $NetworkSettings + $NetAdapter.GetRelated('Msvm_GuestNetworkAdapterConfiguration')
166
- }
167
- }
168
-
169
- $NetworkSettings[0].IPAddresses = $IPAddress
170
- $NetworkSettings[0].DefaultGateways = $Gateway
171
- $NetworkSettings[0].DNSServers = $DNSServers
172
- $NetworkSettings[0].Subnets = $Subnet
173
- $NetworkSettings[0].ProtocolIFType = 4096
174
- $NetworkSettings[0].DHCPEnabled = $false
175
-
176
-
177
- $Service = Get-WmiObject -Class 'Msvm_VirtualSystemManagementService' -Namespace 'root\virtualization\v2'
178
- $setIP = $Service.SetGuestNetworkAdapterConfiguration($vm, $NetworkSettings[0].GetText(1))
179
-
180
- if ($setIP.ReturnValue -eq 4096) {
181
- $job = [WMI]$setIP.job
182
-
183
- while ($job.JobState -eq 3 -or $job.JobState -eq 4) {
184
- Start-Sleep 1
185
- $job = [WMI]$setIP.job
186
- }
187
-
188
- if ($job.JobState -ne 7) {
189
- $job.GetError()
190
- }
191
- }
192
- (Get-VM -Id $NetworkAdapter.VmId).NetworkAdapter | Select-Object Name, IpAddress
193
- }
194
-
195
- function Get-VmDetail {
196
- [cmdletbinding()]
197
- param($Id)
198
-
199
- Get-VM -Id $Id |
200
- ForEach-Object {
201
- $vm = $_
202
- do {
203
- Start-Sleep -Seconds 1
204
- }
205
- while (-not (Get-VmIP $vm))
206
-
207
- [pscustomobject]@{
208
- Name = $vm.name
209
- Id = $vm.ID
210
- IpAddress = (Get-VmIP $vm)
211
- }
212
- }
213
- }
214
-
215
- function Get-DefaultVMSwitch {
216
- [CmdletBinding()]
217
- param ($Name)
218
- Get-VMSwitch @PSBoundParameters |
219
- Select-Object -First 1 |
220
- Select-Object Name, Id
221
- }
222
-
223
- function Mount-VMISO {
224
- [cmdletbinding()]
225
- param($Id, $Path)
226
-
227
- if ((Get-VM -Id $Id).Generation -eq 2) {
228
- Add-VMDvdDrive (Get-VM -Id $Id).Name | Set-VMDvdDrive -VMName (Get-VM -Id $Id).Name -Path $Path
229
- }
230
-
231
- Set-VMDvdDrive -VMName (Get-VM -Id $Id).Name -Path $Path
232
- }
233
-
1
+ #requires -Version 2 -Modules Hyper-V
2
+
3
+ #implicitly import hyperv module to avoid powercli cmdlets
4
+ if ((Get-Module -Name 'hyper-v') -ne $null) {
5
+ Remove-Module -Name hyper-v
6
+ Import-Module -Name hyper-v
7
+ }
8
+ else {
9
+ Import-Module -Name hyper-v
10
+ }
11
+
12
+ $ProgressPreference = 'SilentlyContinue'
13
+
14
+
15
+ function New-DifferencingDisk {
16
+ [cmdletbinding()]
17
+ param (
18
+ [parameter(Mandatory)]
19
+ [ValidateNotNullOrEmpty()]
20
+ [string[]]$Path,
21
+ [parameter(Mandatory)]
22
+ [ValidateNotNullOrEmpty()]
23
+ [string]$ParentPath
24
+ )
25
+ if (-not (Test-Path $Path)) {
26
+ $null = new-vhd @psboundparameters -Differencing
27
+ }
28
+ }
29
+
30
+ function Assert-VmRunning {
31
+ [cmdletbinding()]
32
+ param([string]$Id)
33
+
34
+ if ([string]::IsNullOrEmpty($Id)) {
35
+ $Output = [pscustomobject]@{
36
+ Name = ''
37
+ State = ''
38
+ }
39
+ }
40
+ else {
41
+ $Output = Get-VM -Id $Id |
42
+ ForEach-Object -Process {
43
+ if ($_.State -notlike 'Running') {
44
+ $_ |
45
+ Start-VM -passthru
46
+ }
47
+ else {
48
+ $_
49
+ }
50
+ } |
51
+ Select-Object -Property Name, Id, State
52
+ }
53
+ $Output
54
+ }
55
+
56
+ function New-KitchenVM {
57
+ [cmdletbinding()]
58
+ param (
59
+ $Generation = 1,
60
+ $DisableSecureBoot,
61
+ $MemoryStartupBytes,
62
+ $StaticMacAddress,
63
+ $Name,
64
+ $Path,
65
+ $VHDPath,
66
+ $SwitchName,
67
+ $VlanId,
68
+ $ProcessorCount,
69
+ $UseDynamicMemory,
70
+ $DynamicMemoryMinBytes,
71
+ $DynamicMemoryMaxBytes,
72
+ $boot_iso_path,
73
+ $EnableGuestServices,
74
+ $AdditionalDisks
75
+ )
76
+ $null = $psboundparameters.remove('DisableSecureBoot')
77
+ $null = $psboundparameters.remove('ProcessorCount')
78
+ $null = $psboundparameters.remove('StaticMacAddress')
79
+ $null = $psboundparameters.remove('UseDynamicMemory')
80
+ $null = $psboundparameters.remove('DynamicMemoryMinBytes')
81
+ $null = $psboundparameters.remove('DynamicMemoryMaxBytes')
82
+ $null = $psboundparameters.remove('boot_iso_path')
83
+ $null = $psboundparameters.remove('EnableGuestServices')
84
+ $null = $psboundparameters.remove('VlanId')
85
+ $null = $psboundparameters.remove('AdditionalDisks')
86
+ $DisableSecureBoot = [Convert]::ToBoolean($DisableSecureBoot)
87
+ $UseDynamicMemory = [Convert]::ToBoolean($UseDynamicMemory)
88
+ $null = [bool]::TryParse($EnableGuestServices, [ref]$EnableGuestServices)
89
+
90
+ $vm = new-vm @psboundparameters |
91
+ Set-Vm -ProcessorCount $ProcessorCount -passthru
92
+
93
+ if ($UseDynamicMemory) {
94
+ $vm | Set-VMMemory -DynamicMemoryEnabled $true -MinimumBytes $DynamicMemoryMinBytes -MaximumBytes $DynamicMemoryMaxBytes
95
+ }
96
+ else {
97
+ $vm | Set-VMMemory -DynamicMemoryEnabled $false
98
+ }
99
+ if (-not [string]::IsNullOrEmpty($boot_iso_path)) {
100
+ Mount-VMISO -Id $vm.Id -Path $boot_iso_path
101
+ }
102
+ if (-not [string]::IsNullOrEmpty($StaticMacAddress)) {
103
+ Set-VMNetworkAdapter -VMName $vm.VMName -StaticMacAddress $StaticMacAddress
104
+ }
105
+ if ($EnableGuestServices -and (Get-command Enable-VMIntegrationService -ErrorAction SilentlyContinue)) {
106
+ Enable-VMIntegrationService -VM $vm -Name 'Guest Service Interface'
107
+ }
108
+ if (($VlanId -ne $null) -and (Get-command Set-VMNetworkAdapterVlan -ErrorAction SilentlyContinue)) {
109
+ Set-VMNetworkAdapterVlan -VM $vm -Access -VlanId $VlanId
110
+ }
111
+ if ($AdditionalDisks -and (Get-command Add-VMHardDiskDrive -ErrorAction SilentlyContinue)) {
112
+ foreach ($AdditionalDisk in $AdditionalDisks) {
113
+ Add-VMHardDiskDrive -VM $vm -Path $AdditionalDisk
114
+ }
115
+ }
116
+ if ($DisableSecureBoot -and ($Generation -eq 2) -and (Get-command Set-VMFirmware -ErrorAction SilentlyContinue)) {
117
+ Set-VMFirmware -VM $vm -EnableSecureBoot Off
118
+ }
119
+ if ((Get-Command -Name Set-Vm).Parameters["AutomaticCheckpointsEnabled"]) {
120
+ Set-VM -Name $vm.VMName -AutomaticCheckpointsEnabled $false
121
+ }
122
+ $vm | Start-Vm -passthru |
123
+ foreach {
124
+ $vm = $_
125
+ do {
126
+ start-sleep -seconds 2
127
+ }
128
+ while ($vm.state -notlike 'Running')
129
+ $vm
130
+ } |
131
+ select Name, Id, State
132
+ }
133
+
134
+ function Get-VmIP($vm) {
135
+ start-sleep -seconds 10
136
+ $vm.networkadapters.ipaddresses |
137
+ Where-Object {
138
+ $_ -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
139
+ } |
140
+ Select-Object -First 1
141
+ }
142
+
143
+ Function Set-VMNetworkConfiguration {
144
+ [CmdletBinding()]
145
+ Param (
146
+ [parameter(valuefrompipeline)]
147
+ [object]$NetworkAdapter,
148
+ [String[]]$IPAddress = @(),
149
+ [String[]]$Gateway = @(),
150
+ [String[]]$DNSServers = @(),
151
+ [String[]]$Subnet = @()
152
+ )
153
+
154
+ $vm = Get-CimInstance -Namespace 'root\virtualization\v2' -ClassName 'Msvm_ComputerSystem' -Filter "ElementName = '$($NetworkAdapter.VMName)'"
155
+
156
+ $VMSettings = Get-CimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' |
157
+ Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' }
158
+
159
+ $VMNetAdapters = Get-CimAssociatedInstance -InputObject $VMSettings -ResultClassName 'Msvm_SyntheticEthernetPortSettingData'
160
+
161
+ $NetworkSettings = @()
162
+ foreach ($NetAdapter in $VMNetAdapters) {
163
+ if ($NetAdapter.Address -eq $NetworkAdapter.MacAddress) {
164
+ $NetworkSettings += Get-CimAssociatedInstance -InputObject $NetAdapter -ResultClassName 'Msvm_GuestNetworkAdapterConfiguration'
165
+ }
166
+ }
167
+
168
+ if ($NetworkSettings.Count -eq 0) {
169
+ throw "No guest network adapter configuration found for MAC address $($NetworkAdapter.MacAddress) on VM $($NetworkAdapter.VMName)."
170
+ }
171
+
172
+ $NetworkSettings[0].IPAddresses = $IPAddress
173
+ $NetworkSettings[0].DefaultGateways = $Gateway
174
+ $NetworkSettings[0].DNSServers = $DNSServers
175
+ $NetworkSettings[0].Subnets = $Subnet
176
+ $NetworkSettings[0].ProtocolIFType = 4096
177
+ $NetworkSettings[0].DHCPEnabled = $false
178
+
179
+
180
+ $Service = Get-CimInstance -Namespace 'root\virtualization\v2' -ClassName 'Msvm_VirtualSystemManagementService'
181
+
182
+ # NetworkConfiguration is declared string[] of embedded instances; the CIM
183
+ # layer serializes the CimInstance for us, replacing WMI's GetText(1).
184
+ $setIP = Invoke-CimMethod -InputObject $Service -MethodName 'SetGuestNetworkAdapterConfiguration' -Arguments @{
185
+ ComputerSystem = $vm
186
+ NetworkConfiguration = @($NetworkSettings[0])
187
+ }
188
+
189
+ # 4096 means the method was accepted and is running as a job.
190
+ if ($setIP.ReturnValue -eq 4096) {
191
+ $job = $setIP.Job | Get-CimInstance
192
+
193
+ # 3 = Starting, 4 = Running.
194
+ while ($job.JobState -eq 3 -or $job.JobState -eq 4) {
195
+ Start-Sleep -Seconds 1
196
+ $job = $job | Get-CimInstance
197
+ }
198
+
199
+ # 7 = Completed. Anything else previously emitted the error and carried
200
+ # on, which hid a failed address assignment behind a successful create.
201
+ if ($job.JobState -ne 7) {
202
+ throw "Setting the guest network adapter configuration failed: $($job.ErrorDescription)"
203
+ }
204
+ }
205
+ elseif ($setIP.ReturnValue -ne 0) {
206
+ throw "Setting the guest network adapter configuration failed with return value $($setIP.ReturnValue)."
207
+ }
208
+
209
+ (Get-VM -Id $NetworkAdapter.VmId).NetworkAdapters | Select-Object Name, IPAddresses
210
+ }
211
+
212
+ function Get-VmDetail {
213
+ [cmdletbinding()]
214
+ param($Id)
215
+
216
+ Get-VM -Id $Id |
217
+ ForEach-Object {
218
+ $vm = $_
219
+ do {
220
+ Start-Sleep -Seconds 1
221
+ }
222
+ while (-not (Get-VmIP $vm))
223
+
224
+ [pscustomobject]@{
225
+ Name = $vm.name
226
+ Id = $vm.ID
227
+ IpAddress = (Get-VmIP $vm)
228
+ }
229
+ }
230
+ }
231
+
232
+ function Get-VmStatus {
233
+ [cmdletbinding()]
234
+ param($Id)
235
+
236
+ Get-VM -Id $Id -ErrorAction SilentlyContinue |
237
+ ForEach-Object {
238
+ [pscustomobject]@{
239
+ Name = $_.Name
240
+ Id = [string]$_.Id
241
+ State = [string]$_.State
242
+ }
243
+ }
244
+ }
245
+
246
+ function Get-DefaultVMSwitch {
247
+ [CmdletBinding()]
248
+ param ($Name)
249
+ Get-VMSwitch @PSBoundParameters |
250
+ Select-Object -First 1 |
251
+ Select-Object Name, Id
252
+ }
253
+
254
+ function Mount-VMISO {
255
+ [cmdletbinding()]
256
+ param($Id, $Path)
257
+
258
+ if ((Get-VM -Id $Id).Generation -eq 2) {
259
+ Add-VMDvdDrive (Get-VM -Id $Id).Name | Set-VMDvdDrive -VMName (Get-VM -Id $Id).Name -Path $Path
260
+ }
261
+
262
+ Set-VMDvdDrive -VMName (Get-VM -Id $Id).Name -Path $Path
263
+ }
264
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-hyperv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.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-08-22 00:00:00.000000000 Z
11
+ date: 2026-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -30,7 +30,7 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.4'
33
+ version: '3.0'
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
36
  version: '5'
@@ -40,7 +40,7 @@ dependencies:
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: '1.4'
43
+ version: '3.0'
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '5'