class Kitchen::Driver::Vagrant

Vagrant driver for Kitchen. It communicates to Vagrant via the CLI.

@author Fletcher Nichol <fnichol@nichol.ca>

Constants

MIN_VER
WEBSITE

Attributes

vagrant_version[RW]

@return [String] the version of Vagrant installed on the workstation @api private

Public Instance Methods

cache_directory() click to toggle source

Setting up the ‘cache_directory` to store omnibus packages in cache and share a local folder to that directory so that we don’t pull them down every single time

# File lib/kitchen/driver/vagrant.rb, line 219
def cache_directory
  if enable_cache?
    config[:cache_directory]
  else
    false
  end
end
create(state) click to toggle source

Creates a Vagrant VM instance.

@param state [Hash] mutable instance state @raise [ActionFailed] if the action could not be completed

# File lib/kitchen/driver/vagrant.rb, line 116
def create(state)
  create_vagrantfile
  run_pre_create_command
  run_box_auto_update
  run_box_auto_prune
  run_vagrant_up
  update_state(state)
  instance.transport.connection(state).wait_until_ready
  info("Vagrant instance #{instance.to_str} created.")
end
default_box() click to toggle source

@return [String,nil] the Vagrant box for this Instance

# File lib/kitchen/driver/vagrant.rb, line 128
def default_box
  if bento_box?(instance.platform.name)
    "bento/#{instance.platform.name}"
  else
    instance.platform.name
  end
end
default_box_url() click to toggle source

@return [String,nil] the Vagrant box URL for this Instance

# File lib/kitchen/driver/vagrant.rb, line 137
def default_box_url
  nil
end
destroy(state) click to toggle source

Destroys an instance.

@param state [Hash] mutable instance state @raise [ActionFailed] if the action could not be completed

# File lib/kitchen/driver/vagrant.rb, line 145
def destroy(state)
  return if state[:hostname].nil?

  create_vagrantfile
  @vagrantfile_created = false
  instance.transport.connection(state).close
  run("#{config[:vagrant_binary]} destroy -f")
  FileUtils.rm_rf(vagrant_root)
  info("Vagrant instance #{instance.to_str} destroyed.")
  state.delete(:hostname)
end
finalize_config!(instance) click to toggle source

A lifecycle method that should be invoked when the object is about ready to be used. A reference to an Instance is required as configuration dependant data may be access through an Instance. This also acts as a hook point where the object may wish to perform other last minute checks, validations, or configuration expansions.

@param instance [Instance] an associated instance @return [self] itself, for use in chaining @raise [ClientError] if instance parameter is nil

Calls superclass method
# File lib/kitchen/driver/vagrant.rb, line 181
def finalize_config!(instance)
  super
  finalize_vm_hostname!
  finalize_box_auto_update!
  finalize_box_auto_prune!
  finalize_pre_create_command!
  finalize_synced_folders!
  finalize_ca_cert!
  finalize_network!
  self
end
package(state) click to toggle source
# File lib/kitchen/driver/vagrant.rb, line 157
def package(state)
  if state[:hostname].nil?
    raise UserError, "Vagrant instance not created!"
  end

  unless config[:ssh] && config[:ssh][:insert_key] == false
    m = "Disable vagrant ssh key replacement to preserve the default key!"
    warn(m)
  end
  instance.transport.connection(state).close
  box_name = File.join(Dir.pwd, instance.name + ".box")
  run("#{config[:vagrant_binary]} package --output #{box_name}")
  destroy(state)
end
verify_dependencies() click to toggle source

Performs whatever tests that may be required to ensure that this driver will be able to function in the current environment. This may involve checking for the presence of certain directories, software installed, etc.

@raise [UserError] if the driver will not be able to perform or if a

documented dependency is missing from the system
Calls superclass method
# File lib/kitchen/driver/vagrant.rb, line 200
def verify_dependencies
  super
  if Gem::Version.new(vagrant_version) < Gem::Version.new(MIN_VER.dup)
    raise UserError, "Detected an old version of Vagrant " \
      "(#{vagrant_version})." \
      " Please upgrade to version #{MIN_VER} or higher from #{WEBSITE}."
  end
end
winrm_transport?() click to toggle source

@return [TrueClass,FalseClass] whether or not the transport’s name

implies a WinRM-based transport

@api private

# File lib/kitchen/driver/vagrant.rb, line 212
def winrm_transport?
  instance.transport.name.downcase =~ /win_?rm/
end

Protected Instance Methods

add_extra_synced_folders!() click to toggle source

We would like to sync a local folder to the instance so we can take advantage of the packages that we might have in cache, therefore we wont download a package we already have

# File lib/kitchen/driver/vagrant.rb, line 359
def add_extra_synced_folders!
  if cache_directory
    FileUtils.mkdir_p(local_kitchen_cache)
    config[:synced_folders].push([
      local_kitchen_cache,
      cache_directory,
      "create: true",
    ])
  end
end
bento_box?(name) click to toggle source

Retuns whether or not a platform name could have a correcponding Bento box produced by the Bento project. (github.com/chef/bento).

@return [TrueClass,FalseClass] whether or not the name could be a Bento

box

@api private

# File lib/kitchen/driver/vagrant.rb, line 245
def bento_box?(name)
  name =~ /^(centos|debian|fedora|freebsd|opensuse|ubuntu|oracle|hardenedbsd|amazonlinux|almalinux|rockylinux|springdalelinux)-/
end
create_vagrantfile() click to toggle source

Renders and writes out a Vagrantfile dedicated to this instance.

@api private

# File lib/kitchen/driver/vagrant.rb, line 274
def create_vagrantfile
  return if @vagrantfile_created

  vagrantfile = File.join(vagrant_root, "Vagrantfile")
  debug("Creating Vagrantfile for #{instance.to_str} (#{vagrantfile})")
  FileUtils.mkdir_p(vagrant_root)
  File.open(vagrantfile, "wb") { |f| f.write(render_template) }
  debug_vagrantfile(vagrantfile)
  @vagrantfile_created = true
end
debug_vagrantfile(vagrantfile) click to toggle source

Logs the Vagrantfile’s contents to the debug log level.

@param vagrantfile [String] path to the Vagrantfile @api private

# File lib/kitchen/driver/vagrant.rb, line 289
def debug_vagrantfile(vagrantfile)
  return unless logger.debug?

  debug("------------")
  IO.read(vagrantfile).each_line { |l| debug("#{l.chomp}") }
  debug("------------")
end
enable_cache?() click to toggle source

Return true if we found the criteria to enable the cache_directory functionality

# File lib/kitchen/driver/vagrant.rb, line 262
def enable_cache?
  return false unless config[:cache_directory]
  return true if safe_share?(config[:box])
  return true if config[:use_cached_chef_client]

  # Otherwise
  false
end
finalize_box_auto_prune!() click to toggle source

Create vagrant command to remove older versions of the box

# File lib/kitchen/driver/vagrant.rb, line 320
def finalize_box_auto_prune!
  return if config[:box_auto_prune].nil?

  cmd = "#{config[:vagrant_binary]} box prune --force --keep-active-boxes --name #{config[:box]}"
  cmd += " --provider #{config[:provider]}" if config[:provider]
  config[:box_auto_prune] = cmd
end
finalize_box_auto_update!() click to toggle source

Create vagrant command to update box to the latest version

# File lib/kitchen/driver/vagrant.rb, line 309
def finalize_box_auto_update!
  return if config[:box_auto_update].nil?

  cmd = "#{config[:vagrant_binary]} box update --box #{config[:box]}"
  cmd += " --architecture #{config[:box_arch]}" if config[:box_arch]
  cmd += " --provider #{config[:provider]}" if config[:provider]
  cmd += " --insecure" if config[:box_download_insecure]
  config[:box_auto_update] = cmd
end
finalize_ca_cert!() click to toggle source

Setup path for CA cert

@api private

# File lib/kitchen/driver/vagrant.rb, line 300
def finalize_ca_cert!
  unless config[:box_download_ca_cert].nil?
    config[:box_download_ca_cert] = File.expand_path(
      config[:box_download_ca_cert], config[:kitchen_root]
    )
  end
end
finalize_network!() click to toggle source

If Hyper-V and no network configuration check KITCHEN_HYPERV_SWITCH and fallback to helper method to select the best switch @api private

# File lib/kitchen/driver/vagrant.rb, line 386
def finalize_network!
  if config[:provider] == "hyperv" && config[:network].empty?
    config[:network].push([
      "public_network",
      "bridge: \"#{hyperv_switch}\"",
      ])
  end
end
finalize_pre_create_command!() click to toggle source

Replaces any ‘{{vagrant_root}}` tokens in the pre create command.

@api private

# File lib/kitchen/driver/vagrant.rb, line 331
def finalize_pre_create_command!
  return if config[:pre_create_command].nil?

  config[:pre_create_command] = config[:pre_create_command]
    .gsub("{{vagrant_root}}", vagrant_root)
end
finalize_synced_folders!() click to toggle source

Replaces an ‘%{instance_name}` tokens in the synced folder items.

@api private

# File lib/kitchen/driver/vagrant.rb, line 341
def finalize_synced_folders!
  config[:synced_folders] = config[:synced_folders]
    .map do |source, destination, options|
      [
        File.expand_path(
          source.gsub("%{instance_name}", instance.name),
          config[:kitchen_root]
        ),
        destination.gsub("%{instance_name}", instance.name),
        options || "nil",
      ]
    end
  add_extra_synced_folders!
end
finalize_vm_hostname!() click to toggle source

Truncates the length of ‘:vm_hostname` to 12 characters for Windows-based operating systems.

@api private

# File lib/kitchen/driver/vagrant.rb, line 374
def finalize_vm_hostname!
  string = config[:vm_hostname]

  if windows_os? && string.is_a?(String) && string.size > 15
    config[:vm_hostname] = "#{string[0...12]}-#{string[-1]}"
  end
end
local_kitchen_cache() click to toggle source

@return [String] full absolute path to the kitchen cache directory @api private

# File lib/kitchen/driver/vagrant.rb, line 545
def local_kitchen_cache
  @local_kitchen_cache ||= config[:kitchen_cache_directory]
end
render_template() click to toggle source

Renders the Vagrantfile ERb template.

@return [String] the contents for a Vagrantfile @raise [ActionFailed] if the Vagrantfile template was not found @api private

# File lib/kitchen/driver/vagrant.rb, line 400
def render_template
  template = File.expand_path(
    config[:vagrantfile_erb], config[:kitchen_root]
  )

  if File.exist?(template)
    ERB.new(IO.read(template)).result(binding).gsub(/^\s*$\n/, "")
  else
    raise ActionFailed, "Could not find Vagrantfile template #{template}"
  end
end
run(cmd, options = {}) click to toggle source

Convenience method to run a command locally.

@param cmd [String] command to run locally @param options [Hash] options hash @see Kitchen::ShellOut.run_command @api private

# File lib/kitchen/driver/vagrant.rb, line 418
def run(cmd, options = {})
  cmd = "echo #{cmd}" if config[:dry_run]
  run_command(cmd, { cwd: vagrant_root }.merge(options))
end
run_box_auto_prune() click to toggle source

Tell vagrant to remove older vagrant boxes

# File lib/kitchen/driver/vagrant.rb, line 491
def run_box_auto_prune
  if config[:box_auto_prune]
    run(config[:box_auto_prune])
  end
end
run_box_auto_update() click to toggle source

Tell vagrant to update vagrant box to latest version

# File lib/kitchen/driver/vagrant.rb, line 478
def run_box_auto_update
  if config[:box_auto_update]
    begin
      run(config[:box_auto_update])
    rescue Kitchen::ShellOut::ShellCommandFailed => e
      # If the box has never been downloaded, the update command will fail with this message.
      # Just ignore it and move on. Re-raise all other errors.
      raise e unless e.message.match?(/The box '.*' does not exist/m)
    end
  end
end
run_command(cmd, options = {}) click to toggle source

Delegates to Kitchen::ShellOut.run_command, overriding some default options:

  • ‘:use_sudo` defaults to the value of `config` in the Driver object

  • ‘:log_subject` defaults to a String representation of the Driver’s class name

Since vagrant does not support being run through bundler, we escape any bundler environment should we detect one. Otherwise, subcommands will inherit our bundled environment. @see github.com/test-kitchen/kitchen-vagrant/issues/190 @see Kitchen::ShellOut#run_command rubocop:disable Metrics/CyclomaticComplexity

Calls superclass method
# File lib/kitchen/driver/vagrant.rb, line 437
def run_command(cmd, options = {})
  merged = {
    use_sudo: config[:use_sudo],
    log_subject: name,
    environment: {},
  }.merge(options)

  # Attempt to extract bundler and associated GEM related values.
  # TODO: To this accurately, we'd need to create a test-kitchen
  # launch wrapper that serializes the existing environment before
  # bundler touches it so that we can go back to it. Since that is
  # "A Hard Problem"(TM), we simply blow away all known bundler
  # related changes.
  env = merged[:environment]
  %w{BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH GEM_ROOT RUBYLIB
     RUBYOPT _ORIGINAL_GEM_PATH}.each do |var|
       env[var] = nil
     end

  # Altering the path seems to break vagrant. When the :environment
  # is passed to a windows process with a PATH, Vagrant's batch installer
  # (https://github.com/mitchellh/vagrant-installers/blob/master/substrate
  # /modules/vagrant_installer/templates/windows_vagrant.bat.erb)
  # does not efectively prepend the vagrant ruby path in a persistent
  # manner which causes vagrant to use the same ruby as test-kitchen and
  # then the environment is essentially corrupted leading to many errors
  # and dispair
  unless windows_host?
    gem_home = ENV["GEM_HOME"]
    if gem_home && (env["PATH"] || ENV["PATH"])
      env["PATH"] ||= ENV["PATH"].dup if ENV["PATH"]
      gem_bin = File.join(gem_home, "bin") + File::PATH_SEPARATOR
      env["PATH"][gem_bin] = "" if env["PATH"].include?(gem_bin)
    end
  end

  super(cmd, merged)
end
run_pre_create_command() click to toggle source

Runs a local command before ‘vagrant up` has been called.

@api private

# File lib/kitchen/driver/vagrant.rb, line 500
def run_pre_create_command
  if config[:pre_create_command]
    run(config[:pre_create_command], cwd: config[:kitchen_root])
  end
end
run_silently(cmd, options = {}) click to toggle source

Runs a local command without streaming the stdout to the logger.

@param cmd [String] command to run locally @api private

# File lib/kitchen/driver/vagrant.rb, line 510
def run_silently(cmd, options = {})
  merged = {
    live_stream: nil, quiet: (logger.debug? ? false : true)
  }.merge(options)
  run(cmd, merged)
end
run_vagrant_up() click to toggle source

Runs the ‘vagrant up` command locally.

@api private

# File lib/kitchen/driver/vagrant.rb, line 520
def run_vagrant_up
  cmd = "#{config[:vagrant_binary]} up"
  cmd += " --no-provision" unless config[:provision]
  cmd += " --provider #{config[:provider]}" if config[:provider]
  run(cmd)
end
safe_share?(box) click to toggle source

Returns whether or not the we expect the box to work with shared folders by matching against a whitelist of bento boxes @return [TrueClass,FalseClass] whether or not the box shoud work with

shared folders

@api private

# File lib/kitchen/driver/vagrant.rb, line 254
def safe_share?(box)
  return false if /(hyperv|libvirt)/.match?(config[:provider])

  box =~ %r{^bento/(centos|debian|fedora|opensuse|ubuntu|oracle|amazonlinux|almalinux|rockylinux|springdalelinux)-}
end
update_state(state) click to toggle source

Updates any state after creation.

@param state [Hash] mutable instance state @api private

# File lib/kitchen/driver/vagrant.rb, line 531
def update_state(state)
  hash = winrm_transport? ? vagrant_config(:winrm) : vagrant_config(:ssh)

  state[:hostname] = hash["HostName"]
  state[:port] = hash["Port"]
  state[:username] = hash["User"]
  state[:password] = hash["Password"] if hash["Password"]
  state[:ssh_key] = hash["IdentityFile"] if hash["IdentityFile"]
  state[:proxy_command] = hash["ProxyCommand"] if hash["ProxyCommand"]
  state[:rdp_port] = hash["RDPPort"] if hash["RDPPort"]
end
vagrant_config(type) click to toggle source

@param type [Symbol] either ‘:ssh` or `:winrm` @return [Hash] key/value pairs resulting from parsing a

`vagrant ssh-config` or `vagrant winrm-config` local command
invocation

@api private

# File lib/kitchen/driver/vagrant.rb, line 567
def vagrant_config(type)
  lines = run_silently("#{config[:vagrant_binary]} #{type}-config")
    .split("\n").map do |line|
      tokens = line.strip.partition(" ")
      [tokens.first, tokens.last.delete('"')]
    end
  Hash[lines]
end
vagrant_root() click to toggle source

@return [String] full local path to the directory containing the

instance's Vagrantfile

@api private

# File lib/kitchen/driver/vagrant.rb, line 552
def vagrant_root
  if !@vagrant_root && !instance.nil?
    @vagrant_root = File.join(
      config[:kitchen_root], %w{.kitchen kitchen-vagrant},
      "#{instance.name}"
    )
  end
  @vagrant_root
end
vagrant_version() click to toggle source

@return [String] version of Vagrant @raise [UserError] if the ‘vagrant` command can not be found locally @api private

# File lib/kitchen/driver/vagrant.rb, line 579
def vagrant_version
  self.class.vagrant_version ||= run_silently(
    "#{config[:vagrant_binary]} --version", cwd: Dir.pwd
  )
    .chomp.split(" ").last
rescue Errno::ENOENT
  raise UserError, "Vagrant #{MIN_VER} or higher is not installed." \
    " Please download a package from #{WEBSITE}."
end
windows_host?() click to toggle source

@return [true,false] whether or not the host is windows

@api private

# File lib/kitchen/driver/vagrant.rb, line 592
def windows_host?
  RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
end