class Chef::Provisioning::OpenNebulaDriver::Driver

A Driver instance represents a place where machines can be created and found, and contains methods to create, delete, start, stop, and find them.

For AWS, a Driver instance corresponds to a single account. For Vagrant, it is a directory where VM files are found.

How to Make a Driver

To implement a Driver, you must implement the following methods:

Optionally, you can also implement:

Additionally, you must create a file named `chef/provisioning/driver_init/<scheme>.rb`, where <scheme> is the name of the scheme you chose for your driver_url. This file, when required, must call Chef::Provisioning.add_registered_driver(<scheme>, <class>). The given <class>.from_url(url, config) will be called with a driver_url and configuration.

All of these methods must be idempotent - if the work is already done, they just don't do anything.

Attributes

one[R]

Public Class Methods

canonicalize_url(driver_url, config) click to toggle source

URL must have an endpoint to prevent machine moving, which is not possible today between different endpoints.

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 184
def self.canonicalize_url(driver_url, config)
  [driver_url, config]
end
from_url(driver_url, config) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 178
def self.from_url(driver_url, config)
  Driver.new(driver_url, config)
end
new(driver_url, config) click to toggle source

OpenNebula by default reads the following information:

username:password from ENV['ONE_AUTH'] or ENV['HOME']/.one/one_auth
endpoint from ENV['ONE_XMLRPC']

driver_url format:

opennebula:<endpoint>:<profile>

where <profile> points to a one_auth file in ENV/.one/<profile>

driver_options:

credentials: bbsl-auto:text_pass  credentials has precedence over secret_file
endpoint: opennebula endpoint
options: additional options for OpenNebula::Client
Calls superclass method
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 171
def initialize(driver_url, config)
  super
  @one = Chef::Provisioning::OpenNebulaDriver.get_onelib(:driver_url => driver_url)
  @driver_url_with_profile = driver_url
  @driver_url = @one.client.one_endpoint
end

Public Instance Methods

allocate_image(action_handler, image_spec, image_options, machine_spec) click to toggle source

Allocate an image. Returns quickly with an ID that tracks the image.

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::ImageSpec] image_spec

A machine specification representing this machine.

@param [Hash] image_options

A set of options representing the desired state of the machine
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 363
def allocate_image(action_handler, image_spec, image_options, machine_spec)
  if image_spec.reference
    # check if image already exists
    image = @one.get_resource(:image, :id => image_spec.reference['image_id'].to_i)
    action_handler.report_progress "image #{image_spec.name} (ID: #{image_spec.reference['image_id']}) already exists" unless image.nil?
  else
    action_handler.perform_action "create image #{image_spec.name} from machine ID #{machine_spec.reference['instance_id']} with options #{image_options.inspect}" do
      vm = @one.get_resource(:virtualmachine, :id => machine_spec.reference['instance_id'])
      fail "allocate_image: VM does not exist" if vm.nil?
      # set default disk ID
      disk_id = 1
      if image_options.disk_id
        disk_id = image_options.disk_id.is_a?(Integer) ? image_options.disk_id : @one.get_disk_id(vm, new_resource.disk_id)
      end
      new_img = @one.version_ge_4_14 ? vm.disk_saveas(disk_id, image_spec.name) : vm.disk_snapshot(disk_id, image_spec.name, "", true)

      fail "Failed to create snapshot '#{new_resource.name}': #{new_img.message}" if OpenNebula.is_error?(new_img)
      populate_img_object(image_spec, new_image)
    end
  end
end
allocate_load_balancer(_action_handler, _lb_spec, _lb_options, _machine_specs) click to toggle source

Allocate a load balancer @param [ChefMetal::ActionHandler] action_handler The action handler @param [ChefMetal::LoadBalancerSpec] lb_spec Frozen LB specification @param [Hash] lb_options A hash of options to pass the LB @param [Array] machine_specs

An array of machine specs the load balancer should have
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 512
def allocate_load_balancer(_action_handler, _lb_spec, _lb_options, _machine_specs)
  fail "'allocate_load_balancer' is not implemented"
end
allocate_machine(action_handler, machine_spec, machine_options) click to toggle source

Allocate a machine from the underlying service. This method does not need to wait for the machine to boot or have an IP, but it must store enough information in machine_spec.location to find the machine later in ready_machine.

If a machine is powered off or otherwise unusable, this method may start it, but does not need to wait until it is started. The idea is to get the gears moving, but the job doesn't need to be done :)

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::MachineSpec] machine_spec

A machine specification representing this machine.

@param [Hash] machine_options

A set of options representing the desired options when
constructing the machine

@return [Chef::Provisioning::MachineSpec]

Modifies the passed-in machine_spec.  Anything in here will be
saved back after allocate_machine completes.
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 210
def allocate_machine(action_handler, machine_spec, machine_options)
  instance = instance_for(machine_spec)
  return machine_spec unless instance.nil?

  unless machine_options[:bootstrap_options]
    instance = @one.retry_one("Retrying allocate_machine.instance_for (missing bootstrap options)") do
      instance_for(machine_spec)
    end
    return machine_spec unless instance.nil?
    fail "'bootstrap_options' must be specified"
  end
  check_unique_names(machine_options, machine_spec)
  action_handler.perform_action "created vm '#{machine_spec.name}'" do
    Chef::Log.debug(machine_options)
    tpl = @one.get_template(machine_spec.name,
                            machine_options[:bootstrap_options])
    vm = @one.allocate_vm(tpl)
    populate_node_object(machine_spec, machine_options, vm)
    @one.chmod_resource(vm, machine_options[:bootstrap_options][:mode])

    # This option allows to manipulate how the machine shows up
    # in the OpenNebula UI and CLI tools.  We either set the VM
    # name to the short hostname of the machine, rename it
    # to the String passed to us, or leave it alone.
    if machine_options[:vm_name] == :short
      @one.rename_vm(vm, machine_spec.name.split('.').first)
    elsif machine_options[:vm_name].is_a?(String)
      @one.rename_vm(vm, machine_options[:vm_name])
      # else use machine_spec.name for name in OpenNebula
    end
  end
  Chef::Log.debug(machine_spec.reference)

  machine_spec
end
allocate_machines(action_handler, specs_and_options, parallelizer) { |machine_spec| ... } click to toggle source

Allocate a set of machines. This should have the same effect as running allocate_machine on all machine_specs.

Drivers do not need to implement this; the default implementation calls acquire_machine in parallel.

Parallelizing

The parallelizer must implement parallelize @example Example parallelizer

parallelizer.parallelize(specs_and_options) do |machine_spec|
  allocate_machine(action_handler, machine_spec)
end.to_a
# The to_a at the end causes you to wait until the
parallelization is done

This object is shared among other chef-provisioning actions, ensuring that you do not go over parallelization limits set by the user. Use of the parallelizer to parallelizer machines is not required.

Passing a block

If you pass a block to this function, each machine will be yielded to you as it completes, and then the function will return when all machines are yielded.

@example Passing a block

allocate_machines(
  action_handler,
  specs_and_options,
  parallelizer) do |machine_spec|
  ...
end

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method; this
is generally a driver, but could be anything that can support
the interface (i.e., in the case of the test kitchen
provisioning driver for acquiring and destroying VMs).

@param [Hash] specs_and_options

A hash of machine_spec -> machine_options representing the
machines to allocate.

@param [Parallelizer] parallelizer an object with a

parallelize() method that works like this:

@return [Array<Machine>] An array of machine objects created

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 473
def allocate_machines(action_handler, specs_and_options, parallelizer)
  parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
    allocate_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
    yield machine_spec if block_given?
    machine_spec
  end.to_a
end
connect_to_machine(machine_spec, machine_options) click to toggle source

Connect to a machine without allocating or readying it. This method will NOT make any changes to anything, or attempt to wait.

@param [Chef::Provisioning::MachineSpec] machine_spec

MachineSpec representing this machine.

@param [Hash] machine_options @return [Machine] A machine object pointing at the machine, allowing

useful actions like setup, converge, execute, file and directory.
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 292
def connect_to_machine(machine_spec, machine_options)
  machine_for(machine_spec, machine_options)
end
destroy_image(action_handler, image_spec, _image_options) click to toggle source

Destroy an image using this service.

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::ImageSpec] image_spec

A machine specification representing this machine.

@param [Hash] image_options

A set of options representing the desired state of the machine
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 411
def destroy_image(action_handler, image_spec, _image_options)
  img = @one.get_resource(:image, :id => image_spec.location['image_id'].to_i)
  if img.nil?
    action_handler.report_progress "image #{image_spec.name} (#{image_spec.location['image_id']}) does not exist - (up to date)"
  else
    action_handler.perform_action "deleted image #{image_spec.name} (#{image_spec.location['image_id']})" do
      rc = img.delete
      fail "Failed to delete image '#{image_spec.name}' : #{rc.message}" if OpenNebula.is_error?(rc)
    end
  end
end
destroy_load_balancer(_action_handler, _lb_spec, _lb_options) click to toggle source

Destroy the load balancer @param [ChefMetal::ActionHandler] action_handler The action handler @param [ChefMetal::LoadBalancerSpec] lb_spec Frozen LB specification @param [Hash] lb_options A hash of options to pass the LB

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 528
def destroy_load_balancer(_action_handler, _lb_spec, _lb_options)
  fail "'destroy_load_balancer' is not implemented"
end
destroy_machine(action_handler, machine_spec, machine_options) click to toggle source

Delete the given machine – destroy the machine, returning things to the state before allocate_machine was called.

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::MachineSpec] machine_spec

A machine specification representing this machine.

@param [Hash] machine_options

A set of options representing the desired state of the machine
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 305
def destroy_machine(action_handler, machine_spec, machine_options)
  instance = instance_for(machine_spec)
  if !instance.nil?
    action_handler.perform_action "destroyed machine #{machine_spec.name} (#{machine_spec.reference['instance_id']})" do
      instance.delete
      1.upto(10) do
        instance.info
        break if instance.state_str == 'DONE'
        Chef::Log.debug("Waiting for VM '#{instance.id}' to be in 'DONE' state: '#{instance.state_str}'")
        sleep(2)
      end
      fail "Failed to destroy '#{instance.name}'.  Current state: #{instance.state_str}" if instance.state_str != 'DONE'
    end
  elsif machine_spec.reference
    Chef::Log.info("vm #{machine_spec.name} (#{machine_spec.reference['instance_id']}) does not exist - (up to date)")
  else
    Chef::Log.info("vm #{machine_spec.name} does not exist - (up to date)")
  end
  begin
    strategy = convergence_strategy_for(machine_spec, machine_options)
    strategy.cleanup_convergence(action_handler, machine_spec)
  rescue Net::HTTPServerException => e
    raise unless e.response.code == '404'
  end
end
destroy_machines(action_handler, specs_and_options, parallelizer) { |machine_spec| ... } click to toggle source

Delete machines in batch, in parallel if possible.

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 499
def destroy_machines(action_handler, specs_and_options, parallelizer)
  parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
    destroy_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
    yield machine_spec if block_given?
  end.to_a
end
ready_image(action_handler, image_spec, _image_options) click to toggle source

Ready an image, waiting till the point where it is ready to be used.

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::ImageSpec] image_spec

A machine specification representing this machine.

@param [Hash] image_options

A set of options representing the desired state of the machine
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 393
def ready_image(action_handler, image_spec, _image_options)
  img = @one.get_resource(:image, :id => image_spec.reference['image_id'].to_i)
  fail "Image #{image_spec.name} (#{image_spec.reference['image_id']}) does not exist" if img.nil?
  action_handler.perform_action "image #{image_spec.name} is ready" do
    deployed = @one.wait_for_img(img.name, img.id)
    image_spec.reference['state'] = deployed.state_str
  end
  img
end
ready_load_balancer(_action_handler, _lb_spec, _lb_options, _machine_specs) click to toggle source

Make the load balancer ready @param [ChefMetal::ActionHandler] action_handler The action handler @param [ChefMetal::LoadBalancerSpec] lb_spec Frozen LB specification @param [Hash] lb_options A hash of options to pass the LB

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 520
def ready_load_balancer(_action_handler, _lb_spec, _lb_options, _machine_specs)
  fail "'ready_load_balancer' is not implemented"
end
ready_machine(action_handler, machine_spec, machine_options) click to toggle source

Ready a machine, to the point where it is running and accessible via a transport. This will NOT allocate a machine, but may kick it if it is down. This method waits for the machine to be usable, returning a Machine object pointing at the machine, allowing useful actions like setup, converge, execute, file and directory.

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::MachineSpec] machine_spec

A machine specification representing this machine.

@param [Hash] machine_options

A set of options representing the desired state of the machine

@return [Machine] A machine object pointing at the machine, allowing

useful actions like setup, converge, execute, file and directory.
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 263
def ready_machine(action_handler, machine_spec, machine_options)
  instance = instance_for(machine_spec)
  fail "Machine '#{machine_spec.name}' does not have an instance associated with it, or instance does not exist." if instance.nil?

  # TODO: Currently it does not start stopped VMs, it only waits for new VMs to be in RUNNING state
  machine = nil
  action_handler.perform_action "vm '#{machine_spec.name}' is ready" do
    deployed = @one.wait_for_vm(instance.id)
    machine_spec.reference['name'] = deployed.name
    machine_spec.reference['state'] = deployed.state_str
    if deployed.to_hash['VM']['TEMPLATE']['NIC']
      ip = [deployed.to_hash['VM']['TEMPLATE']['NIC']].flatten.first['IP']
    end
    fail "Could not get IP from VM '#{deployed.name}'" if ip.nil? || ip.to_s.empty?
    machine_spec.reference['ip'] = ip
    machine = machine_for(machine_spec, machine_options)
  end
  machine
end
ready_machines(action_handler, specs_and_options, parallelizer) { |machine| ... } click to toggle source

Ready machines in batch, in parallel if possible.

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 482
def ready_machines(action_handler, specs_and_options, parallelizer)
  parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
    machine = ready_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
    yield machine if block_given?
    machine
  end.to_a
end
stop_machine(action_handler, machine_spec, machine_options) click to toggle source

Stop the given machine.

@param [Chef::Provisioning::ActionHandler] action_handler

The action_handler object that is calling this method

@param [Chef::Provisioning::MachineSpec] machine_spec

A machine specification representing this machine.

@param [Hash] machine_options

A set of options representing the desired state of the machine
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 339
def stop_machine(action_handler, machine_spec, machine_options)
  instance = instance_for(machine_spec)
  if !instance.nil?
    action_handler.perform_action "powered off machine #{machine_spec.name} (#{machine_spec.reference['instance_id']})" do
      if machine_spec.reference[:is_shutdown] || (machine_options[:bootstrap_options] && machine_options[:bootstrap_options][:is_shutdown])
        hard = machine_spec.reference[:shutdown_hard] || machine_options[:bootstrap_options][:shutdown_hard] || false
        instance.shutdown(hard)
      else
        instance.stop
      end
    end
  else
    Chef::Log.info("vm #{machine_spec.name} (#{machine_spec.reference['instance_id']}) does not exist - (up to date)")
  end
end
stop_machines(action_handler, specs_and_options, parallelizer) { |machine_spec| ... } click to toggle source

Stop machines in batch, in parallel if possible.

# File lib/chef/provisioning/opennebula_driver/driver.rb, line 491
def stop_machines(action_handler, specs_and_options, parallelizer)
  parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
    stop_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
    yield machine_spec if block_given?
  end.to_a
end

Protected Instance Methods

add_prefix(machine_spec, action_handler) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 678
def add_prefix(machine_spec, action_handler)
  AddPrefixActionHandler.new(action_handler, "[#{machine_spec.name}] ")
end
check_unique_names(machine_options, machine_spec) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 546
def check_unique_names(machine_options, machine_spec)
  return unless machine_options[:bootstrap_options][:unique_names]
  hostname = if machine_options[:vm_name] == :short
               machine_spec.name.split('.').first
             elsif machine_options[:vm_name].is_a?(String)
               machine_options[:vm_name]
             else
               machine_spec.name
             end

  return if @one.get_resource(:virtualmachine, :name => hostname).nil?
  fail "VM with name '#{hostname}' already exists"
end
convergence_strategy_for(machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 665
def convergence_strategy_for(machine_spec, machine_options)
  # TODO: Support Windoze VMs (see chef-provisioning-vagrant)
  convergence_options = Cheffish::MergedConfig.new(machine_options ? machine_options[:convergence_options] || {} : {})

  if !machine_spec.reference
    Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
  elsif machine_options[:cached_installer] == true
    Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
  else
    Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
  end
end
get_private_key(name) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 682
def get_private_key(name)
  Cheffish.get_private_key(name, config)
end
get_ssh_user(machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 630
def get_ssh_user(machine_spec, machine_options)
  # handle ssh_user and ssh_username for backward compatibility
  Chef::Log.warn("':ssh_user' will be deprecated in next version in favour of ':ssh_username'") if machine_options.key?(:ssh_user)
  machine_spec.reference['ssh_username'] || machine_options[:ssh_username] || machine_options[:ssh_user] || 'local'
end
instance_for(machine_spec) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 592
def instance_for(machine_spec)
  instance = nil
  if machine_spec.reference
    current_endpoint, = Chef::Provisioning::OpenNebulaDriver.match_driver_url(machine_spec.driver_url, true)
    fail "Cannot move '#{machine_spec.name}' from #{current_endpoint} to #{driver_url}: machine moving is not supported.  Destroy and recreate." if current_endpoint != driver_url
    instance = @one.get_resource(:virtualmachine, :id => machine_spec.reference['instance_id'].to_i)
    machine_spec.driver_url = @driver_url_with_profile
  elsif machine_spec.location
    current_endpoint, = Chef::Provisioning::OpenNebulaDriver.match_driver_url(machine_spec.driver_url, true)
    fail "Cannot move '#{machine_spec.name}' from #{current_endpoint} to #{driver_url}: machine moving is not supported.  Destroy and recreate." if current_endpoint != driver_url
    instance = @one.get_resource(:virtualmachine, :id => machine_spec.location['server_id'].to_i)
    machine_spec.driver_url = @driver_url_with_profile
    unless instance.nil?
      # Convert from previous driver
      machine_spec.reference = {
          'driver_version' => machine_spec.location['driver_version'],
          'allocated_at' => machine_spec.location['allocated_at'],
          'image_id' => machine_spec.location['image_id'],
          'instance_id' => machine_spec.location['server_id'],
          'name' => machine_spec.location['name'],
          'state' => machine_spec.location['state']
      }
    end
  end
  instance
end
machine_for(machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 619
def machine_for(machine_spec, machine_options)
  instance = instance_for(machine_spec)
  fail "#{machine_spec.name} (#{machine_spec.reference['instance_id']}) does not exist!" if instance.nil?
  # TODO: Support Windoze VMs (see chef-provisioning-vagrant)
  Chef::Provisioning::Machine::UnixMachine.new(
    machine_spec,
    transport_for(machine_spec, machine_options, instance),
    convergence_strategy_for(machine_spec, machine_options)
  )
end
one_credentials() click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 534
def one_credentials
  @one_credentials ||= begin
                         credentials = Credentials.new(driver_options[:one_options] || {})
                         if driver_options[:credentials]
                           credentials.load_plain(driver_options[:credentials], driver_options[:one_options] || {})
                         elsif driver_options[:secret_file]
                           credentials.load_file(driver_options[:secret_file], driver_options[:one_options] || {})
                         end
                         credentials
                       end
end
populate_img_object(image_spec, new_image) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 580
def populate_img_object(image_spec, new_image)
  image_spec.driver_url = @driver_url_with_profile
  image_spec.reference = {
    'driver_version' => Chef::Provisioning::OpenNebulaDriver::VERSION,
    'image_id'       => new_image,
    'allocated_at'   => Time.now.to_i,
    'state'          => 'none'
  }
  image_spec.machine_options ||= {}
  image_spec.machine_options.merge!(:bootstrap_options => { :image_id => new_image })
end
populate_node_object(machine_spec, machine_options, vm) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 560
def populate_node_object(machine_spec, machine_options, vm)
  machine_spec.driver_url = @driver_url_with_profile
  machine_spec.reference = {
    'driver_version' => Chef::Provisioning::OpenNebulaDriver::VERSION,
    'allocated_at' => Time.now.utc.to_s,
    'image_id' => machine_options[:bootstrap_options][:image_id] || nil,
    'is_shutdown' => machine_options[:bootstrap_options][:is_shutdown] || false,
    'shutdown_hard' => machine_options[:bootstrap_options][:shutdown_hard] || false,
    'instance_id' => vm.id,
    'name' => vm.name,
    'state' => vm.state_str
  }
  # handle ssh_user and ssh_username for backward compatibility
  Chef::Log.warn("':ssh_user' will be deprecated in next version in favour of ':ssh_username'") if machine_options.key?(:ssh_user)
  machine_spec.reference['ssh_username'] = get_ssh_user(machine_spec, machine_options)
  %w(is_windows sudo transport_address_location ssh_gateway).each do |key|
    machine_spec.reference[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
  end
end
transport_for(machine_spec, machine_options, _instance) click to toggle source
# File lib/chef/provisioning/opennebula_driver/driver.rb, line 636
def transport_for(machine_spec, machine_options, _instance)
  ssh_options = {
    :keys_only => false,
    :forward_agent => true,
    :use_agent => true,
    :user_known_hosts_file => '/dev/null',
    :timeout => 10
  }.merge(machine_options[:ssh_options] || {})
  ssh_options[:proxy] = Net::SSH::Proxy::Command.new(ssh_options[:proxy]) if ssh_options.key?(:proxy)

  connection_timeout = machine_options[:connection_timeout] || 300 # default is 5 min
  username = get_ssh_user(machine_spec, machine_options)

  options = {}
  if machine_spec.reference[:sudo] || (!machine_spec.reference.key?(:sudo) && username != 'root')
    options[:prefix] = 'sudo '
  end
  options[:ssh_pty_enable] = machine_options[:ssh_pty_enable] || true
  # User provided ssh_gateway takes precedence over machine_spec value
  options[:ssh_gateway] = machine_options[:ssh_gateway] || machine_spec.reference['ssh_gateway']

  transport = Chef::Provisioning::Transport::SSH.new(machine_spec.reference['ip'], username, ssh_options, options, config)

  retries = connection_timeout.to_i / 3
  rc = @one.retry_one("Waiting for SSH connection", retries, 3) { transport.available? ? true : nil }
  fail "Failed to establish SSH connection to '#{machine_spec.name}'" if rc.nil?
  transport
end