Module: Kitchen::Driver::PowerShellScripts

Included in:
Hyperv
Defined in:
lib/kitchen/driver/powershell.rb

Overview

PowerShell generation and execution for Hyperv.

Every method here is either a script generator -- a *_ps method returning PowerShell source -- or part of the pipeline that runs one: #run_ps wraps the script so it dot-sources support/hyperv.ps1, #encode_command encodes it for powershell.exe -encodedcommand, and #execute_command runs it over the Train connection and parses the JSON that comes back.

Encoding sidesteps every layer of quoting between Ruby and PowerShell, which matters because these scripts embed Windows paths and user-supplied strings.

The module reads config, instance and @state from the driver it is mixed into, so it is not usable standalone.

See Also:

Constant Summary collapse

SIXTY_FOUR_BIT_ARCHITECTURES =

Values Windows reports in PROCESSOR_ARCHITECTURE for a 64-bit OS.

ARM64 matters for Windows on ARM devices, which run Hyper-V: matching only AMD64 there made both width checks false and sent the driver to the Sysnative path, which does not exist for a native 64-bit process.

%w{AMD64 ARM64 IA64}.freeze

Instance Method Summary collapse

Instance Method Details

#additional_disksString?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The AdditionalDisks entry spliced into #new_vm_ps.

Reads the paths Hyperv#create_additional_disks recorded, so it is only meaningful after that has run.

Returns:

  • (String, nil)

    the parameter line, or nil when no additional disks are configured



282
283
284
285
286
287
288
# File 'lib/kitchen/driver/powershell.rb', line 282

def additional_disks
  return if config[:additional_disks].nil?

  <<-EOH
  AdditionalDisks = @("#{@additional_disk_objects.join('","')}")
  EOH
end

#copy_vm_file_ps(source, dest) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that copies a file or directory into the running guest.

Enables the guest service interface first if it is off, and walks a directory source file by file since Copy-VMFile handles only files.

Parameters:

  • source (String)

    path on the Hyper-V host

  • dest (String)

    path inside the guest

Returns:

  • (String)

    PowerShell source



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/kitchen/driver/powershell.rb', line 412

def copy_vm_file_ps(source, dest)
  <<-FILECOPY
    Function CopyFile ($VM, [string]$SourcePath, [string]$DestPath) {
        $p = @{ CreateFullPath = $true ; FileSource = 'Host'; Force = $true }
        $VM |
          Copy-VMFile -SourcePath $SourcePath -DestinationPath $DestPath @p
    }

    $sourceLocation = '#{source}'
    $destinationLocation = '#{dest}'
    $vmId = '#{@state[:id]}'
    If (Test-Path $sourceLocation) {
        $vm = Get-VM -ID $vmId
        $service = 'Guest Service Interface'

        If ((Get-VMIntegrationService -Name $service -VM $vm).Enabled -ne $true) {
            Enable-VMIntegrationService -Name $service -VM $vm
            Start-Sleep -Seconds 3
        }

        If ((Get-Item $sourceLocation) -is [System.IO.DirectoryInfo]) {
            ForEach ($item in (Get-ChildItem -Path $sourceLocation -File)) {
                $destFullPath = (Join-Path $destinationLocation $item.Name)
                CopyFile $vm $item.FullName $destFullPath
            }
        }
        Else {
          CopyFile $vm $sourceLocation $destinationLocation
        }
    }
    else {
        Write-Error "Source file path does not exist: $sourceLocation"
    }
  FILECOPY
end

#delete_vm_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that forces the VM off and removes it.

Returns:

  • (String)

    PowerShell source



334
335
336
337
338
339
340
341
# File 'lib/kitchen/driver/powershell.rb', line 334

def delete_vm_ps
  <<-REMOVE

    $null = Get-VM -ID "#{@state[:id]}" |
      Stop-VM -Force -TurnOff -PassThru |
      Remove-VM -Force
  REMOVE
end

#encode_command(script) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Encode a script the way powershell.exe -encodedcommand expects it: UTF-16LE, then Base64.

Parameters:

  • script (String)

    UTF-8 PowerShell source

Returns:

  • (String)

    strict Base64, with no line breaks



59
60
61
62
# File 'lib/kitchen/driver/powershell.rb', line 59

def encode_command(script)
  encoded_script = script.encode("UTF-16LE", "UTF-8")
  Base64.strict_encode64(encoded_script)
end

#ensure_vm_running_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that confirms the VM exists and starts it if it is stopped.

Returns:

  • (String)

    PowerShell source



238
239
240
241
242
243
# File 'lib/kitchen/driver/powershell.rb', line 238

def ensure_vm_running_ps
  <<-RUNNING

    Assert-VmRunning -ID "#{@state[:id]}" | ConvertTo-Json
  RUNNING
end

#execute_command(cmd, options = {}) ⇒ Hash, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Run a prepared command line and parse its output.

Parameters:

  • cmd (String)

    the full command line from #wrap_command

  • options (Hash) (defaults to: {})

    options passed through to the Train connection

Returns:

  • (Hash, Array, nil)

    the parsed JSON output, or nil when the script produced none

Raises:

  • (RuntimeError)

    if the command exits non-zero



184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/kitchen/driver/powershell.rb', line 184

def execute_command(cmd, options = {})
  debug("#Command BEGIN (#{cmd})")

  sh = nil
  bm = Benchmark.measure do
    sh = connection.run_command(cmd, options)
  end

  debug("Command END #{Util.duration(bm.total)}")
  raise "Failed: #{sh.stderr}" if sh.exit_status != 0

  stdout = sanitize_stdout(sh.stdout)
  JSON.parse(stdout) if stdout.length > 2
end

#hyperv_module_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that reports whether the Hyper-V PowerShell module is installed.

Returns:

  • (String)

    PowerShell source



320
321
322
323
324
325
326
327
328
# File 'lib/kitchen/driver/powershell.rb', line 320

def hyperv_module_ps
  <<-MODULE

    Get-Module -ListAvailable -Name Hyper-V |
      Select-Object -First 1 |
      ForEach-Object { [pscustomobject]@{ Name = $_.Name; Version = [string]$_.Version } } |
      ConvertTo-Json
  MODULE
end

#is_32bit?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Deprecated.

Use #thirty_two_bit?. Kept because this module is mixed into a published driver class.

Returns:

  • (Boolean)


120
121
122
# File 'lib/kitchen/driver/powershell.rb', line 120

def is_32bit?
  thirty_two_bit?
end

#is_64bit?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Deprecated.

Use #sixty_four_bit?. Kept because this module is mixed into a published driver class.

Returns:

  • (Boolean)


112
113
114
# File 'lib/kitchen/driver/powershell.rb', line 112

def is_64bit?
  sixty_four_bit?
end

#mount_vm_isoString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that attaches the configured ISO to the VM's DVD drive.

Returns:

  • (String)

    PowerShell source



377
378
379
380
381
# File 'lib/kitchen/driver/powershell.rb', line 377

def mount_vm_iso
  <<-MOUNTISO
    mount-vmiso -id "#{@state[:id]}" -Path #{config[:iso_path]}
  MOUNTISO
end

#new_additional_disk_ps(disk_path, disk_size) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that creates one additional data disk.

Parameters:

  • disk_path (String)

    full path of the disk to create

  • disk_size (Integer)

    size in gigabytes

Returns:

  • (String)

    PowerShell source



227
228
229
230
231
232
# File 'lib/kitchen/driver/powershell.rb', line 227

def new_additional_disk_ps(disk_path, disk_size)
  <<-ADDDISK

    New-VHD -Path "#{disk_path}" -SizeBytes #{disk_size}GB | Out-Null
  ADDDISK
end

#new_differencing_disk_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that clones the parent VHD into this instance's differencing disk.

Returns:

  • (String)

    PowerShell source



214
215
216
217
218
219
# File 'lib/kitchen/driver/powershell.rb', line 214

def new_differencing_disk_ps
  <<-DIFF

    New-DifferencingDisk -Path "#{differencing_disk_path}" -ParentPath "#{parent_vhd_path}"
  DIFF
end

#new_vm_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that creates the VM from the current configuration.

Returns:

  • (String)

    PowerShell source



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/kitchen/driver/powershell.rb', line 249

def new_vm_ps
  <<-NEWVM

    $NewVMParams = @{
      Generation = #{config[:vm_generation]}
      DisableSecureBoot = "#{config[:disable_secureboot]}"
      MemoryStartupBytes = #{config[:memory_startup_bytes]}
      StaticMacAddress = "#{config[:static_mac_address]}"
      Name = "#{instance.name}"
      Path = "#{kitchen_vm_path}"
      VHDPath = "#{differencing_disk_path}"
      SwitchName = "#{config[:vm_switch]}"
      VlanId = #{config[:vm_vlan_id] || "$null"}
      ProcessorCount = #{config[:processor_count]}
      UseDynamicMemory = "#{config[:dynamic_memory]}"
      DynamicMemoryMinBytes = #{config[:dynamic_memory_min_bytes]}
      DynamicMemoryMaxBytes = #{config[:dynamic_memory_max_bytes]}
      boot_iso_path = "#{boot_iso_path}"
      EnableGuestServices = "#{config[:enable_guest_services]}"
      #{additional_disks}
    }
    New-KitchenVM @NewVMParams | ConvertTo-Json
  NEWVM
end

#os_architectureString?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The OS architecture, seeing through WOW64.

A 32-bit process on 64-bit Windows reads its own architecture from PROCESSOR_ARCHITECTURE; PROCESSOR_ARCHITEW6432 is what reveals the real one, and is only set in that case.

Returns:

  • (String, nil)


72
73
74
# File 'lib/kitchen/driver/powershell.rb', line 72

def os_architecture
  ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
end

#powershell_64_bitString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Path to a PowerShell that can see the Hyper-V cmdlets.

When a 32-bit Ruby runs on 64-bit Windows the WOW64 filesystem redirector rewrites system32 to SysWOW64, which would launch a 32-bit PowerShell with no Hyper-V module. sysnative is the virtual path that escapes redirection.

Returns:

  • (String)


133
134
135
136
137
138
139
# File 'lib/kitchen/driver/powershell.rb', line 133

def powershell_64_bit
  if sixty_four_bit? || thirty_two_bit?
    'c:\windows\system32\windowspowershell\v1.0\powershell.exe'
  else
    'c:\windows\sysnative\windowspowershell\v1.0\powershell.exe'
  end
end

#resize_vhdString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that grows the parent VHD to the configured size.

Returns:

  • (String)

    PowerShell source



387
388
389
390
391
# File 'lib/kitchen/driver/powershell.rb', line 387

def resize_vhd
  <<-VMNOTE
    Resize-VHD -Path "#{parent_vhd_path}" -SizeBytes #{config[:resize_vhd]}
  VMNOTE
end

#ruby_architecture_bitsInteger

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Pointer width of the running Ruby, in bits.

Returns:

  • (Integer)

    32 or 64



80
81
82
# File 'lib/kitchen/driver/powershell.rb', line 80

def ruby_architecture_bits
  RbConfig::SIZEOF.fetch("void*", 8) * 8
end

#run_ps(cmd, options = {}) ⇒ Hash, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Run a PowerShell script on the Hyper-V host.

With dry_run set the script is echoed rather than executed, which is the quickest way to see exactly what the driver would have run.

Parameters:

  • cmd (String)

    PowerShell source

  • options (Hash) (defaults to: {})

    options passed through to the Train connection

Returns:

  • (Hash, Array, nil)

    the parsed JSON output, or nil when the script produced none

Raises:

  • (RuntimeError)

    if the script exits non-zero



168
169
170
171
172
173
174
# File 'lib/kitchen/driver/powershell.rb', line 168

def run_ps(cmd, options = {})
  cmd = "echo #{cmd}" if config[:dry_run]
  debug("Preparing to run: ")
  debug("  #{cmd}")
  wrapped_command = wrap_command cmd
  execute_command wrapped_command, options
end

#sanitize_stdout(stdout) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Strip the interactive prompt lines PowerShell interleaves with output, which would otherwise make the result invalid JSON.

Parameters:

  • stdout (String)

    raw stdout from the host

Returns:

  • (String)

    stdout with prompt lines removed



205
206
207
# File 'lib/kitchen/driver/powershell.rb', line 205

def sanitize_stdout(stdout)
  stdout.split("\n").select { |s| !s.start_with?("PS") }.join("\n")
end

#set_vm_ipaddress_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that assigns the VM a static address once its adapter is up.

Returns:

  • (String)

    PowerShell source



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/kitchen/driver/powershell.rb', line 347

def set_vm_ipaddress_ps
  <<-VMIP

    while ((Get-VM -id "#{@state[:id]}").NetworkAdapters[0].Status -ne 'Ok'){
      start-sleep 10
    }

    (Get-VM -id "#{@state[:id]}").NetworkAdapters |
      Set-VMNetworkConfiguration -ipaddress "#{config[:ip_address]}" `
        -subnet "#{config[:subnet]}" `
        -gateway "#{config[:gateway]}" `
        -dnsservers #{ruby_array_to_ps_array(config[:dns_servers])} |
      ConvertTo-Json
  VMIP
end

#set_vm_noteString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that writes the configured note onto the VM.

Returns:

  • (String)

    PowerShell source



397
398
399
400
401
# File 'lib/kitchen/driver/powershell.rb', line 397

def set_vm_note
  <<-VMNOTE
    Set-VM -Name (Get-VM | Where-Object{ $_.ID -eq "#{@state[:id]}"}).Name -Note "#{config[:vm_note]}"
  VMNOTE
end

#sixty_four_bit?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether a 64-bit PowerShell is directly reachable.

Always true for a remote host, where the local architecture is irrelevant.

Returns:

  • (Boolean)


91
92
93
94
95
96
# File 'lib/kitchen/driver/powershell.rb', line 91

def sixty_four_bit?
  return true if remote_hyperv

  SIXTY_FOUR_BIT_ARCHITECTURES.include?(os_architecture) &&
    ruby_architecture_bits == 64
end

#thirty_two_bit?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether both the OS and Ruby are 32-bit, so no WOW64 redirection is in play.

Returns:

  • (Boolean)


103
104
105
106
# File 'lib/kitchen/driver/powershell.rb', line 103

def thirty_two_bit?
  !SIXTY_FOUR_BIT_ARCHITECTURES.include?(os_architecture) &&
    ruby_architecture_bits == 32
end

#vm_default_switch_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that resolves the virtual switch to attach the VM to.

Returns:

  • (String)

    PowerShell source



367
368
369
370
371
# File 'lib/kitchen/driver/powershell.rb', line 367

def vm_default_switch_ps
  <<-VMSWITCH
    Get-DefaultVMSwitch "#{config[:vm_switch]}" | ConvertTo-Json
  VMSWITCH
end

#vm_details_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

TODO:

Report if VM has no IP address instead of silently waiting forever

Script that reads the VM's name, id and IP address.

Returns:

  • (String)

    PowerShell source



295
296
297
298
299
300
# File 'lib/kitchen/driver/powershell.rb', line 295

def vm_details_ps
  <<-DETAILS

    Get-VmDetail -id "#{@state[:id]}" | ConvertTo-Json
  DETAILS
end

#vm_status_psString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Script that reads the VM's current power state without changing it.

Unlike #ensure_vm_running_ps, this never starts a stopped VM, so it is safe for kitchen list --probe.

Returns:

  • (String)

    PowerShell source



309
310
311
312
313
314
# File 'lib/kitchen/driver/powershell.rb', line 309

def vm_status_ps
  <<-STATUS

    Get-VmStatus -Id "#{@state[:id]}" | ConvertTo-Json
  STATUS
end

#wrap_command(script) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Turn a script into a full powershell.exe command line.

Prepends a dot-source of the support script so the helper functions are defined, then encodes the result.

Parameters:

  • script (String)

    PowerShell source

Returns:

  • (String)

    the command line to hand to the connection



149
150
151
152
153
154
155
# File 'lib/kitchen/driver/powershell.rb', line 149

def wrap_command(script)
  debug("Loading functions from #{base_script_path}")
  new_script = [ ". #{base_script_path}", "#{script}" ].join(";\n")
  debug("Wrapped script: #{new_script}")
  "#{powershell_64_bit} -noprofile -executionpolicy bypass" \
  " -encodedcommand #{encode_command new_script} -outputformat Text"
end