Module: Kitchen::Driver::HypervHelpers

Included in:
Vagrant
Defined in:
lib/kitchen/driver/helpers.rb

Overview

Helpers for talking to Hyper-V from the Vagrant driver.

Hyper-V has no command line interface of its own, so everything here funnels through PowerShell: a script is wrapped so that it dot-sources the bundled support/hyperv.ps1, base64-encoded for -EncodedCommand (which sidesteps all shell quoting problems), executed, and its JSON output parsed back into Ruby.

The module expects to be mixed into a Configurable that also provides Logging, which the Vagrant driver does.

Author:

Instance Method Summary collapse

Instance Method Details

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

Encodes a PowerShell script for powershell.exe -EncodedCommand.

PowerShell requires UTF-16LE, not UTF-8, and strict (unwrapped) base64.

Parameters:

  • script (String)

    a PowerShell script

Returns:

  • (String)

    the script as single-line base64



46
47
48
49
# File 'lib/kitchen/driver/helpers.rb', line 46

def encode_command(script)
  encoded_script = script.encode("UTF-16LE", "UTF-8")
  Base64.strict_encode64(encoded_script)
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.

Runs a command locally and parses its output as JSON.

Parameters:

  • cmd (String)

    command to run locally

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

    options passed through to Mixlib::ShellOut

Returns:

  • (Hash, Array, nil)

    the parsed output, or nil if there was none

Raises:

  • (RuntimeError)

    if the command exited non-zero



147
148
149
150
151
152
153
154
155
156
# File 'lib/kitchen/driver/helpers.rb', line 147

def execute_command(cmd, options = {})
  debug("#Local Command BEGIN (#{cmd})")
  sh = Mixlib::ShellOut.new(cmd, options)
  sh.run_command
  debug("Local Command END #{Util.duration(sh.execution_time)}")
  raise "Failed: #{sh.stderr}" if sh.error?

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

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

The PowerShell that #hyperv_switch runs. Honours KITCHEN_HYPERV_SWITCH; without it, Get-DefaultVMSwitch picks one.

Returns:

  • (String)

    a PowerShell script emitting a JSON switch object



189
190
191
192
193
# File 'lib/kitchen/driver/helpers.rb', line 189

def hyperv_default_switch_ps
  <<-VMSWITCH
    Get-DefaultVMSwitch #{ENV["KITCHEN_HYPERV_SWITCH"]} | ConvertTo-Json
  VMSWITCH
end

#hyperv_switchString

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.

Asks Hyper-V for the virtual switch new VMs should be attached to.

Returns:

  • (String)

    the name of the switch

Raises:

  • (RuntimeError)

    if no usable switch could be determined



173
174
175
176
177
178
179
180
181
182
# File 'lib/kitchen/driver/helpers.rb', line 173

def hyperv_switch
  default_switch_object = run_ps hyperv_default_switch_ps
  if default_switch_object.nil? ||
      !default_switch_object.key?("Name") ||
      default_switch_object["Name"].empty?
    raise "Failed to find a default VM Switch."
  end

  default_switch_object["Name"]
end

#is_32bit?true, false

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 the running Ruby are 32-bit.

Note this is deliberately not the negation of #is_64bit?: a 32-bit Ruby on a 64-bit Windows (the WOW64 case) is neither.

Returns:

  • (true, false)

    whether this is a 32-bit Ruby on a 32-bit Windows



88
89
90
# File 'lib/kitchen/driver/helpers.rb', line 88

def is_32bit?
  os_architecture != "AMD64" && !ruby_64bit?
end

#is_64bit?true, false

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 the running Ruby are 64-bit.

Returns:

  • (true, false)

    whether this is a 64-bit Ruby on a 64-bit Windows



77
78
79
# File 'lib/kitchen/driver/helpers.rb', line 77

def is_64bit?
  os_architecture == "AMD64" && ruby_64bit?
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 processor architecture Windows reports for this process.

PROCESSOR_ARCHITEW6432 is only set for a 32-bit process running under WOW64, where it holds the machine's architecture while PROCESSOR_ARCHITECTURE holds the emulated one -- so it wins when present.

Returns:

  • (String, nil)

    e.g. "AMD64", or nil off Windows



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

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 matches the machine's architecture.

When Ruby and Windows agree on bitness, the real System32 PowerShell is correct. When they disagree -- a 32-bit Ruby on 64-bit Windows -- System32 would be silently redirected by WOW64 to the 32-bit PowerShell, which cannot see the Hyper-V cmdlets; Sysnative is the alias that reaches the 64-bit one.

Returns:

  • (String)

    absolute path to powershell.exe



102
103
104
105
106
107
108
# File 'lib/kitchen/driver/helpers.rb', line 102

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

#ruby_64bit?true, false

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 the running Ruby is a 64-bit build, determined from the size of a packed pointer.

Returns:

  • (true, false)

    whether this Ruby is 64-bit



69
70
71
# File 'lib/kitchen/driver/helpers.rb', line 69

def ruby_64bit?
  ["foo"].pack("p").size != 4
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.

Convenience method to run a powershell command locally.

Parameters:

  • cmd (String)

    command to run locally

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

    options hash

Returns:

  • (Hash, nil)

    the parsed JSON the script emitted, if any

See Also:

  • ShellOut.run_command


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

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.

Strips the interactive prompt lines PowerShell interleaves with real output, which would otherwise make the result unparseable as JSON.

Parameters:

  • stdout (String)

    raw standard output

Returns:

  • (String)

    output with PS ...> lines removed



164
165
166
# File 'lib/kitchen/driver/helpers.rb', line 164

def sanitize_stdout(stdout)
  stdout.split("\n").select { |s| !s.start_with?("PS") }.join("\n")
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.

Builds a full powershell.exe command line that dot-sources the gem's support/hyperv.ps1 helper functions and then runs script.

Parameters:

  • script (String)

    a PowerShell script

Returns:

  • (String)

    a command line ready to hand to a shell



116
117
118
119
120
121
122
123
# File 'lib/kitchen/driver/helpers.rb', line 116

def wrap_command(script)
  base_script_path = File.join(File.dirname(__FILE__), "/../../../support/hyperv.ps1")
  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