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:

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



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

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



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/kitchen/driver/powershell.rb', line 341

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



263
264
265
266
267
268
269
270
# File 'lib/kitchen/driver/powershell.rb', line 263

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



50
51
52
53
# File 'lib/kitchen/driver/powershell.rb', line 50

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



195
196
197
198
199
200
# File 'lib/kitchen/driver/powershell.rb', line 195

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



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/kitchen/driver/powershell.rb', line 141

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

#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.

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

Returns:

  • (Boolean)


75
76
77
78
79
# File 'lib/kitchen/driver/powershell.rb', line 75

def is_32bit?
  os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
  ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
  os_arch != "AMD64" && ruby_arch == 32
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.

Whether a 64-bit PowerShell is directly reachable.

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

Returns:

  • (Boolean)


62
63
64
65
66
67
68
# File 'lib/kitchen/driver/powershell.rb', line 62

def is_64bit?
  return true if remote_hyperv

  os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
  ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
  os_arch == "AMD64" && ruby_arch == 64
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



306
307
308
309
310
# File 'lib/kitchen/driver/powershell.rb', line 306

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



184
185
186
187
188
189
# File 'lib/kitchen/driver/powershell.rb', line 184

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



171
172
173
174
175
176
# File 'lib/kitchen/driver/powershell.rb', line 171

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



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/kitchen/driver/powershell.rb', line 206

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

#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)


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

def powershell_64_bit
  if is_64bit? || is_32bit?
    '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



316
317
318
319
320
# File 'lib/kitchen/driver/powershell.rb', line 316

def resize_vhd
  <<-VMNOTE
    Resize-VHD -Path "#{parent_vhd_path}" -SizeBytes #{config[:resize_vhd]}
  VMNOTE
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



125
126
127
128
129
130
131
# File 'lib/kitchen/driver/powershell.rb', line 125

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



162
163
164
# File 'lib/kitchen/driver/powershell.rb', line 162

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



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/kitchen/driver/powershell.rb', line 276

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



326
327
328
329
330
# File 'lib/kitchen/driver/powershell.rb', line 326

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



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

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



252
253
254
255
256
257
# File 'lib/kitchen/driver/powershell.rb', line 252

def vm_details_ps
  <<-DETAILS

    Get-VmDetail -id "#{@state[:id]}" | ConvertTo-Json
  DETAILS
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



106
107
108
109
110
111
112
# File 'lib/kitchen/driver/powershell.rb', line 106

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