lib/kitchen/driver/vagrant.rb in kitchen-vagrant-0.15.0 vs lib/kitchen/driver/vagrant.rb in kitchen-vagrant-0.16.0

- old
+ new

@@ -14,222 +14,317 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -require 'fileutils' -require 'rubygems/version' +require "erb" +require "fileutils" +require "rubygems/version" -require 'kitchen' +require "kitchen" module Kitchen module Driver # Vagrant driver for Kitchen. It communicates to Vagrant via the CLI. # # @author Fletcher Nichol <fnichol@nichol.ca> - # - # @todo Vagrant installation check and version will be placed into any - # dependency hook checks when feature is released class Vagrant < Kitchen::Driver::SSHBase + default_config(:box) { |driver| driver.default_box } + required_config :box + + default_config :box_check_update, nil + + default_config(:box_url) { |driver| driver.default_box_url } + + default_config :box_version, nil + default_config :customize, {} + + default_config :gui, nil + default_config :network, [] - default_config :synced_folders, [] + default_config :pre_create_command, nil - default_config :vagrantfile_erb, - File.join(File.dirname(__FILE__), "../../../templates/Vagrantfile.erb") + default_config :provision, false - default_config :provider, - ENV.fetch('VAGRANT_DEFAULT_PROVIDER', "virtualbox") - - default_config :vm_hostname do |driver| - "#{driver.instance.name}.vagrantup.com" + default_config :provider do |_| + ENV.fetch("VAGRANT_DEFAULT_PROVIDER", "virtualbox") end - default_config :box do |driver| - "opscode-#{driver.instance.platform.name}" - end + default_config :ssh, {} - default_config :box_url do |driver| - driver.default_box_url - end + default_config :synced_folders, [] - required_config :box + default_config :vagrantfile_erb, + File.join(File.dirname(__FILE__), "../../../templates/Vagrantfile.erb") + expand_path_for :vagrantfile_erb + default_config :vagrantfiles, [] + + default_config(:vm_hostname) { |driver| driver.instance.name } + no_parallel_for :create, :destroy + # Creates a Vagrant VM instance. + # + # @param state [Hash] mutable instance state + # @raise [ActionFailed] if the action could not be completed def create(state) create_vagrantfile run_pre_create_command - cmd = "vagrant up --no-provision" - cmd += " --provider=#{config[:provider]}" if config[:provider] - run cmd - set_ssh_state(state) + run_vagrant_up + update_state(state) info("Vagrant instance #{instance.to_str} created.") end - def converge(state) - create_vagrantfile - super + # @return [String,nil] the Vagrant box for this Instance + def default_box + if bento_boxes.include?(instance.platform.name) + "opscode-#{instance.platform.name}" + else + instance.platform.name + end end - def setup(state) - create_vagrantfile - super - end + # @return [String,nil] the Vagrant box URL for this Instance + def default_box_url + return unless bento_boxes.include?(instance.platform.name) - def verify(state) - create_vagrantfile - super + provider = config[:provider] + provider = "vmware" if config[:provider] =~ /^vmware_(.+)$/ + + if %w[virtualbox vmware].include?(provider) + "https://opscode-vm-bento.s3.amazonaws.com/vagrant/#{provider}/" \ + "opscode_#{instance.platform.name}_chef-provisionerless.box" + end end + # Destroys an instance. + # + # @param state [Hash] mutable instance state + # @raise [ActionFailed] if the action could not be completed def destroy(state) return if state[:hostname].nil? create_vagrantfile @vagrantfile_created = false - run "vagrant destroy -f" + run("vagrant destroy -f") FileUtils.rm_rf(vagrant_root) info("Vagrant instance #{instance.to_str} destroyed.") state.delete(:hostname) end - def verify_dependencies - check_vagrant_version + # 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 + def finalize_config!(instance) + super + config[:vagrantfiles] = config[:vagrantfiles].map do |path| + File.expand_path(path, config[:kitchen_root]) + end + finalize_pre_create_command! + finalize_synced_folders! + self end - def instance=(instance) - @instance = instance - resolve_config! + # 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 + 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 - def default_box_url - bucket = config[:provider] - bucket = 'vmware' if config[:provider] =~ /^vmware_(.+)$/ - - "https://opscode-vm-bento.s3.amazonaws.com/vagrant/#{bucket}/" + - "opscode_#{instance.platform.name}_chef-provisionerless.box" - end - protected - WEBSITE = "http://downloads.vagrantup.com/" - MIN_VER = "1.1.0" + WEBSITE = "http://www.vagrantup.com/downloads.html".freeze + MIN_VER = "1.1.0".freeze - def run(cmd, options = {}) - cmd = "echo #{cmd}" if config[:dry_run] - run_command(cmd, { :cwd => vagrant_root }.merge(options)) + # Retuns a list of Vagrant base boxes produced by the Bento project + # (https://github.com/chef/bento). + # + # @return [Arrau<String>] list of Bento box names + # @api private + def bento_boxes + %W[ + centos-5.11 centos-6.6 centos-7.0 debian-6.0.10 debian-7.8 fedora-20 + fedora-21 freebsd-9.3 freebsd-10.1 opensuse-13.1 ubuntu-10.04 + ubuntu-12.04 ubuntu-14.04 ubuntu-14.10 + ].map { |name| [name, "#{name}-i386"] }.flatten end - def silently_run(cmd) - run_command(cmd, - :live_stream => nil, :quiet => logger.debug? ? false : true) - end - - def run_pre_create_command - if config[:pre_create_command] - run(config[:pre_create_command], :cwd => config[:kitchen_root]) - end - end - - def vagrant_root - @vagrant_root ||= File.join( - config[:kitchen_root], %w{.kitchen kitchen-vagrant}, instance.name - ) - end - + # Renders and writes out a Vagrantfile dedicated to this instance. + # + # @api private def create_vagrantfile return if @vagrantfile_created - finalize_synced_folder_config - 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 - def finalize_synced_folder_config - 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 + # Logs the Vagrantfile's contents to the debug log level. + # + # @param vagrantfile [String] path to the Vagrantfile + # @api private + def debug_vagrantfile(vagrantfile) + return unless logger.debug? + + debug("------------") + IO.read(vagrantfile).each_line { |l| debug("#{l.chomp}") } + debug("------------") end + # Replaces any `{{vagrant_root}}` tokens in the pre create command. + # + # @api private + 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 + + # Replaces an `%{instance_name}` tokens in the synced folder items. + # + # @api private + 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 + end + + # Renders the Vagrantfile ERb template. + # + # @return [String] the contents for a Vagrantfile + # @raise [ActionFailed] if the Vagrantfile template was not found + # @api private def render_template - if File.exists?(template) - ERB.new(IO.read(template)).result(binding).gsub(%r{^\s*$\n}, '') + template = File.expand_path( + config[:vagrantfile_erb], config[:kitchen_root]) + + if File.exist?(template) + ERB.new(IO.read(template)).result(binding).gsub(%r{^\s*$\n}, "") else raise ActionFailed, "Could not find Vagrantfile template #{template}" end end - def template - File.expand_path(config[:vagrantfile_erb], config[:kitchen_root]) + # 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 + def run(cmd, options = {}) + cmd = "echo #{cmd}" if config[:dry_run] + run_command(cmd, { :cwd => vagrant_root }.merge(options)) end - def set_ssh_state(state) + # Runs a local command before `vagrant up` has been called. + # + # @api private + def run_pre_create_command + if config[:pre_create_command] + run(config[:pre_create_command], :cwd => config[:kitchen_root]) + end + end + + # Runs a local command without streaming the stdout to the logger. + # + # @param cmd [String] command to run locally + # @api private + def run_silently(cmd, options = {}) + merged = { + :live_stream => nil, :quiet => (logger.debug? ? false : true) + }.merge(options) + run(cmd, merged) + end + + # Runs the `vagrant up` command locally. + # + # @api private + def run_vagrant_up + cmd = "vagrant up" + cmd += " --no-provision" unless config[:provision] + cmd += " --provider #{config[:provider]}" if config[:provider] + run(cmd) + end + + # Updates any state after creation. + # + # @param state [Hash] mutable instance state + # @api private + def update_state(state) hash = vagrant_ssh_config state[:hostname] = hash["HostName"] state[:username] = hash["User"] state[:ssh_key] = hash["IdentityFile"] state[:port] = hash["Port"] + state[:proxy_command] = hash["ProxyCommand"] if hash["ProxyCommand"] end + # @return [String] full local path to the directory containing the + # instance's Vagrantfile + # @api private + def vagrant_root + @vagrant_root ||= instance.nil? ? nil : File.join( + config[:kitchen_root], %w[.kitchen kitchen-vagrant], + "kitchen-#{File.basename(config[:kitchen_root])}-#{instance.name}" + ) + end + + # @return [Hash] key/value pairs resulting from parsing a + # `vagrant ssh-config` local command invocation + # @api private def vagrant_ssh_config - output = run("vagrant ssh-config", :live_stream => nil) - lines = output.split("\n").map do |line| + lines = run_silently("vagrant ssh-config").split("\n").map do |line| tokens = line.strip.partition(" ") - [tokens.first, tokens.last.gsub(/"/, '')] + [tokens.first, tokens.last.gsub(/"/, "")] end Hash[lines] end - def debug_vagrantfile(vagrantfile) - if logger.debug? - debug("------------") - IO.read(vagrantfile).each_line { |l| debug("#{l.chomp}") } - debug("------------") - end - end - - def resolve_config! - unless config[:vagrantfile_erb].nil? - config[:vagrantfile_erb] = - File.expand_path(config[:vagrantfile_erb], config[:kitchen_root]) - end - unless config[:pre_create_command].nil? - config[:pre_create_command] = - config[:pre_create_command].gsub("{{vagrant_root}}", vagrant_root) - end - end - + # @return [String] version of Vagrant + # @raise [UserError] if the `vagrant` command can not be found locally + # @api private def vagrant_version - version_string = silently_run("vagrant --version") - version_string = version_string.chomp.split(" ").last + @version ||= run_silently("vagrant --version", :cwd => Dir.pwd). + chomp.split(" ").last rescue Errno::ENOENT - raise UserError, "Vagrant #{MIN_VER} or higher is not installed." + + raise UserError, "Vagrant #{MIN_VER} or higher is not installed." \ " Please download a package from #{WEBSITE}." - end - - def check_vagrant_version - version = vagrant_version - if Gem::Version.new(version) < Gem::Version.new(MIN_VER) - raise UserError, "Detected an old version of Vagrant (#{version})." + - " Please upgrade to version #{MIN_VER} or higher from #{WEBSITE}." - end end end end end