diff --git a/.gitignore b/.gitignore index a206bde81..04a01b562 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ acceptance_config.yml boxes/* /Vagrantfile /.vagrant +/website/docs/.vagrant /website/www/.vagrant /vagrant-spec.config.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index d13b6d105..d32a34b99 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -9,6 +9,7 @@ module Vagrant # and are thus available to all plugins as a "standard library" of sorts. module Builtin autoload :BoxAdd, "vagrant/action/builtin/box_add" + autoload :BoxCheckOutdated, "vagrant/action/builtin/box_check_outdated" autoload :BoxRemove, "vagrant/action/builtin/box_remove" autoload :Call, "vagrant/action/builtin/call" autoload :Confirm, "vagrant/action/builtin/confirm" @@ -16,6 +17,7 @@ module Vagrant autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm" autoload :EnvSet, "vagrant/action/builtin/env_set" autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt" + autoload :HandleBox, "vagrant/action/builtin/handle_box" autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions" autoload :Lock, "vagrant/action/builtin/lock" @@ -44,6 +46,14 @@ module Vagrant end end + # This actions checks if a box is outdated in a given Vagrant + # environment for a single machine. + def self.action_box_outdated + Builder.new.tap do |b| + b.use Builtin::BoxCheckOutdated + end + end + # This is the action that will remove a box given a name (and optionally # a provider). This middleware sequence is built-in to Vagrant. Plugins # can hook into this like any other middleware sequence. diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index 5adfa80ce..891e05145 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -1,6 +1,9 @@ require "digest/sha1" require "log4r" +require "pathname" +require "uri" +require "vagrant/box_metadata" require "vagrant/util/downloader" require "vagrant/util/file_checksum" require "vagrant/util/platform" @@ -19,34 +22,205 @@ module Vagrant def call(env) @download_interrupted = false - box_name = env[:box_name] - box_formats = env[:box_provider] - if box_formats - # Determine the formats a box can support and allow the box to - # be any of those formats. - provider_plugin = Vagrant.plugin("2").manager.providers[env[:box_provider]] - if provider_plugin - box_formats = provider_plugin[1][:box_format] - box_formats ||= env[:box_provider] + url = Array(env[:box_url]).map do |u| + u = u.gsub("\\", "/") + if Util::Platform.windows? && u =~ /^[a-z]:/i + # On Windows, we need to be careful about drive letters + u = "file://#{u}" + end + + if u =~ /^[a-z0-9]+:.*$/i && !u.start_with?("file://") + # This is not a file URL... carry on + next u + end + + # Expand the path and try to use that, if possible + p = File.expand_path(u.gsub(/^file:\/\//, "")) + p = Util::Platform.cygwin_windows_path(p) + next "file://#{p}" if File.file?(p) + + u + end + + # If we received a shorthand URL ("mitchellh/precise64"), + # then expand it properly. + expanded = false + url.each_index do |i| + next if url[i] !~ /^[^\/]+\/[^\/]+$/ + + if !File.file?(url[i]) + server = Vagrant.server_url + raise Errors::BoxServerNotSet if !server + + expanded = true + url[i] = "#{server}/#{url[i]}" end end - # Determine if we already have the box before downloading - # it again. We can only do this if we specify a format - if box_formats && !env[:box_force] + # Test if any of our URLs point to metadata + is_metadata_results = url.map do |u| begin - if env[:box_collection].find(box_name, box_formats) - raise Errors::BoxAlreadyExists, - :name => box_name, - :formats => [box_formats].flatten.join(", ") - end - rescue Vagrant::Errors::BoxUpgradeRequired - # If the box needs to be upgraded, do it. - env[:box_collection].upgrade(box_name) - retry + metadata_url?(u, env) + rescue Errors::DownloaderError => e + e end end + if expanded && url.length == 1 + is_error = is_metadata_results.find do |b| + b.is_a?(Errors::DownloaderError) + end + + if is_error + raise Errors::BoxAddShortNotFound, + error: is_error.extra_data[:message], + name: env[:box_url], + url: url + end + end + + is_metadata = is_metadata_results.any? { |b| b === true } + if is_metadata && url.length > 1 + raise Errors::BoxAddMetadataMultiURL, + urls: url.join(", ") + end + + if is_metadata + add_from_metadata(url.first, env, expanded) + else + add_direct(url, env) + end + + @app.call(env) + end + + # Adds a box file directly (no metadata component, versioning, + # etc.) + # + # @param [Array] urls + # @param [Hash] env + def add_direct(urls, env) + name = env[:box_name] + if !name || name == "" + raise Errors::BoxAddNameRequired + end + + provider = env[:box_provider] + provider = Array(provider) if provider + + box_add( + urls, + name, + "0", + provider, + nil, + env) + end + + # Adds a box given that the URL is a metadata document. + def add_from_metadata(url, env, expanded) + original_url = env[:box_url] + provider = env[:box_provider] + provider = Array(provider) if provider + version = env[:box_version] + + env[:ui].output(I18n.t( + "vagrant.box_loading_metadata", + name: Array(original_url).first)) + if original_url != url + env[:ui].detail(I18n.t( + "vagrant.box_expanding_url", url: url)) + end + + metadata = nil + begin + metadata_path = download(url, env, ui: false) + + File.open(metadata_path) do |f| + metadata = BoxMetadata.new(f) + end + rescue Errors::DownloaderError => e + raise if !expanded + raise Errors::BoxAddShortNotFound, + error: e.extra_data[:message], + name: original_url, + url: url + ensure + metadata_path.delete if metadata_path && metadata_path.file? + end + + if env[:box_name] && metadata.name != env[:box_name] + raise Errors::BoxAddNameMismatch, + actual_name: metadata.name, + requested_name: env[:box_name] + end + + metadata_version = metadata.version( + version || ">= 0", provider: provider) + if !metadata_version + if !provider + raise Errors::BoxAddNoMatchingVersion, + constraints: version || ">= 0", + name: metadata.name, + url: url, + versions: metadata.versions.join(", ") + else + # TODO: show supported providers + raise Errors::BoxAddNoMatchingProvider, + name: metadata.name, + requested: provider, + url: url + end + end + + metadata_provider = nil + if provider + # If a provider was specified, make sure we get that specific + # version. + provider.each do |p| + metadata_provider = metadata_version.provider(p) + break if metadata_provider + end + elsif metadata_version.providers.length == 1 + # If we have only one provider in the metadata, just use that + # provider. + metadata_provider = metadata_version.provider( + metadata_version.providers.first) + else + providers = metadata_version.providers.sort + + choice = 0 + options = providers.map do |p| + choice += 1 + "#{choice}) #{p}" + end.join("\n") + + # We have more than one provider, ask the user what they want + choice = env[:ui].ask(I18n.t( + "vagrant.box_add_choose_provider", + options: options) + " ", prefix: false) + choice = choice.to_i if choice + while !choice || choice <= 0 || choice > providers.length + choice = env[:ui].ask(I18n.t( + "vagrant.box_add_choose_provider_again") + " ", + prefix: false) + choice = choice.to_i if choice + end + + metadata_provider = metadata_version.provider( + providers[choice-1]) + end + + box_add( + [metadata_provider.url], + metadata.name, + metadata_version.version, + metadata_provider.name, + url, + env) + end + +=begin # Determine the checksum type to use checksum = (env[:box_checksum] || "").to_s checksum_klass = nil @@ -104,46 +278,89 @@ module Vagrant expected: checksum end end + end +=end - # Add the box - env[:ui].info I18n.t("vagrant.actions.box.add.adding", :name => box_name) - box_added = nil + protected + + # Shared helper to add a box once you know various details + # about it. Shared between adding via metadata or by direct. + # + # @param [Array] urls + # @param [String] name + # @param [String] version + # @param [String] provider + # @param [Hash] env + # @return [Box] + def box_add(urls, name, version, provider, md_url, env, **opts) + env[:ui].output(I18n.t( + "vagrant.box_add_with_version", + name: name, + version: version, + providers: Array(provider).join(", "))) + + # Verify the box we're adding doesn't already exist + if provider && !env[:box_force] + box = env[:box_collection].find( + name, provider, version) + if box + raise Errors::BoxAlreadyExists, + name: name, + provider: provider, + version: version + end + end + + # Now we have a URL, we have to download this URL. + box = nil begin - box_added = env[:box_collection].add( - @temp_path, box_name, box_formats, env[:box_force]) - rescue Vagrant::Errors::BoxUpgradeRequired - # Upgrade the box - env[:box_collection].upgrade(box_name) + box_url = nil - # Try adding it again - retry + urls.each do |url| + begin + box_url = download(url, env) + break + rescue Errors::DownloaderError => e + env[:ui].error(I18n.t( + "vagrant.box_download_error", message: e.message)) + box_url = nil + end + end + + # Add the box! + box = env[:box_collection].add( + box_url, name, version, + force: env[:box_force], + metadata_url: md_url, + providers: provider) + ensure + # Make sure we delete the temporary file after we add it, + # unless we were interrupted, in which case we keep it around + # so we can resume the download later. + if !@download_interrupted + @logger.debug("Deleting temporary box: #{box_url}") + box_url.delete if box_url + end end - # Call the 'recover' method in all cases to clean up the - # downloaded temporary file. - recover(env) + env[:ui].success(I18n.t( + "vagrant.box_added", + name: box.name, + version: box.version, + provider: box.provider)) - # Success, we added a box! - env[:ui].success( - I18n.t("vagrant.actions.box.add.added", name: box_added.name, provider: box_added.provider)) + # Store the added box in the env for future middleware + env[:box_added] = box - # Persists URL used on download and the time it was added - write_extra_info(box_added, download_url) - - # Passes on the newly added box to the rest of the middleware chain - env[:box_added] = box_added - - # Carry on! - @app.call(env) + box end - def recover(env) - if @temp_path && File.exist?(@temp_path) && !@download_interrupted - File.unlink(@temp_path) - end - end + # Returns the download options for the download. + # + # @return [Hash] + def downloader(url, env, **opts) + opts[:ui] = true if !opts.has_key?(:ui) - def download_box_url(url, env) temp_path = env[:tmp_path].join("box" + Digest::SHA1.hexdigest(url)) @logger.info("Downloading box: #{url} => #{temp_path}") @@ -154,13 +371,6 @@ module Vagrant url = "file:#{file_path}" end - downloader_options = {} - downloader_options[:ca_cert] = env[:box_download_ca_cert] - downloader_options[:continue] = true - downloader_options[:insecure] = env[:box_download_insecure] - downloader_options[:ui] = env[:ui] - downloader_options[:client_cert] = env[:box_client_cert] - # If the temporary path exists, verify it is not too old. If its # too old, delete it first because the data may have changed. if temp_path.file? @@ -176,19 +386,35 @@ module Vagrant temp_path.unlink if delete end + downloader_options = {} + downloader_options[:ca_cert] = env[:box_download_ca_cert] + downloader_options[:continue] = true + downloader_options[:insecure] = env[:box_download_insecure] + downloader_options[:client_cert] = env[:box_client_cert] + downloader_options[:ui] = env[:ui] if opts[:ui] + + Util::Downloader.new(url, temp_path, downloader_options) + end + + def download(url, env, **opts) + opts[:ui] = true if !opts.has_key?(:ui) + + d = downloader(url, env, **opts) + # Download the box to a temporary path. We store the temporary # path as an instance variable so that the `#recover` method can # access it. - env[:ui].info(I18n.t( - "vagrant.actions.box.download.downloading", - url: url)) - if temp_path.file? - env[:ui].info(I18n.t("vagrant.actions.box.download.resuming")) + if opts[:ui] + env[:ui].detail(I18n.t( + "vagrant.box_downloading", + url: url)) + if File.file?(d.destination) + env[:ui].info(I18n.t("vagrant.actions.box.download.resuming")) + end end begin - downloader = Util::Downloader.new(url, temp_path, downloader_options) - downloader.download! + d.download! rescue Errors::DownloaderInterrupted # The downloader was interrupted, so just return, because that # means we were interrupted as well. @@ -196,18 +422,45 @@ module Vagrant env[:ui].info(I18n.t("vagrant.actions.box.download.interrupted")) rescue Errors::DownloaderError # The download failed for some reason, clean out the temp path - temp_path.unlink if temp_path.file? + File.unlink(d.destination) if File.file?(d.destination) raise end - temp_path + Pathname.new(d.destination) end - def write_extra_info(box_added, url) - info = {'url' => url, 'downloaded_at' => Time.now.utc} - box_added.directory.join('info.json').open("w+") do |f| - f.write(JSON.dump(info)) + # Tests whether the given URL points to a metadata file or a + # box file without completely downloading the file. + # + # @param [String] url + # @return [Boolean] true if metadata + def metadata_url?(url, env) + d = downloader(url, env, ui: false) + + # If we're downloading a file, cURL just returns no + # content-type (makes sense), so we just test if it is JSON + # by trying to parse JSON! + uri = URI.parse(d.source) + if uri.scheme == "file" + url = uri.path + url ||= uri.opaque + + begin + File.open(url, "r") do |f| + BoxMetadata.new(f) + end + return true + rescue Errors::BoxMetadataMalformed + return false + rescue Errno::ENOENT + return false + end end + + output = d.head + match = output.scan(/^Content-Type: (.+?)$/).last + return false if !match + match.last.chomp == "application/json" end end end diff --git a/lib/vagrant/action/builtin/box_check_outdated.rb b/lib/vagrant/action/builtin/box_check_outdated.rb new file mode 100644 index 000000000..944fb6246 --- /dev/null +++ b/lib/vagrant/action/builtin/box_check_outdated.rb @@ -0,0 +1,83 @@ +require "log4r" + +module Vagrant + module Action + module Builtin + # This middleware checks if there are outdated boxes. By default, + # it only checks locally, but if `box_outdated_refresh` is set, it + # will refresh the metadata associated with a box. + class BoxCheckOutdated + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new( + "vagrant::action::builtin::box_check_outdated") + end + + def call(env) + machine = env[:machine] + + if !env[:box_outdated_force] + if !machine.config.vm.box_check_update + return @app.call(env) + end + end + + if !machine.box + # The box doesn't exist. I suppose technically that means + # that it is "outdated" but we show a specialized error + # message anyways. + raise Errors::BoxOutdatedNoBox, name: machine.config.vm.box + end + + box = machine.box + if box.version == "0" && !box.metadata_url + return @app.call(env) + end + + constraints = machine.config.vm.box_version + + env[:ui].output(I18n.t( + "vagrant.box_outdated_checking_with_refresh", + name: box.name)) + update = nil + begin + update = box.has_update?(constraints) + rescue Errors::VagrantError => e + raise if !env[:box_outdated_ignore_errors] + env[:ui].detail(I18n.t( + "vagrant.box_outdated_metadata_error_single", + message: e.message)) + end + env[:box_outdated] = update != nil + if update + env[:ui].warn(I18n.t( + "vagrant.box_outdated_single", + name: update[0].name, + current: box.version, + latest: update[1].version)) + end + + @app.call(env) + end + + def check_outdated_local(env) + machine = env[:machine] + box = env[:box_collection].find( + machine.box.name, machine.box.provider, + "> #{machine.box.version}") + if box + env[:ui].warn(I18n.t( + "vagrant.box_outdated_local", + name: box.name, + old: machine.box.version, + new: box.version)) + env[:box_outdated] = true + return + end + + env[:box_outdated] = false + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/box_remove.rb b/lib/vagrant/action/builtin/box_remove.rb index 93a1dd355..faa1d685c 100644 --- a/lib/vagrant/action/builtin/box_remove.rb +++ b/lib/vagrant/action/builtin/box_remove.rb @@ -12,24 +12,67 @@ module Vagrant def call(env) box_name = env[:box_name] - box_provider = env[:box_provider].to_sym + box_provider = env[:box_provider] + box_provider = box_provider.to_sym if box_provider + box_version = env[:box_version] - box = nil - begin - box = env[:box_collection].find(box_name, box_provider) - rescue Vagrant::Errors::BoxUpgradeRequired - env[:box_collection].upgrade(box_name) - retry + boxes = {} + env[:box_collection].all.each do |n, v, p| + boxes[n] ||= {} + boxes[n][p] ||= [] + boxes[n][p] << v end - raise Vagrant::Errors::BoxNotFound, :name => box_name, :provider => box_provider if !box + all_box = boxes[box_name] + if !all_box + raise Errors::BoxRemoveNotFound, name: box_name + end + + all_versions = nil + if !box_provider + if all_box.length == 1 + # There is only one provider, just use that. + all_versions = all_box.values.first + box_provider = all_box.keys.first + else + raise Errors::BoxRemoveMultiProvider, + name: box_name, + providers: all_box.keys.map(&:to_s).sort.join(", ") + end + else + all_versions = all_box[box_provider] + if !all_versions + raise Errors::BoxRemoveProviderNotFound, + name: box_name, + provider: box_provider.to_s, + providers: all_box.keys.map(&:to_s).sort.join(", ") + end + end + + if !box_version + if all_versions.length == 1 + # There is only one version, just use that. + box_version = all_versions.first + else + # There are multiple versions, we can't choose. + raise Errors::BoxRemoveMultiVersion, + name: box_name, + provider: box_provider.to_s, + versions: all_versions.join(", ") + end + end + + box = env[:box_collection].find( + box_name, box_provider, box_version) + env[:ui].info(I18n.t("vagrant.commands.box.removing", - :name => box_name, - :provider => box_provider)) + :name => box.name, + :provider => box.provider)) box.destroy! # Passes on the removed box to the rest of the middleware chain env[:box_removed] = box + @app.call(env) end end diff --git a/lib/vagrant/action/builtin/box_update.rb b/lib/vagrant/action/builtin/box_update.rb new file mode 100644 index 000000000..bd510445b --- /dev/null +++ b/lib/vagrant/action/builtin/box_update.rb @@ -0,0 +1,20 @@ +require "log4r" + +module Vagrant + module Action + module Builtin + # This middleware updates a specific box if there are updates available. + class BoxUpdate + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new( + "vagrant::action::builtin::box_update") + end + + def call(env) + machine = env[:machine] + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/handle_box.rb b/lib/vagrant/action/builtin/handle_box.rb new file mode 100644 index 000000000..6aea7d9ea --- /dev/null +++ b/lib/vagrant/action/builtin/handle_box.rb @@ -0,0 +1,84 @@ +require "thread" + +require "log4r" + +module Vagrant + module Action + module Builtin + # This built-in middleware handles the `box` setting by verifying + # the box is already installed, dowloading the box if it isn't, + # updating the box if it is requested, etc. + class HandleBox + @@big_lock = Mutex.new + @@small_locks = Hash.new { |h,k| h[k] = Mutex.new } + + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::handle_box") + end + + def call(env) + machine = env[:machine] + + if !machine.config.vm.box + @logger.info("Skipping HandleBox because no box is set") + return @app.call(env) + end + + # Acquire a lock for this box to handle multi-threaded + # environments. + lock = nil + @@big_lock.synchronize do + lock = @@small_locks[machine.config.vm.box] + end + + lock.synchronize do + handle_box(env) + end + + # Reload the environment and set the VM to be the new loaded VM. + env[:machine] = env[:machine].env.machine( + env[:machine].name, env[:machine].provider_name, true) + + @app.call(env) + end + + def handle_box(env) + machine = env[:machine] + + if machine.box + @logger.info("Machine already has box. HandleBox will not run.") + return + end + + # Determine the set of formats that this box can be in + box_download_ca_cert = env[:machine].config.vm.box_download_ca_cert + box_download_client_cert = env[:machine].config.vm.box_download_client_cert + box_download_insecure = env[:machine].config.vm.box_download_insecure + box_formats = env[:machine].provider_options[:box_format] || + env[:machine].provider_name + + env[:ui].output(I18n.t( + "vagrant.box_auto_adding", name: machine.config.vm.box)) + env[:ui].detail("Box Provider: #{Array(box_formats).join(", ")}") + env[:ui].detail("Box Version: #{machine.config.vm.box_version}") + + begin + env[:action_runner].run(Vagrant::Action.action_box_add, env.merge({ + box_name: machine.config.vm.box, + box_url: machine.config.vm.box_url || machine.config.vm.box, + box_provider: box_formats, + box_version: machine.config.vm.box_version, + box_client_cert: box_download_client_cert, + box_download_ca_cert: box_download_ca_cert, + box_download_insecure: box_download_insecure, + })) + rescue Errors::BoxAlreadyExists + # Just ignore this, since it means the next part will succeed! + # This can happen in a multi-threaded environment. + end + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/handle_box_url.rb b/lib/vagrant/action/builtin/handle_box_url.rb index 6a71be6a5..b77937883 100644 --- a/lib/vagrant/action/builtin/handle_box_url.rb +++ b/lib/vagrant/action/builtin/handle_box_url.rb @@ -1,96 +1,12 @@ -require "thread" - -require "log4r" - module Vagrant module Action module Builtin - # This built-in middleware handles the `box_url` setting, downloading - # the box if necessary. You should place this early in your middleware - # sequence for a provider after configuration validation but before - # you attempt to use any box. - class HandleBoxUrl - @@big_lock = Mutex.new - @@handle_box_url_locks = Hash.new { |h,k| h[k] = Mutex.new } - - def initialize(app, env) - @app = app - @logger = Log4r::Logger.new("vagrant::action::builtin::handle_box_url") - end - + class HandleBoxUrl < HandleBox def call(env) - if !env[:machine].config.vm.box || !env[:machine].config.vm.box_url - @logger.info("Skipping HandleBoxUrl because box or box_url not set.") - @app.call(env) - return - end - - if env[:machine].box - @logger.info("Skipping HandleBoxUrl because box is already available") - @app.call(env) - return - end - - # Get a "big lock" to make sure that our more fine grained - # lock access is thread safe. - lock = nil - @@big_lock.synchronize do - lock = @@handle_box_url_locks[env[:machine].config.vm.box] - end - - box_name = env[:machine].config.vm.box - box_url = env[:machine].config.vm.box_url - box_download_ca_cert = env[:machine].config.vm.box_download_ca_cert - box_download_checksum = env[:machine].config.vm.box_download_checksum - box_download_checksum_type = env[:machine].config.vm.box_download_checksum_type - box_download_client_cert = env[:machine].config.vm.box_download_client_cert - box_download_insecure = env[:machine].config.vm.box_download_insecure - - # Expand the CA cert file relative to the Vagrantfile path, if - # there is one. - if box_download_ca_cert - box_download_ca_cert = File.expand_path( - box_download_ca_cert, env[:machine].env.root_path) - end - - lock.synchronize do - # Check that we don't already have the box, which can happen - # if we're slow to acquire the lock because of another thread - box_formats = env[:machine].provider_options[:box_format] || - env[:machine].provider_name - if env[:box_collection].find(box_name, box_formats) - break - end - - # Add the box then reload the box collection so that it becomes - # aware of it. - env[:ui].info I18n.t( - "vagrant.actions.vm.check_box.not_found", - :name => box_name, - :provider => env[:machine].provider_name) - - begin - env[:action_runner].run(Vagrant::Action.action_box_add, { - :box_checksum => box_download_checksum, - :box_checksum_type => box_download_checksum_type, - :box_client_cert => box_download_client_cert, - :box_download_ca_cert => box_download_ca_cert, - :box_download_insecure => box_download_insecure, - :box_name => box_name, - :box_provider => box_formats, - :box_url => box_url, - }) - rescue Errors::BoxAlreadyExists - # Just ignore this, since it means the next part will succeed! - # This can happen in a multi-threaded environment. - end - end - - # Reload the environment and set the VM to be the new loaded VM. - env[:machine] = env[:machine].env.machine( - env[:machine].name, env[:machine].provider_name, true) - - @app.call(env) + env[:ui].warn("HandleBoxUrl middleware is deprecated. Use HandleBox instead.") + env[:ui].warn("This is a bug with the provider. Please contact the creator") + env[:ui].warn("of the provider you use to fix this.") + super end end end diff --git a/lib/vagrant/box.rb b/lib/vagrant/box.rb index e7e89a9ca..1e1c26dba 100644 --- a/lib/vagrant/box.rb +++ b/lib/vagrant/box.rb @@ -1,8 +1,11 @@ require 'fileutils' +require "tempfile" require "json" require "log4r" +require "vagrant/box_metadata" +require "vagrant/util/downloader" require "vagrant/util/platform" require "vagrant/util/safe_chdir" require "vagrant/util/subprocess" @@ -23,6 +26,11 @@ module Vagrant # @return [Symbol] attr_reader :provider + # The version of this box. + # + # @return [String] + attr_reader :version + # This is the directory on disk where this box exists. # # @return [Pathname] @@ -34,16 +42,24 @@ module Vagrant # @return [Hash] attr_reader :metadata + # This is the URL to the version info and other metadata for this + # box. + # + # @return [String] + attr_reader :metadata_url + # This is used to initialize a box. # # @param [String] name Logical name of the box. # @param [Symbol] provider The provider that this box implements. # @param [Pathname] directory The directory where this box exists on # disk. - def initialize(name, provider, directory) + def initialize(name, provider, version, directory, **opts) @name = name + @version = version @provider = provider @directory = directory + @metadata_url = opts[:metadata_url] metadata_file = directory.join("metadata.json") raise Errors::BoxMetadataFileNotFound, :name => @name if !metadata_file.file? @@ -69,6 +85,51 @@ module Vagrant return true end + # Loads the metadata URL and returns the latest metadata associated + # with this box. + # + # @return [BoxMetadata] + def load_metadata + tf = Tempfile.new("vagrant") + tf.close + + url = @metadata_url + if File.file?(url) || url !~ /^[a-z0-9]+:.*$/i + url = File.expand_path(url) + url = Util::Platform.cygwin_windows_path(url) + url = "file:#{url}" + end + + Util::Downloader.new(url, tf.path).download! + BoxMetadata.new(File.open(tf.path, "r")) + end + + # Checks if the box has an update and returns the metadata, version, + # and provider. If the box doesn't have an update that satisfies the + # constraints, it will return nil. + # + # This will potentially make a network call if it has to load the + # metadata from the network. + # + # @param [String] version Version constraints the update must + # satisfy. If nil, the version constrain defaults to being a + # larger version than this box. + # @return [Array] + def has_update?(version=nil) + if !@metadata_url + raise Errors::BoxUpdateNoMetadata, name: @name + end + + version += ", " if version + version ||= "" + version += "> #{@version}" + md = self.load_metadata + newer = md.version(version, provider: @provider) + return nil if !newer + + [md, newer, newer.provider(@provider)] + end + # This repackages this box and outputs it to the given path. # # @param [Pathname] path The full path (filename included) of where @@ -96,7 +157,8 @@ module Vagrant return super if !other.is_a?(self.class) # Comparison is done by composing the name and provider - "#{@name}-#{@provider}" <=> "#{other.name}-#{other.provider}" + "#{@name}-#{@version}-#{@provider}" <=> + "#{other.name}-#{other.version}-#{other.provider}" end end end diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb index 9d26f5af5..62f942389 100644 --- a/lib/vagrant/box_collection.rb +++ b/lib/vagrant/box_collection.rb @@ -1,5 +1,5 @@ require "digest/sha1" -require "thread" +require "monitor" require "tmpdir" require "log4r" @@ -44,7 +44,7 @@ module Vagrant options ||= {} @directory = directory - @lock = Mutex.new + @lock = Monitor.new @temp_root = options[:temp_dir_root] @logger = Log4r::Logger.new("vagrant::box_collection") end @@ -56,53 +56,54 @@ module Vagrant # * BoxProviderDoesntMatch - If the given box provider doesn't match the # actual box provider in the untarred box. # * BoxUnpackageFailure - An invalid tar file. - # * BoxUpgradeRequired - You're attempting to add a box when there is a - # V1 box with the same name that must first be upgraded. # # Preconditions: # * File given in `path` must exist. # # @param [Pathname] path Path to the box file on disk. # @param [String] name Logical name for the box. - # @param [Symbol] provider The provider that the box should be for. This - # will be verified with the `metadata.json` file in the box and is + # @param [String] version The version of this box. + # @param [Array] providers The providers that this box can + # be a part of. This will be verified with the `metadata.json` and is # meant as a basic check. If this isn't given, then whatever provider # the box represents will be added. # @param [Boolean] force If true, any existing box with the same name # and provider will be replaced. - def add(path, name, formats=nil, force=false) - formats = [formats] if formats && !formats.is_a?(Array) + def add(path, name, version, **opts) + providers = opts[:providers] + providers = Array(providers) if providers provider = nil - with_collection_lock do - # A helper to check if a box exists. We store this in a variable - # since we call it multiple times. - check_box_exists = lambda do |box_formats| - box = find(name, box_formats) - next if !box + # A helper to check if a box exists. We store this in a variable + # since we call it multiple times. + check_box_exists = lambda do |box_formats| + box = find(name, box_formats, version) + next if !box - if !force - @logger.error("Box already exists, can't add: #{name} #{box_formats.join(", ")}") - raise Errors::BoxAlreadyExists, :name => name, :formats => box_formats.join(", ") - end - - # We're forcing, so just delete the old box - @logger.info( - "Box already exists, but forcing so removing: #{name} #{box_formats.join(", ")}") - box.destroy! + if !opts[:force] + @logger.error( + "Box already exists, can't add: #{name} v#{version} #{box_formats.join(", ")}") + raise Errors::BoxAlreadyExists, + name: name, + provider: box_formats.join(", "), + version: version end - log_provider = formats ? formats.join(", ") : "any provider" + # We're forcing, so just delete the old box + @logger.info( + "Box already exists, but forcing so removing: " + + "#{name} v#{version} #{box_formats.join(", ")}") + box.destroy! + end + + with_collection_lock do + log_provider = providers ? providers.join(", ") : "any provider" @logger.debug("Adding box: #{name} (#{log_provider}) from #{path}") # Verify the box doesn't exist early if we're given a provider. This # can potentially speed things up considerably since we don't need # to unpack any files. - check_box_exists.call(formats) if formats - - # Verify that a V1 box doesn't exist. If it does, then we signal - # to the user that we need an upgrade. - raise Errors::BoxUpgradeRequired, :name => name if v1_box?(@directory.join(name)) + check_box_exists.call(providers) if providers # Create a temporary directory since we're not sure at this point if # the box we're unpackaging already exists (if no provider was given) @@ -111,7 +112,10 @@ module Vagrant @logger.debug("Unpacking box into temporary directory: #{temp_dir}") result = Util::Subprocess.execute( "bsdtar", "-v", "-x", "-m", "-C", temp_dir.to_s, "-f", path.to_s) - raise Errors::BoxUnpackageFailure, :output => result.stderr.to_s if result.exit_code != 0 + if result.exit_code != 0 + raise Errors::BoxUnpackageFailure, + output: result.stderr.to_s + end # If we get a V1 box, we want to update it in place if v1_box?(temp_dir) @@ -125,22 +129,14 @@ module Vagrant with_temp_dir(temp_dir) do |final_temp_dir| # Get an instance of the box we just added before it is finalized # in the system so we can inspect and use its metadata. - box = Box.new(name, nil, final_temp_dir) + box = Box.new(name, nil, version, final_temp_dir) # Get the provider, since we'll need that to at the least add it # to the system or check that it matches what is given to us. box_provider = box.metadata["provider"] - if formats - found = false - formats.each do |format| - # Verify that the given provider matches what the box has. - if box_provider.to_sym == format.to_sym - found = true - break - end - end - + if providers + found = providers.find { |p| p.to_sym == box_provider.to_sym } if !found @logger.error("Added box provider doesnt match expected: #{log_provider}") raise Errors::BoxProviderDoesntMatch, @@ -155,7 +151,8 @@ module Vagrant provider = box_provider.to_sym # Create the directory for this box, not including the provider - box_dir = @directory.join(name) + root_box_dir = @directory.join(dir_name(name)) + box_dir = root_box_dir.join(version) box_dir.mkpath @logger.debug("Box directory: #{box_dir}") @@ -177,20 +174,25 @@ module Vagrant @logger.debug("Moving: #{f} => #{destination}") FileUtils.mv(f, destination) end + + if opts[:metadata_url] + root_box_dir.join("metadata_url").open("w") do |f| + f.write(opts[:metadata_url]) + end + end end end end # Return the box - find(name, provider) + find(name, provider, version) end # This returns an array of all the boxes on the system, given by # their name and their provider. # - # @return [Array] Array of `[name, provider]` pairs of the boxes - # installed on this system. An optional third element in the array - # may specify `:v1` if the box is a version 1 box. + # @return [Array] Array of `[name, version, provider]` of the boxes + # installed on this system. def all results = [] @@ -201,27 +203,25 @@ module Vagrant # us in our folder structure. next if !child.directory? - box_name = child.basename.to_s + box_name = undir_name(child.basename.to_s) - # If this is a V1 box, we still return that name, but specify - # that the box is a V1 box. - if v1_box?(child) - @logger.debug("V1 box found: #{box_name}") - results << [box_name, :virtualbox, :v1] - next - end - - # Otherwise, traverse the subdirectories and see what providers + # Otherwise, traverse the subdirectories and see what versions # we have. - child.children(true).each do |provider| - # Verify this is a potentially valid box. If it looks - # correct enough then include it. - if provider.directory? && provider.join("metadata.json").file? - provider_name = provider.basename.to_s.to_sym - @logger.debug("Box: #{box_name} (#{provider_name})") - results << [box_name, provider_name] - else - @logger.debug("Invalid box, ignoring: #{provider}") + child.children(true).each do |versiondir| + next if !versiondir.directory? + + version = versiondir.basename.to_s + + versiondir.children(true).each do |provider| + # Verify this is a potentially valid box. If it looks + # correct enough then include it. + if provider.directory? && provider.join("metadata.json").file? + provider_name = provider.basename.to_s.to_sym + @logger.debug("Box: #{box_name} (#{provider_name})") + results << [box_name, version, provider_name] + else + @logger.debug("Invalid box, ignoring: #{provider}") + end end end end @@ -234,78 +234,108 @@ module Vagrant # # @param [String] name Name of the box (logical name). # @param [Array] providers Providers that the box implements. + # @param [String] version Version constraints to adhere to. Example: + # "~> 1.0" or "= 1.0, ~> 1.1" # @return [Box] The box found, or `nil` if not found. - def find(name, providers) - providers = [providers].flatten + def find(name, providers, version) + providers = Array(providers) + + # Build up the requirements we have + requirements = version.split(",").map do |v| + Gem::Requirement.new(v.strip) + end with_collection_lock do - providers.each do |provider| - # First look directly for the box we're asking for. - box_directory = @directory.join(name, provider.to_s, "metadata.json") - @logger.info("Searching for box: #{name} (#{provider}) in #{box_directory}") - if box_directory.file? - @logger.info("Box found: #{name} (#{provider})") - return Box.new(name, provider, box_directory.dirname) + box_directory = @directory.join(dir_name(name)) + if !box_directory.directory? + @logger.info("Box not found: #{name} (#{providers.join(", ")})") + return nil + end + + versions = box_directory.children(true).map do |versiondir| + next if !versiondir.directory? + version = versiondir.basename.to_s + Gem::Version.new(version) + end.compact + + # Traverse through versions with the latest version first + versions.sort.reverse.each do |v| + if !requirements.all? { |r| r.satisfied_by?(v) } + # Unsatisfied version requirements + next end - # If we're looking for a VirtualBox box, then we check if there is - # a V1 box. - if provider.to_sym == :virtualbox - # Check if a V1 version of this box exists, and if so, raise an - # exception notifying the caller that the box exists but needs - # to be upgraded. We don't do the upgrade here because it can be - # a fairly intensive activity and don't want to immediately degrade - # user performance on a find. - # - # To determine if it is a V1 box we just do a simple heuristic - # based approach. - @logger.info("Searching for V1 box: #{name}") - if v1_box?(@directory.join(name)) - @logger.warn("V1 box found: #{name}") - raise Errors::BoxUpgradeRequired, :name => name - end + versiondir = box_directory.join(v.to_s) + providers.each do |provider| + provider_dir = versiondir.join(provider.to_s) + next if !provider_dir.directory? + @logger.info("Box found: #{name} (#{provider})") + + metadata_url = nil + metadata_url_file = box_directory.join("metadata_url") + metadata_url = metadata_url_file.read if metadata_url_file.file? + + return Box.new( + name, provider, v.to_s, provider_dir, + metadata_url: metadata_url, + ) end end end - # Didn't find it, return nil - @logger.info("Box not found: #{name} (#{providers.join(", ")})") nil end - # Upgrades a V1 box with the given name to a V2 box. If a box with the - # given name doesn't exist, then a `BoxNotFound` exception will be raised. - # If the given box is found but is not a V1 box then `true` is returned - # because this just works fine. - # - # @param [String] name Name of the box (logical name). - # @return [Boolean] `true` otherwise an exception is raised. - def upgrade(name) + # This upgrades a v1.1 - v1.4 box directory structure up to a v1.5 + # directory structure. This will raise exceptions if it fails in any + # way. + def upgrade_v1_1_v1_5 with_collection_lock do - @logger.debug("Upgrade request for box: #{name}") - box_dir = @directory.join(name) + temp_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX, @temp_root)) - # If the box doesn't exist at all, raise an exception - raise Errors::BoxNotFound, :name => name, :provider => "virtualbox" if !box_dir.directory? + @directory.children(true).each do |boxdir| + # Ignore all non-directories because they can't be boxes + next if !boxdir.directory? - if v1_box?(box_dir) - @logger.debug("V1 box #{name} found. Upgrading!") + box_name = boxdir.basename.to_s - # First we actually perform the upgrade - temp_dir = v1_upgrade(box_dir) + # If it is a v1 box, then we need to upgrade it first + if v1_box?(boxdir) + upgrade_dir = v1_upgrade(boxdir) + FileUtils.mv(upgrade_dir, boxdir.join("virtualbox")) + end - # Rename the temporary directory to the provider. - FileUtils.mv(temp_dir.to_s, box_dir.join("virtualbox").to_s) - @logger.info("Box '#{name}' upgraded from V1 to V2.") + # Create the directory for this box + new_box_dir = temp_dir.join(dir_name(box_name), "0") + new_box_dir.mkpath + + # Go through each provider and move it + boxdir.children(true).each do |providerdir| + FileUtils.cp_r(providerdir, new_box_dir.join(providerdir.basename)) + end end - end - # We did it! Or the v1 box didn't exist so it doesn't matter. - return true + # Move the folder into place + @directory.rmtree + FileUtils.mv(temp_dir.to_s, @directory.to_s) + end end protected + # Returns the directory name for the box of the given name. + # + # @param [String] name + # @return [String] + def dir_name(name) + name.gsub("/", "-VAGRANTSLASH-") + end + + # Returns the directory name for the box cleaned up + def undir_name(name) + name.gsub("-VAGRANTSLASH-", "/") + end + # This checks if the given directory represents a V1 box on the # system. # @@ -361,17 +391,7 @@ module Vagrant # This locks the region given by the block with a lock on this # collection. def with_collection_lock - lock = @lock - - begin - lock.synchronize {} - rescue ThreadError - # If we already hold the lock, just create a new lock so - # we definitely don't block and don't get an error. - lock = Mutex.new - end - - lock.synchronize do + @lock.synchronize do return yield end end diff --git a/lib/vagrant/box_metadata.rb b/lib/vagrant/box_metadata.rb new file mode 100644 index 000000000..20562452a --- /dev/null +++ b/lib/vagrant/box_metadata.rb @@ -0,0 +1,129 @@ +require "json" + +module Vagrant + # BoxMetadata represents metadata about a box, including the name + # it should have, a description of it, the versions it has, and + # more. + class BoxMetadata + # The name that the box should be if it is added. + # + # @return [String] + attr_accessor :name + + # The long-form human-readable description of a box. + # + # @return [String] + attr_accessor :description + + # Loads the metadata associated with the box from the given + # IO. + # + # @param [IO] io An IO object to read the metadata from. + def initialize(io) + begin + @raw = JSON.load(io) + rescue JSON::ParserError => e + raise Errors::BoxMetadataMalformed, + error: e.to_s + end + + @raw ||= {} + @name = @raw["name"] + @description = @raw["description"] + @version_map = (@raw["versions"] || []).map do |v| + [Gem::Version.new(v["version"]), v] + end + @version_map = Hash[@version_map] + + # TODO: check for corruption: + # - malformed version + end + + # Returns data about a single version that is included in this + # metadata. + # + # @param [String] version The version to return, this can also + # be a constraint. + # @return [Version] The matching version or nil if a matching + # version was not found. + def version(version, **opts) + requirements = version.split(",").map do |v| + Gem::Requirement.new(v.strip) + end + + providers = nil + providers = Array(opts[:provider]).map(&:to_sym) if opts[:provider] + + @version_map.keys.sort.reverse.each do |v| + next if !requirements.all? { |r| r.satisfied_by?(v) } + version = Version.new(@version_map[v]) + next if (providers & version.providers).empty? if providers + return version + end + + nil + end + + # Returns all the versions supported by this metadata. These + # versions are sorted so the last element of the list is the + # latest version. + # + # @return[Array] + def versions + @version_map.keys.sort.map(&:to_s) + end + + # Represents a single version within the metadata. + class Version + # The version that this Version object represents. + # + # @return [String] + attr_accessor :version + + def initialize(raw=nil) + return if !raw + + @version = raw["version"] + @provider_map = (raw["providers"] || []).map do |p| + [p["name"].to_sym, p] + end + @provider_map = Hash[@provider_map] + end + + # Returns a [Provider] for the given name, or nil if it isn't + # supported by this version. + def provider(name) + p = @provider_map[name.to_sym] + return nil if !p + Provider.new(p) + end + + # Returns the providers that are available for this version + # of the box. + # + # @return [Array] + def providers + @provider_map.keys.map(&:to_sym) + end + end + + # Provider represents a single provider-specific box available + # for a version for a box. + class Provider + # The name of the provider. + # + # @return [String] + attr_accessor :name + + # The URL of the box. + # + # @return [String] + attr_accessor :url + + def initialize(raw) + @name = raw["name"] + @url = raw["url"] + end + end + end +end diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 29e805659..38ced5377 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -14,6 +14,12 @@ module Vagrant # defined as basically a folder with a "Vagrantfile." This class allows # access to the VMs, CLI, etc. all in the scope of this environment. class Environment + # This is the current version that this version of Vagrant is + # compatible with in the home directory. + # + # @return [String] + CURRENT_SETUP_VERSION = "1.5" + DEFAULT_LOCAL_DATA = ".vagrant" # The `cwd` that this environment represents @@ -342,14 +348,8 @@ module Vagrant load_box_and_overrides = lambda do box = nil if config.vm.box - begin - box = boxes.find(config.vm.box, box_formats) - rescue Errors::BoxUpgradeRequired - # Upgrade the box if we must - @logger.info("Upgrading box during config load: #{config.vm.box}") - boxes.upgrade(config.vm.box) - retry - end + box = boxes.find( + config.vm.box, box_formats, config.vm.box_version) end # If a box was found, then we attempt to load the Vagrantfile for @@ -630,13 +630,37 @@ module Vagrant raise Errors::HomeDirectoryNotAccessible, home_path: @home_path.to_s end - # Create the version file to mark the version of the home directory - # we're using. + # Create the version file that we use to track the structure of + # the home directory. If we have an old version, we need to explicitly + # upgrade it. Otherwise, we just mark that its the current version. version_file = @home_path.join("setup_version") + if version_file.file? + version = version_file.read + if version > CURRENT_SETUP_VERSION + raise Errors::HomeDirectoryLaterVersion + end + + case version + when CURRENT_SETUP_VERSION + # We're already good, at the latest version. + when "1.1" + # We need to update our directory structure + upgrade_home_path_v1_1 + + # Delete the version file so we put our latest version in + version_file.delete + else + raise Errors::HomeDirectoryUnknownVersion, + path: @home_path.to_s, + version: version + end + end + if !version_file.file? - @logger.debug("Setting up the version file.") + @logger.debug( + "Creating home directory version file: #{CURRENT_SETUP_VERSION}") version_file.open("w") do |f| - f.write("1.1") + f.write(CURRENT_SETUP_VERSION) end end @@ -723,6 +747,14 @@ module Vagrant nil end + # This upgrades a home directory that was in the v1.1 format to the + # v1.5 format. It will raise exceptions if anything fails. + def upgrade_home_path_v1_1 + collection = BoxCollection.new( + @home_path.join("boxes"), temp_dir_root: tmp_path) + collection.upgrade_v1_1_v1_5 + end + # This upgrades a Vagrant 1.0.x "dotfile" to the new V2 format. # # This is a destructive process. Once the upgrade is complete, the diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 0680de435..01d3219de 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -120,8 +120,32 @@ module Vagrant error_key(:batch_multi_error) end + class BoxAddMetadataMultiURL < VagrantError + error_key(:box_add_metadata_multi_url) + end + + class BoxAddNameMismatch < VagrantError + error_key(:box_add_name_mismatch) + end + + class BoxAddNameRequired < VagrantError + error_key(:box_add_name_required) + end + + class BoxAddNoMatchingProvider < VagrantError + error_key(:box_add_no_matching_provider) + end + + class BoxAddNoMatchingVersion < VagrantError + error_key(:box_add_no_matching_version) + end + + class BoxAddShortNotFound < VagrantError + error_key(:box_add_short_not_found) + end + class BoxAlreadyExists < VagrantError - error_key(:already_exists, "vagrant.actions.box.unpackage") + error_key(:box_add_exists) end class BoxChecksumInvalidType < VagrantError @@ -144,20 +168,56 @@ module Vagrant error_key(:box_metadata_file_not_found) end + class BoxMetadataMalformed < VagrantError + error_key(:box_metadata_malformed) + end + class BoxNotFound < VagrantError error_key(:box_not_found) end + class BoxNotFoundWithProvider < VagrantError + error_key(:box_not_found_with_provider) + end + + class BoxOutdatedNoBox < VagrantError + error_key(:box_outdated_no_box) + end + class BoxProviderDoesntMatch < VagrantError error_key(:box_provider_doesnt_match) end + class BoxRemoveNotFound < VagrantError + error_key(:box_remove_not_found) + end + + class BoxRemoveProviderNotFound < VagrantError + error_key(:box_remove_provider_not_found) + end + + class BoxRemoveMultiProvider < VagrantError + error_key(:box_remove_multi_provider) + end + + class BoxRemoveMultiVersion < VagrantError + error_key(:box_remove_multi_version) + end + + class BoxServerNotSet < VagrantError + error_key(:box_server_not_set) + end + class BoxUnpackageFailure < VagrantError error_key(:untar_failure, "vagrant.actions.box.unpackage") end - class BoxUpgradeRequired < VagrantError - error_key(:box_upgrade_required) + class BoxUpdateMultiProvider < VagrantError + error_key(:box_update_multi_provider) + end + + class BoxUpdateNoMetadata < VagrantError + error_key(:box_update_no_metadata) end class BoxVerificationFailed < VagrantError @@ -260,10 +320,18 @@ module Vagrant error_key(:environment_locked) end + class HomeDirectoryLaterVersion < VagrantError + error_key(:home_dir_later_version) + end + class HomeDirectoryNotAccessible < VagrantError error_key(:home_dir_not_accessible) end + class HomeDirectoryUnknownVersion < VagrantError + error_key(:home_dir_unknown_version) + end + class ForwardPortAdapterNotFound < VagrantError error_key(:forward_port_adapter_not_found) end diff --git a/lib/vagrant/shared_helpers.rb b/lib/vagrant/shared_helpers.rb index f1385987d..d1ab04053 100644 --- a/lib/vagrant/shared_helpers.rb +++ b/lib/vagrant/shared_helpers.rb @@ -1,6 +1,13 @@ require "pathname" module Vagrant + # This is the default endpoint of the Vagrant Cloud in + # use. API calls will be made to this for various functions + # of Vagrant that may require remote access. + # + # @return [String] + DEFAULT_SERVER_URL = "http://www.vagrantcloud.com" + # This returns whether or not 3rd party plugins should be loaded. # # @return [Boolean] @@ -8,6 +15,13 @@ module Vagrant !ENV["VAGRANT_NO_PLUGINS"] end + # Returns the URL prefix to the server. + # + # @return [String] + def self.server_url + ENV["VAGRANT_SERVER_URL"] || DEFAULT_SERVER_URL + end + # The source root is the path to the root directory of the Vagrant source. # # @return [Pathname] diff --git a/lib/vagrant/ui.rb b/lib/vagrant/ui.rb index 9563af974..e2e918f67 100644 --- a/lib/vagrant/ui.rb +++ b/lib/vagrant/ui.rb @@ -1,4 +1,5 @@ require "delegate" +require "io/console" require "thread" require "log4r" @@ -123,17 +124,28 @@ module Vagrant # Setup the options so that the new line is suppressed opts ||= {} + opts[:echo] = true if !opts.has_key?(:echo) opts[:new_line] = false if !opts.has_key?(:new_line) opts[:prefix] = false if !opts.has_key?(:prefix) # Output the data say(:info, message, opts) + input = nil + if opts[:echo] + input = $stdin.gets + else + input = $stdin.noecho(&:gets) + + # Output a newline because without echo, the newline isn't + # echoed either. + say(:info, "\n", opts) + end + # Get the results and chomp off the newline. We do a logical OR # here because `gets` can return a nil, for example in the case # that ctrl-D is pressed on the input. - input = $stdin.gets || "" - input.chomp + (input || "").chomp end # This is used to output progress reports to the UI. @@ -209,7 +221,10 @@ module Vagrant class_eval <<-CODE def #{method}(message, *args, **opts) super(message) - opts[:bold] = #{method.inspect} != :detail if !opts.has_key?(:bold) + if !opts.has_key?(:bold) + opts[:bold] = #{method.inspect} != :detail && \ + #{method.inspect} != :ask + end @ui.#{method}(format_message(#{method.inspect}, message, **opts), *args, **opts) end CODE @@ -241,7 +256,8 @@ module Vagrant prefix = "" if !opts.has_key?(:prefix) || opts[:prefix] prefix = OUTPUT_PREFIX - prefix = " " * OUTPUT_PREFIX.length if type == :detail + prefix = " " * OUTPUT_PREFIX.length if \ + type == :detail || type == :ask end # Fast-path if there is no prefix @@ -281,16 +297,17 @@ module Vagrant opts[:color] = :green if type == :success opts[:color] = :yellow if type == :warn - # If there is no color specified, exit early - return message if !opts.has_key?(:color) - # If it is a detail, it is not bold. Every other message type # is bolded. bold = !!opts[:bold] - color = COLORS[opts[:color]] + colorseq = "#{bold ? 1 : 0 }" + if opts[:color] + color = COLORS[opts[:color]] + colorseq += ";#{color}" + end # Color the message and make sure to reset the color at the end - "\033[#{bold ? 1 : 0};#{color}m#{message}\033[0m" + "\033[#{colorseq}m#{message}\033[0m" end end end diff --git a/lib/vagrant/util/downloader.rb b/lib/vagrant/util/downloader.rb index 6f26447d0..69f1e8700 100644 --- a/lib/vagrant/util/downloader.rb +++ b/lib/vagrant/util/downloader.rb @@ -1,6 +1,7 @@ require "log4r" require "vagrant/util/busy" +require "vagrant/util/platform" require "vagrant/util/subprocess" module Vagrant @@ -13,6 +14,9 @@ module Vagrant # are properly tracked. USER_AGENT = "Vagrant/#{VERSION}" + attr_reader :source + attr_reader :destination + def initialize(source, destination, options=nil) @logger = Log4r::Logger.new("vagrant::util::downloader") @source = source.to_s @@ -34,31 +38,10 @@ module Vagrant # If this method returns without an exception, the download # succeeded. An exception will be raised if the download failed. def download! - # Build the list of parameters to execute with cURL - options = [ - "--fail", - "--location", - "--max-redirs", "10", - "--user-agent", USER_AGENT, - "--output", @destination, - ] - - options += ["--cacert", @ca_cert] if @ca_cert - options += ["--continue-at", "-"] if @continue - options << "--insecure" if @insecure - options << "--cert" << @client_cert if @client_cert + options, subprocess_options = self.options + options += ["--output", @destination] options << @source - # Specify some options for the subprocess - subprocess_options = {} - - # If we're in Vagrant, then we use the packaged CA bundle - if Vagrant.in_installer? - subprocess_options[:env] ||= {} - subprocess_options[:env]["CURL_CA_BUNDLE"] = - File.expand_path("cacert.pem", ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"]) - end - # This variable can contain the proc that'll be sent to # the subprocess execute. data_proc = nil @@ -109,12 +92,48 @@ module Vagrant output = "Progress: #{columns[0]}% (Rate: #{columns[11]}/s, Estimated time remaining: #{columns[10]})" @ui.clear_line - @ui.info(output, :new_line => false) + @ui.detail(output, :new_line => false) end end end - # Add the subprocess options onto the options we'll execute with + @logger.info("Downloader starting download: ") + @logger.info(" -- Source: #{@source}") + @logger.info(" -- Destination: #{@destination}") + + begin + execute_curl(options, subprocess_options, &data_proc) + ensure + # If we're outputting to the UI, clear the output to + # avoid lingering progress meters. + if @ui + @ui.clear_line + + # Windows doesn't clear properly for some reason, so we just + # output one more newline. + @ui.detail("") if Platform.windows? + end + end + + # Everything succeeded + true + end + + # Does a HEAD request of the URL and returns the output. + def head + options, subprocess_options = self.options + options.unshift("-I") + options << @source + + @logger.info("HEAD: #{@source}") + result = execute_curl(options, subprocess_options) + result.stdout + end + + protected + + def execute_curl(options, subprocess_options, &data_proc) + options = options.dup options << subprocess_options # Create the callback that is called if we are interrupted @@ -124,10 +143,6 @@ module Vagrant interrupted = true end - @logger.info("Downloader starting download: ") - @logger.info(" -- Source: #{@source}") - @logger.info(" -- Destination: #{@destination}") - # Execute! result = Busy.busy(int_callback) do Subprocess.execute("curl", *options, &data_proc) @@ -136,10 +151,6 @@ module Vagrant # If the download was interrupted, then raise a specific error raise Errors::DownloaderInterrupted if interrupted - # If we're outputting to the UI, clear the output to - # avoid lingering progress meters. - @ui.clear_line if @ui - # If it didn't exit successfully, we need to parse the data and # show an error message. if result.exit_code != 0 @@ -149,8 +160,37 @@ module Vagrant raise Errors::DownloaderError, :message => parts[1].chomp end - # Everything succeeded - true + result + end + + # Returns the varoius cURL and subprocess options. + # + # @return [Array] + def options + # Build the list of parameters to execute with cURL + options = [ + "--fail", + "--location", + "--max-redirs", "10", + "--user-agent", USER_AGENT, + ] + + options += ["--cacert", @ca_cert] if @ca_cert + options += ["--continue-at", "-"] if @continue + options << "--insecure" if @insecure + options << "--cert" << @client_cert if @client_cert + + # Specify some options for the subprocess + subprocess_options = {} + + # If we're in Vagrant, then we use the packaged CA bundle + if Vagrant.in_installer? + subprocess_options[:env] ||= {} + subprocess_options[:env]["CURL_CA_BUNDLE"] = + File.expand_path("cacert.pem", ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"]) + end + + return [options, subprocess_options] end end end diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index f53283704..976700ce9 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -8,18 +8,10 @@ module VagrantPlugins options = {} opts = OptionParser.new do |o| - o.banner = "Usage: vagrant box add [--provider provider] [-h]" + o.banner = "Usage: vagrant box add [-h]" o.separator "" - o.on("--checksum VALUE", String, "Checksum") do |c| - options[:checksum] = c - end - - o.on("--checksum-type VALUE", String, "Checksum type") do |c| - options[:checksum_type] = c.to_sym - end - - o.on("-c", "--clean", "Remove old temporary download if it exists.") do |c| + o.on("-c", "--clean", "Clean any temporary download files") do |c| options[:clean] = c end @@ -27,45 +19,72 @@ module VagrantPlugins options[:force] = f end - o.on("--insecure", "If set, SSL certs will not be validated.") do |i| + o.on("--insecure", "Do not validate SSL certificates") do |i| options[:insecure] = i end - o.on("--cacert certfile", String, "CA certificate") do |c| + o.on("--cacert certfile", String, "CA certificate for SSL download") do |c| options[:ca_cert] = c end o.on("--cert certfile", String, - "The client SSL cert") do |c| + "A client SSL cert, if needed") do |c| options[:client_cert] = c end - o.on("--provider provider", String, - "The provider that backs the box.") do |p| + o.on("--provider VALUE", String, "Provider the box should satisfy") do |p| options[:provider] = p end + + o.on("--box-version VALUE", String, "Constrain version of the added box") do |v| + options[:version] = v + end + + o.separator "" + o.separator "The options below only apply if you're adding a box file directly," + o.separator "and not using a Vagrant server or a box structured like 'user/box':" + o.separator "" + + o.on("--checksum VALUE", String, "Checksum for the box") do |c| + options[:checksum] = c + end + + o.on("--checksum-type VALUE", String, "Checksum type (md5, sha1, sha256)") do |c| + options[:checksum_type] = c.to_sym + end + + o.on("--name VALUE", String, "Name of the box") do |n| + options[:name] = n + end end # Parse the options argv = parse_options(opts) return if !argv - raise Vagrant::Errors::CLIInvalidUsage, :help => opts.help.chomp if argv.length < 2 + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end - # Get the provider if one was set - provider = nil - provider = options[:provider].to_sym if options[:provider] + url = argv[0] + if argv.length == 2 + options[:name] = argv[0] + url = argv[1] + end @env.action_runner.run(Vagrant::Action.action_box_add, { - :box_name => argv[0], - :box_provider => provider, - :box_url => argv[1], - :box_checksum_type => options[:checksum_type], - :box_checksum => options[:checksum], - :box_clean => options[:clean], - :box_force => options[:force], - :box_download_ca_cert => options[:ca_cert], - :box_download_client_cert => options[:client_cert], - :box_download_insecure => options[:insecure], + box_url: url, + box_name: options[:name], + box_provider: options[:provider], + box_version: options[:version], + box_checksum_type: options[:checksum_type], + box_checksum: options[:checksum], + box_clean: options[:clean], + box_force: options[:force], + box_download_ca_cert: options[:ca_cert], + box_download_client_cert: options[:client_cert], + box_download_insecure: options[:insecure], + ui: Vagrant::UI::Prefixed.new(@env.ui, "box"), }) # Success, exit status 0 diff --git a/plugins/commands/box/command/list.rb b/plugins/commands/box/command/list.rb index 7b2d09408..5e5c78038 100644 --- a/plugins/commands/box/command/list.rb +++ b/plugins/commands/box/command/list.rb @@ -42,13 +42,15 @@ module VagrantPlugins # ignore the "v1" param for now since I'm not yet sure if its # important for the user to know what boxes need to be upgraded # and which don't, since we plan on doing that transparently. - boxes.each do |name, provider, _v1| - @env.ui.info("#{name.ljust(longest_box_length)} (#{provider})", :prefix => false) + boxes.each do |name, version, provider| + @env.ui.info("#{name.ljust(longest_box_length)} (#{provider})") @env.ui.machine("box-name", name) @env.ui.machine("box-provider", provider) + @env.ui.machine("box-version", version) - info_file = @env.boxes.find(name, provider).directory.join("info.json") + info_file = @env.boxes.find(name, provider, version). + directory.join("info.json") if info_file.file? info = JSON.parse(info_file.read) info.each do |k, v| diff --git a/plugins/commands/box/command/outdated.rb b/plugins/commands/box/command/outdated.rb new file mode 100644 index 000000000..1dcd61de1 --- /dev/null +++ b/plugins/commands/box/command/outdated.rb @@ -0,0 +1,89 @@ +require 'optparse' + +module VagrantPlugins + module CommandBox + module Command + class Outdated < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant box outdated [options]" + o.separator "" + o.separator "Checks if there is a new version available for the box" + o.separator "that are you are using. If you pass in the --global flag," + o.separator "all boxes will be checked for updates." + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--global", "Check all boxes installed.") do |g| + options[:global] = g + end + end + + argv = parse_options(opts) + return if !argv + + # If we're checking the boxes globally, then do that. + if options[:global] + outdated_global + return 0 + end + + with_target_vms(argv) do |machine| + @env.action_runner.run(Vagrant::Action.action_box_outdated, { + box_outdated_force: true, + box_outdated_refresh: true, + box_outdated_success_ui: true, + machine: machine, + }) + end + end + + def outdated_global + boxes = {} + @env.boxes.all.reverse.each do |name, version, provider| + next if boxes[name] + boxes[name] = @env.boxes.find(name, provider, version) + end + + boxes.values.each do |box| + if !box.metadata_url + @env.ui.output(I18n.t( + "vagrant.box_outdated_no_metadata", + name: box.name)) + next + end + + md = nil + begin + md = box.load_metadata + rescue Vagrant::Errors::DownloaderError => e + @env.ui.error(I18n.t( + "vagrant.box_outdated_metadata_error", + name: box.name, + message: e.extra_data[:message])) + next + end + + current = Gem::Version.new(box.version) + latest = Gem::Version.new(md.versions.last) + if latest <= current + @env.ui.success(I18n.t( + "vagrant.box_up_to_date", + name: box.name, + version: box.version)) + else + @env.ui.warn(I18n.t( + "vagrant.box_outdated", + name: box.name, + current: box.version, + latest: latest.to_s,)) + end + end + end + end + end + end +end diff --git a/plugins/commands/box/command/remove.rb b/plugins/commands/box/command/remove.rb index d58f5bcec..0bc8dcf52 100644 --- a/plugins/commands/box/command/remove.rb +++ b/plugins/commands/box/command/remove.rb @@ -5,38 +5,42 @@ module VagrantPlugins module Command class Remove < Vagrant.plugin("2", :command) def execute + options = {} opts = OptionParser.new do |o| - o.banner = "Usage: vagrant box remove " + o.banner = "Usage: vagrant box remove " + o.separator "" + + o.on("--provider VALUE", String, + "The specific provider type for the box to remove.") do |p| + options[:provider] = p + end + + o.on("--box-version VALUE", String, + "The specific version of the box to remove.") do |v| + options[:version] = v + end end # Parse the options argv = parse_options(opts) return if !argv - raise Vagrant::Errors::CLIInvalidUsage, :help => opts.help.chomp if argv.length < 1 + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end - if !argv[1] - # Try to automatically determine the provider. - providers = [] - @env.boxes.all.each do |name, provider| - if name == argv[0] - providers << provider - end - end - - if providers.length > 1 - @env.ui.error( - I18n.t("vagrant.commands.box.remove_must_specify_provider", - name: argv[0], - providers: providers.join(", "))) - return 1 - end - - argv[1] = providers[0] || "" + if argv.length == 2 + # @deprecated + @env.ui.warn("WARNING: The second argument to `vagrant box remove`") + @env.ui.warn("is deprecated. Please use the --provider flag. This") + @env.ui.warn("feature will stop working in the next version.") + options[:provider] = argv[1] end @env.action_runner.run(Vagrant::Action.action_box_remove, { :box_name => argv[0], - :box_provider => argv[1] + :box_provider => options[:provider], + :box_version => options[:version], }) # Success, exit status 0 diff --git a/plugins/commands/box/command/repackage.rb b/plugins/commands/box/command/repackage.rb index 103c0da92..fa986119a 100644 --- a/plugins/commands/box/command/repackage.rb +++ b/plugins/commands/box/command/repackage.rb @@ -14,7 +14,7 @@ module VagrantPlugins # Parse the options argv = parse_options(opts) return if !argv - raise Vagrant::Errors::CLIInvalidUsage, :help => opts.help.chomp if argv.length < 2 + raise Vagrant::Errors::CLIInvalidUsage, :help => opts.help.chomp if argv.length != 2 box_name = argv[0] box_provider = argv[1].to_sym diff --git a/plugins/commands/box/command/root.rb b/plugins/commands/box/command/root.rb index 4150fd659..138a10de9 100644 --- a/plugins/commands/box/command/root.rb +++ b/plugins/commands/box/command/root.rb @@ -24,6 +24,11 @@ module VagrantPlugins List end + @subcommands.register(:outdated) do + require_relative "outdated" + Outdated + end + @subcommands.register(:remove) do require File.expand_path("../remove", __FILE__) Remove @@ -33,6 +38,11 @@ module VagrantPlugins require File.expand_path("../repackage", __FILE__) Repackage end + + @subcommands.register(:update) do + require_relative "update" + Update + end end def execute diff --git a/plugins/commands/box/command/update.rb b/plugins/commands/box/command/update.rb new file mode 100644 index 000000000..5a204647c --- /dev/null +++ b/plugins/commands/box/command/update.rb @@ -0,0 +1,125 @@ +require 'optparse' + +module VagrantPlugins + module CommandBox + module Command + class Update < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant box update [options]" + o.separator "" + o.separator "Updates the box that is in use in the current Vagrant environment," + o.separator "if there any updates available. This does not destroy/recreate the" + o.separator "machine, so you'll have to do that to see changes." + o.separator "" + o.separator "To update a specific box (not tied to a Vagrant environment), use the" + o.separator "--box flag." + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--box VALUE", String, "Update a specific box") do |b| + options[:box] = b + end + + o.on("--provider VALUE", String, "Update box with specific provider.") do |p| + options[:provider] = p.to_sym + end + end + + argv = parse_options(opts) + return if !argv + + if options[:box] + update_specific(options[:box], options[:provider]) + else + update_vms(argv) + end + + 0 + end + + def update_specific(name, provider) + boxes = {} + @env.boxes.all.each do |n, v, p| + boxes[n] ||= {} + boxes[n][p] ||= [] + boxes[n][p] << v + end + + if !boxes[name] + raise Vagrant::Errors::BoxNotFound, name: name.to_s + end + + if !provider + if boxes[name].length > 1 + raise Vagrant::Errors::BoxUpdateMultiProvider, + name: name.to_s, + providers: boxes[name].keys.map(&:to_s).sort.join(", ") + end + + provider = boxes[name].keys.first + elsif !boxes[name][provider] + raise Vagrant::Errors::BoxNotFoundWithProvider, + name: name.to_s, + provider: provider.to_s, + providers: boxes[name].keys.map(&:to_s).sort.join(", ") + end + + to_update = [ + [name, provider, boxes[name][provider].last], + ] + + to_update.each do |n, p, v| + box = @env.boxes.find(n, p, v) + box_update(box, "> #{v}", @env.ui) + end + end + + def update_vms(argv) + with_target_vms(argv) do |machine| + if !machine.box + machine.ui.output(I18n.t( + "vagrant.errors.box_update_no_box", + name: machine.config.vm.box)) + next + end + + box = machine.box + version = machine.config.vm.box_version + box_update(box, version, machine.ui) + end + end + + def box_update(box, version, ui) + ui.output(I18n.t("vagrant.box_update_checking", name: box.name)) + ui.detail("Version constraints: #{version}") + ui.detail("Provider: #{box.provider}") + + update = box.has_update?(version) + if !update + ui.success(I18n.t( + "vagrant.box_up_to_date_single", + name: box.name, version: box.version)) + return + end + + ui.output(I18n.t( + "vagrant.box_updating", + name: update[0].name, + provider: update[2].name, + old: box.version, + new: update[1].version)) + @env.action_runner.run(Vagrant::Action.action_box_add, { + box_url: box.metadata_url, + box_provider: update[2].name, + box_version: update[1].version, + ui: ui, + }) + end + end + end + end +end diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 1e92f68f3..4b35495aa 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -17,7 +17,9 @@ module VagrantPlugins attr_accessor :base_mac attr_accessor :boot_timeout attr_accessor :box + attr_accessor :box_check_update attr_accessor :box_url + attr_accessor :box_version attr_accessor :box_download_ca_cert attr_accessor :box_download_checksum attr_accessor :box_download_checksum_type @@ -32,12 +34,14 @@ module VagrantPlugins def initialize @base_mac = UNSET_VALUE @boot_timeout = UNSET_VALUE + @box_check_update = UNSET_VALUE @box_download_ca_cert = UNSET_VALUE @box_download_checksum = UNSET_VALUE @box_download_checksum_type = UNSET_VALUE @box_download_client_cert = UNSET_VALUE @box_download_insecure = UNSET_VALUE @box_url = UNSET_VALUE + @box_version = UNSET_VALUE @graceful_halt_timeout = UNSET_VALUE @guest = UNSET_VALUE @hostname = UNSET_VALUE @@ -303,12 +307,14 @@ module VagrantPlugins # Defaults @base_mac = nil if @base_mac == UNSET_VALUE @boot_timeout = 300 if @boot_timeout == UNSET_VALUE + @box_check_update = true if @box_check_update == UNSET_VALUE @box_download_ca_cert = nil if @box_download_ca_cert == UNSET_VALUE @box_download_checksum = nil if @box_download_checksum == UNSET_VALUE @box_download_checksum_type = nil if @box_download_checksum_type == UNSET_VALUE @box_download_client_cert = nil if @box_download_client_cert == UNSET_VALUE @box_download_insecure = false if @box_download_insecure == UNSET_VALUE @box_url = nil if @box_url == UNSET_VALUE + @box_version = ">= 0" if @box_version == UNSET_VALUE @graceful_halt_timeout = 60 if @graceful_halt_timeout == UNSET_VALUE @guest = nil if @guest == UNSET_VALUE @hostname = nil if @hostname == UNSET_VALUE @@ -326,9 +332,7 @@ module VagrantPlugins end # Make sure the box URL is an array if it is set - if @box_url && !@box_url.is_a?(Array) - @box_url = [@box_url] - end + @box_url = Array(@box_url) if @box_url # Set the guest properly @guest = @guest.to_sym if @guest @@ -449,11 +453,20 @@ module VagrantPlugins def validate(machine) errors = _detected_errors errors << I18n.t("vagrant.config.vm.box_missing") if !box - errors << I18n.t("vagrant.config.vm.box_not_found", :name => box) if \ - box && !box_url && !machine.box errors << I18n.t("vagrant.config.vm.hostname_invalid_characters") if \ @hostname && @hostname !~ /^[a-z0-9][-.a-z0-9]+$/i + if @box_version + @box_version.split(",").each do |v| + begin + Gem::Requirement.new(v.strip) + rescue Gem::Requirement::BadRequirementError + errors << I18n.t( + "vagrant.config.vm.bad_version", version: v) + end + end + end + if box_download_ca_cert path = Pathname.new(box_download_ca_cert). expand_path(machine.env.root_path) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index a0297cb83..5831d9b9d 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -246,6 +246,7 @@ module VagrantPlugins Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use ConfigValidate + b.use BoxCheckOutdated b.use Call, IsRunning do |env, b2| # If the VM is running, then our work here is done, exit if env[:result] @@ -302,7 +303,7 @@ module VagrantPlugins # works fine. b.use Call, Created do |env, b2| if !env[:result] - b2.use HandleBoxUrl + b2.use HandleBox end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 5c8fef2c6..5438dab66 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -4,6 +4,59 @@ en: Machine booted and ready! boot_waiting: |- Waiting for machine to boot. This may take a few minutes... + box_auto_adding: |- + Box '%{name}' could not be found. Attempting to find and install... + box_add_choose_provider: |- + This box can work with multiple providers! The providers that it + can work with are listed below. Please review the list and choose + the provider you will be working with. + + %{options} + + Enter your choice: + box_add_choose_provider_again: |- + Invalid choice. Try again: + box_add_with_version: |- + Adding box '%{name}' (v%{version}) for provider: %{providers} + box_added: |- + Successfully added box '%{name}' (v%{version}) for '%{provider}'! + box_downloading: |- + Downloading: %{url} + box_download_error: |- + Error downloading: %{message} + box_expanding_url: |- + URL: %{url} + box_loading_metadata: |- + Loading metadata for box '%{name}' + box_outdated: |- + * '%{name}' is outdated! Current: %{current}. Latest: %{latest} + box_outdated_checking_with_refresh: |- + Checking if box '%{name}' is up to date... + box_outdated_local: |- + A newer version of the box '%{name}' is available and already + installed, but your Vagrant machine is running against + version '%{old}'. To update to version '%{new}', + destroy and recreate your machine. + box_outdated_metadata_error_single: |- + Error loading box metadata while attempting to check for + updates: %{message} + box_outdated_single: |- + A newer version of the box '%{name}' is available! You currently + have version '%{current}'. The latest is version '%{latest}'. Run + `vagrant box update` to update. + box_outdated_metadata_error: |- + * '%{name}': Error loading metadata: %{message} + box_outdated_no_metadata: |- + * '%{name}' wasn't added from a catalog, no version information + box_updating: |- + Updating '%{name}' with provider '%{provider}' from version + '%{old}' to '%{new}'... + box_update_checking: |- + Checking for updates to '%{name}' + box_up_to_date: |- + * '%{name}' (v%{version}) is up to date + box_up_to_date_single: |- + Box '%{name}' (v%{version}) is running the latest version. cfengine_bootstrapping: |- Bootstrapping CFEngine with policy server: %{policy_server}... cfengine_bootstrapping_policy_hub: |- @@ -211,6 +264,32 @@ en: Any errors that occurred are shown below. %{message} + box_add_no_matching_provider: |- + The box you're attempting to add doesn't support the provider + you requested. Please find an alternate box or use an alternate + provider. Double-check your requested provider to verify you didn't + simply misspell it. + + Name: %{name} + Address: %{url} + Requested provider: %{requested} + box_add_no_matching_version: |- + The box you're attempting to add has no available version that + matches the constraints you requested. Please double-check your + settings. + + Box: %{name} + Address: %{url} + Constraints: %{constraints} + Available versions: %{versions} + box_add_short_not_found: |- + The box '%{name}' could not be found or + could not be accessed in the remote catalog. Please + double-check the name. The expanded URL and error message + are shown below. + + URL: %{url} + Error: %{error} boot_bad_state: |- The guest machine entered an invalid state while waiting for it to boot. Valid states are '%{valid}'. The machine is in the @@ -236,6 +315,33 @@ en: If the box appears to be booting properly, you may want to increase the timeout ("config.vm.boot_timeout") value. + box_add_exists: |- + The box you're attempting to add already exists. Remove it before + adding it again or add it with the `--force` flag. + + Name: %{name} + Provider: %{provider} + Version: %{version} + box_add_metadata_multi_url: |- + Multiple URLs for a box can't be specified when adding + versioned boxes. Please specify a single URL to the box + metadata (JSON) information. The full list of URLs you + specified is shown below: + + %{urls} + box_add_name_mismatch: |- + The box you're adding has a name different from the name you + requested. For boxes with metadata, you cannot override the name. + If you're adding a box using `vagrant box add`, don't specify + the `--name` parameter. If the box is being added via a Vagrantfile, + change the `config.vm.box` value to match the name below. + + Requested name: %{requested_name} + Actual name: %{actual_name} + box_add_name_required: |- + A name is required when adding a box file directly. Please pass + the `--name` parameter to `vagrant box add`. See + `vagrant box add -h` for more help. box_checksum_invalid_type: |- The specified checksum type is not supported by Vagrant: %{type}. Vagrant supports the following checksum types: @@ -266,16 +372,75 @@ en: box file format can be found at the URL below: http://docs.vagrantup.com/v2/boxes/format.html - box_not_found: Box '%{name}' with '%{provider}' provider could not be found. + box_metadata_malformed: |- + The metadata for the box was malformed. The exact error + is shown below. Please contact the maintainer of the box so + that this issue can be fixed. + + %{error} + box_not_found: |- + The box '%{name}' does not exist. Please double check and + try again. You can see the boxes that are installed with + `vagrant box list`. + box_not_found_with_provider: |- + The box '%{name}' isn't installed for the provider '%{provider}'. + Please double-check and try again. The installed providers for + the box are shown below: + + %{providers} + box_outdated_no_box: |- + The box '%{name}' isn't downloaded or added yet, so we can't + check if it is outdated. Run a `vagrant up` or add the box + with `vagrant box add` to download an appropriate version. box_provider_doesnt_match: |- The box you attempted to add doesn't match the provider you specified. Provider expected: %{expected} Provider of box: %{actual} - box_upgrade_required: |- - The box '%{name}' is still stored on disk in the Vagrant 1.0.x - format. This box must be upgraded in order to work properly with - this version of Vagrant. + box_remove_multi_provider: |- + You requested to remove the box '%{name}'. This box has + multiple providers. You must explicitly select a single + provider to remove with `--provider`. + + Available providers: %{providers} + box_remove_multi_version: |- + You requested to remove the box '%{name}' with provider + '%{provider}'. This box has multiple versions. You must + explicitly specify which version you want to remove with + the `--box-version` flag. + + Versions: %{versions} + box_remove_not_found: |- + The box you requested to be removed could not be found. No + boxes named '%{name}' could be found. + box_remove_provider_not_found: |- + You requested to remove the box '%{name}' with provider + '%{provider}'. The box '%{name}' exists but not with + the provider specified. Please double-check and try again. + + The providers for this are: %{providers} + box_server_not_set: |- + A URL to a Vagrant Cloud server is not set, so boxes cannot + be added with a shorthand ("mitchellh/precise64") format. + You may also be seeing this error if you meant to type in + a path to a box file which doesn't exist locally on your + system. + + To set a URL to a Vagrant Cloud server, set the + `VAGRANT_SERVER_URL` environmental variable. Or, if you + meant to use a file path, make sure the path to the file + is valid. + box_update_multi_provider: |- + You requested to update the box '%{name}'. This box has + multiple providers. You must explicitly select a single + provider to remove with `--provider`. + + Available providers: %{providers} + box_update_no_metadata: |- + The box '%{name}' is not a versioned box. The box was added + directly instead of from a box catalog. Vagrant can only + check the versions of boxes that were added from a catalog + such as from the public Vagrant Server. bundler_disabled: |- Vagrant's built-in bundler management mechanism is disabled because Vagrant is running in an external bundler environment. In these @@ -432,11 +597,20 @@ en: as mounting shared folders and configuring networks. Please add the ability to detect this guest operating system to Vagrant by creating a plugin or reporting a bug. + home_dir_later_version: |- + It appears that a newer version of Vagrant was run on this machine + at some point. The current version of Vagrant is unable to read + the configuration structure of this newer version. Please upgrade to + the latest version of Vagrant. home_dir_not_accessible: |- The home directory you specified is not accessible. The home directory that Vagrant uses must be both readable and writable. You specified: %{home_path} + home_dir_unknown_version: |- + The Vagrant app data directory (%{path}) is in a + structure Vagrant doesn't understand. This is a rare exception. + Please report an issue or ask the mailing list for help. host_explicit_not_detected: |- The host implementation explicitly specified in your Vagrantfile ("%{value}") could not be found. Please verify that the plugin is @@ -859,6 +1033,8 @@ en: ssh: private_key_missing: "`private_key_path` file must exist: %{path}" vm: + bad_version: |- + Invalid box version constraints: %{version} box_download_ca_cert_not_found: |- "box_download_ca_cert" file not found: %{path} box_download_checksum_blank: |- @@ -866,7 +1042,6 @@ en: box_download_checksum_notblank: |- Checksum specified but must also specify "box_download_checksum_type" box_missing: "A box must be specified." - box_not_found: "The box '%{name}' could not be found." hostname_invalid_characters: |- The hostname set for the VM should only contain letters, numbers, hyphens or dots. It cannot start with a hyphen or dot. @@ -916,17 +1091,6 @@ en: vm_not_created: "VM not created. Moving on..." vm_not_running: "VM is not currently running. Please, first bring it up with `vagrant up` then run this command." box: - remove_must_specify_provider: |- - Multiple providers were found for the box '%{name}'. Please specify - the specific provider for the box you want to remove. The list of - providers backing this box is: - - '%{providers}' - - To remove the box for a specific provider, run the following command, - filling in PROVIDER with one of the providers above: - - vagrant box remove '%{name}' PROVIDER no_installed_boxes: "There are no installed boxes! Use `vagrant box add` to add some." removing: |- Removing box '%{name}' with provider '%{provider}'... @@ -1217,23 +1381,15 @@ en: output from attempting to unpackage (if any): %{output} - already_exists: |- - The box you're attempting to add already exists: - - Name: %{name} - Provider: %{formats} add: adding: |- Extracting box... - added: |- - Successfully added box '%{name}' with provider '%{provider}'! checksumming: |- Calculating and comparing box checksum... destroy: destroying: "Deleting box '%{name}'..." download: cleaning: "Cleaning up downloaded box..." - downloading: "Downloading box from URL: %{url}" download_failed: |- Download failed. Will try another box URL if there is one. interrupted: "Box download was interrupted. Exiting." diff --git a/test/unit/plugins/commands/box/command/add_test.rb b/test/unit/plugins/commands/box/command/add_test.rb new file mode 100644 index 000000000..23b109974 --- /dev/null +++ b/test/unit/plugins/commands/box/command/add_test.rb @@ -0,0 +1,67 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/box/command/add") + +describe VagrantPlugins::CommandBox::Command::Add do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + before do + iso_env.stub(action_runner: action_runner) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with one argument" do + let(:argv) { ["foo"] } + + it "executes the runner with the proper actions" do + action_runner.should_receive(:run).with do |action, **opts| + expect(opts[:box_name]).to be_nil + expect(opts[:box_url]).to eq("foo") + true + end + + subject.execute + end + end + + context "with two arguments" do + let(:argv) { ["foo", "bar"] } + + it "executes the runner with the proper actions" do + action_runner.should_receive(:run).with do |action, **opts| + expect(opts[:box_name]).to eq("foo") + expect(opts[:box_url]).to eq("bar") + true + end + + subject.execute + end + end + + context "with more than two arguments" do + let(:argv) { ["one", "two", "three"] } + + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end +end diff --git a/test/unit/plugins/commands/box/command/remove_test.rb b/test/unit/plugins/commands/box/command/remove_test.rb new file mode 100644 index 000000000..a13fd30fb --- /dev/null +++ b/test/unit/plugins/commands/box/command/remove_test.rb @@ -0,0 +1,66 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/box/command/remove") + +describe VagrantPlugins::CommandBox::Command::Remove do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + before do + iso_env.stub(action_runner: action_runner) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with one argument" do + let(:argv) { ["foo"] } + + it "invokes the action runner" do + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_name]).to eq("foo") + true + end + + subject.execute + end + end + + context "with two arguments" do + let(:argv) { ["foo", "bar"] } + + it "uses the 2nd arg as a provider" do + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_name]).to eq("foo") + expect(opts[:box_provider]).to eq("bar") + true + end + + subject.execute + end + end + + context "with more than two arguments" do + let(:argv) { ["one", "two", "three"] } + + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end +end diff --git a/test/unit/plugins/commands/box/command/repackage_test.rb b/test/unit/plugins/commands/box/command/repackage_test.rb new file mode 100644 index 000000000..fd27c78a1 --- /dev/null +++ b/test/unit/plugins/commands/box/command/repackage_test.rb @@ -0,0 +1,54 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/box/command/repackage") + +describe VagrantPlugins::CommandBox::Command::Repackage do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + before do + iso_env.stub(action_runner: action_runner) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with one argument" do + let(:argv) { ["one"] } + + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with two arguments" do + it "repackages the box with the given provider" do + pending + end + end + + context "with more than two arguments" do + let(:argv) { ["one", "two", "three"] } + + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end +end diff --git a/test/unit/plugins/commands/box/command/update_test.rb b/test/unit/plugins/commands/box/command/update_test.rb new file mode 100644 index 000000000..8571d6ba3 --- /dev/null +++ b/test/unit/plugins/commands/box/command/update_test.rb @@ -0,0 +1,217 @@ +require "pathname" +require "tmpdir" + +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/box/command/update") + +describe VagrantPlugins::CommandBox::Command::Update do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + test_iso_env.vagrantfile("") + test_iso_env.create_vagrant_env + end + let(:test_iso_env) { isolated_environment } + + let(:action_runner) { double("action_runner") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + subject { described_class.new(argv, iso_env) } + + before do + iso_env.stub(action_runner: action_runner) + end + + describe "execute" do + context "updating specific box" do + let(:argv) { ["--box", "foo"] } + + let(:metadata_url) { Pathname.new(Dir.mktmpdir).join("metadata.json") } + + before do + metadata_url.open("w") do |f| + f.write("") + end + + test_iso_env.box3( + "foo", "1.0", :virtualbox, metadata_url: metadata_url.to_s) + end + + it "doesn't update if they're up to date" do + action_runner.should_receive(:run).never + + subject.execute + end + + it "does update if there is an update" do + metadata_url.open("w") do |f| + f.write(<<-RAW) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW + end + + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_url]).to eq(metadata_url.to_s) + expect(opts[:box_provider]).to eq("virtualbox") + expect(opts[:box_version]).to eq("1.1") + true + end + + subject.execute + end + + it "raises an error if there are multiple providers" do + test_iso_env.box3("foo", "1.0", :vmware) + + action_runner.should_receive(:run).never + + expect { subject.execute }. + to raise_error(Vagrant::Errors::BoxUpdateMultiProvider) + end + + context "with multiple providers and specifying the provider" do + let(:argv) { ["--box", "foo", "--provider", "vmware"] } + + it "updates the proper box" do + metadata_url.open("w") do |f| + f.write(<<-RAW) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "vmware", + "url": "bar" + } + ] + } + ] + } + RAW + end + + test_iso_env.box3("foo", "1.0", :vmware) + + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_url]).to eq(metadata_url.to_s) + expect(opts[:box_provider]).to eq("vmware") + expect(opts[:box_version]).to eq("1.1") + true + end + + subject.execute + end + + it "raises an error if that provider doesn't exist" do + action_runner.should_receive(:run).never + + expect { subject.execute }. + to raise_error(Vagrant::Errors::BoxNotFoundWithProvider) + end + end + + context "with a box that doesn't exist" do + let(:argv) { ["--box", "nope"] } + + it "raises an exception" do + action_runner.should_receive(:run).never + + expect { subject.execute }. + to raise_error(Vagrant::Errors::BoxNotFound) + end + end + end + + context "updating environment machines" do + before do + subject.stub(:with_target_vms) { |&block| block.call machine } + end + + let(:box) do + box_dir = test_iso_env.box3("foo", "1.0", :virtualbox) + box = Vagrant::Box.new( + "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo") + box.stub(has_update?: nil) + box + end + + it "ignores machines without boxes" do + action_runner.should_receive(:run).never + + subject.execute + end + + it "doesn't update boxes if they're up-to-date" do + machine.stub(box: box) + box.should_receive(:has_update?). + with(machine.config.vm.box_version). + and_return(nil) + + action_runner.should_receive(:run).never + + subject.execute + end + + it "updates boxes if they have an update" do + md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW + + machine.stub(box: box) + box.should_receive(:has_update?). + with(machine.config.vm.box_version). + and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) + + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_url]).to eq(box.metadata_url) + expect(opts[:box_provider]).to eq("virtualbox") + expect(opts[:box_version]).to eq("1.1") + expect(opts[:ui]).to equal(machine.ui) + true + end + + subject.execute + end + end + end +end diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index 99afefdab..f12d94f22 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -5,6 +5,29 @@ require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe VagrantPlugins::Kernel_V2::VMConfig do subject { described_class.new } + let(:machine) { double("machine") } + + def assert_valid + errors = subject.validate(machine) + if !errors.values.all? { |v| v.empty? } + raise "Errors: #{errors.inspect}" + end + end + + before do + env = double("env") + env.stub(root_path: nil) + machine.stub(env: env) + machine.stub(provider_config: nil) + + subject.box = "foo" + end + + it "is valid with test defaults" do + subject.finalize! + assert_valid + end + describe "#base_mac" do it "defaults properly" do subject.finalize! @@ -12,11 +35,58 @@ describe VagrantPlugins::Kernel_V2::VMConfig do end end - describe "#box_url" do - it "defaults properly" do + context "#box_check_update" do + it "defaults to true" do subject.finalize! + + expect(subject.box_check_update).to be_true + end + end + + describe "#box_url" do + it "defaults to nil" do + subject.finalize! + expect(subject.box_url).to be_nil end + + it "turns into an array" do + subject.box_url = "foo" + subject.finalize! + + expect(subject.box_url).to eq( + ["foo"]) + end + + it "keeps in array" do + subject.box_url = ["foo", "bar"] + subject.finalize! + + expect(subject.box_url).to eq( + ["foo", "bar"]) + end + end + + context "#box_version" do + it "defaults to >= 0" do + subject.finalize! + + expect(subject.box_version).to eq(">= 0") + end + + it "errors if invalid version" do + subject.box_version = "nope" + subject.finalize! + + expect { assert_valid }.to raise_error(RuntimeError) + end + + it "can have complex constraints" do + subject.box_version = ">= 0, ~> 1.0" + subject.finalize! + + assert_valid + end end describe "#network(s)" do diff --git a/test/unit/support/isolated_environment.rb b/test/unit/support/isolated_environment.rb index 4f8e7552b..94fb907bd 100644 --- a/test/unit/support/isolated_environment.rb +++ b/test/unit/support/isolated_environment.rb @@ -89,6 +89,44 @@ module Unit box_dir end + # Creates a fake box to exist in this environment according + # to the "gen-3" box format. + # + # @param [String] name + # @param [String] version + # @param [String] provider + # @return [Pathname] + def box3(name, version, provider, **opts) + # Create the directory for the box + box_dir = boxes_dir.join(name, version, provider.to_s) + box_dir.mkpath + + # Create the metadata.json for it + box_metadata_file = box_dir.join("metadata.json") + box_metadata_file.open("w") do |f| + f.write(JSON.generate({ + :provider => provider.to_s + })) + end + + # Create a Vagrantfile + if opts[:vagrantfile] + box_vagrantfile = box_dir.join("Vagrantfile") + box_vagrantfile.open("w") do |f| + f.write(opts[:vagrantfile]) + end + end + + # Create the metadata URL + if opts[:metadata_url] + boxes_dir.join(name, "metadata_url").open("w") do |f| + f.write(opts[:metadata_url]) + end + end + + box_dir + end + # This creates a "box" file that is a valid V1 box. # # @return [Pathname] Path to the newly created box. diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb new file mode 100644 index 000000000..b2b31b403 --- /dev/null +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -0,0 +1,826 @@ +require "digest/sha1" +require "pathname" +require "tempfile" +require "tmpdir" +require "webrick" + +require File.expand_path("../../../../base", __FILE__) + +require "vagrant/util/file_checksum" + +describe Vagrant::Action::Builtin::BoxAdd do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + box_collection: box_collection, + tmp_path: Pathname.new(Dir.mktmpdir), + ui: Vagrant::UI::Silent.new, + } } + + subject { described_class.new(app, env) } + + let(:box_collection) { double("box_collection") } + let(:iso_env) { isolated_environment } + + let(:box) do + box_dir = iso_env.box3("foo", "1.0", :virtualbox) + Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) + end + + # Helper to quickly SHA1 checksum a path + def checksum(path) + FileChecksum.new(path, Digest::SHA1).checksum + end + + def with_web_server(path) + tf = Tempfile.new("vagrant") + tf.close + + mime_types = WEBrick::HTTPUtils::DefaultMimeTypes + mime_types.store "json", "application/json" + + port = 3838 + server = WEBrick::HTTPServer.new( + AccessLog: [], + Logger: WEBrick::Log.new(tf.path, 7), + Port: port, + DocumentRoot: path.dirname.to_s, + MimeTypes: mime_types) + thr = Thread.new { server.start } + yield port + ensure + server.shutdown rescue nil + thr.join rescue nil + end + + before do + box_collection.stub(find: nil) + end + + context "with box file directly" do + it "adds it" do + box_path = iso_env.box2_file(:virtualbox) + + env[:box_name] = "foo" + env[:box_url] = box_path.to_s + + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo") + expect(version).to eq("0") + expect(opts[:metadata_url]).to be_nil + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + end + + it "adds from multiple URLs" do + box_path = iso_env.box2_file(:virtualbox) + + env[:box_name] = "foo" + env[:box_url] = [ + "/foo/bar/baz", + box_path.to_s, + ] + + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo") + expect(version).to eq("0") + expect(opts[:metadata_url]).to be_nil + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + end + + it "adds from HTTP URL" do + box_path = iso_env.box2_file(:virtualbox) + with_web_server(box_path) do |port| + env[:box_name] = "foo" + env[:box_url] = "http://127.0.0.1:#{port}/#{box_path.basename}" + + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo") + expect(version).to eq("0") + expect(opts[:metadata_url]).to be_nil + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + end + end + + it "raises an error if no name is given" do + box_path = iso_env.box2_file(:virtualbox) + + env[:box_url] = box_path.to_s + + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAddNameRequired) + end + + it "raises an error if the box already exists" do + box_path = iso_env.box2_file(:virtualbox) + + env[:box_name] = "foo" + env[:box_url] = box_path.to_s + env[:box_provider] = "virtualbox" + + box_collection.should_receive(:find).with( + "foo", ["virtualbox"], "0").and_return(box) + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAlreadyExists) + end + + it "force adds if exists and specified" do + box_path = iso_env.box2_file(:virtualbox) + + env[:box_force] = true + env[:box_name] = "foo" + env[:box_url] = box_path.to_s + env[:box_provider] = "virtualbox" + + box_collection.stub(find: box) + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo") + expect(version).to eq("0") + expect(opts[:metadata_url]).to be_nil + true + end.and_return(box) + app.should_receive(:call).with(env).once + + subject.call(env) + end + end + + context "with box metadata" do + it "adds from HTTP URL" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant", ".json"]).tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + md_path = Pathname.new(tf.path) + with_web_server(md_path) do |port| + env[:box_url] = "http://127.0.0.1:#{port}/#{md_path.basename}" + + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(checksum(path)).to eq(checksum(box_path)) + expect(opts[:metadata_url]).to eq(env[:box_url]) + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + end + end + + it "adds from shorthand path" do + box_path = iso_env.box2_file(:virtualbox) + td = Pathname.new(Dir.mktmpdir) + tf = td.join("mitchellh", "precise64.json") + tf.dirname.mkpath + tf.open("w") do |f| + f.write(<<-RAW) + { + "name": "mitchellh/precise64", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + end + + with_web_server(tf.dirname) do |port| + url = "http://127.0.0.1:#{port}" + env[:box_url] = "mitchellh/precise64.json" + + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(name).to eq("mitchellh/precise64") + expect(version).to eq("0.7") + expect(checksum(path)).to eq(checksum(box_path)) + expect(opts[:metadata_url]).to eq( + "#{url}/#{env[:box_url]}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + with_temp_env("VAGRANT_SERVER_URL" => url) do + subject.call(env) + end + end + end + + it "raises an error if no Vagrant server is set" do + tf = Tempfile.new("foo") + tf.close + + env[:box_url] = "mitchellh/precise64.json" + + box_collection.should_receive(:add).never + app.should_receive(:call).never + + Vagrant.stub(server_url: nil) + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxServerNotSet) + end + + it "raises an error if shorthand is invalid" do + tf = Tempfile.new("foo") + tf.close + + with_web_server(Pathname.new(tf.path)) do |port| + env[:box_url] = "mitchellh/precise64.json" + + box_collection.should_receive(:add).never + app.should_receive(:call).never + + url = "http://127.0.0.1:#{port}" + with_temp_env("VAGRANT_SERVER_URL" => url) do + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAddShortNotFound) + end + end + end + + it "raises an error if multiple metadata URLs are given" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = [ + "/foo/bar/baz", + tf.path, + ] + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAddMetadataMultiURL) + end + + it "adds the latest version of a box with only one provider" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + end + + it "adds the latest version of a box with the specified provider" do + box_path = iso_env.box2_file(:vmware) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + }, + { + "name": "vmware", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_provider] = "vmware" + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + + expect(env[:box_added]).to equal(box) + end + + it "adds the latest version of a box with the specified provider, even if not latest" do + box_path = iso_env.box2_file(:vmware) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + }, + { + "name": "vmware", + "url": "#{box_path}" + } + ] + }, + { + "version": "1.5" + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_provider] = "vmware" + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + + expect(env[:box_added]).to equal(box) + end + + it "adds the constrained version of a box with the only provider" do + box_path = iso_env.box2_file(:vmware) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5", + "providers": [ + { + "name": "vmware", + "url": "#{box_path}" + } + ] + }, + { "version": "1.1" } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_version] = "~> 0.1" + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.5") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + + expect(env[:box_added]).to equal(box) + end + + it "adds the constrained version of a box with the specified provider" do + box_path = iso_env.box2_file(:vmware) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5", + "providers": [ + { + "name": "vmware", + "url": "#{box_path}" + }, + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + } + ] + }, + { "version": "1.1" } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_provider] = "vmware" + env[:box_version] = "~> 0.1" + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.5") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + + expect(env[:box_added]).to equal(box) + end + + it "adds the latest version of a box with any specified provider" do + box_path = iso_env.box2_file(:vmware) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5", + "providers": [ + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + } + ] + }, + { + "version": "0.7", + "providers": [ + { + "name": "vmware", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_provider] = ["virtualbox", "vmware"] + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + + expect(env[:box_added]).to equal(box) + end + + it "asks the user what provider if multiple options" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + }, + { + "name": "vmware", + "url": "#{iso_env.box2_file(:vmware)}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + + env[:ui].should_receive(:ask).and_return("1") + + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + end + + it "raises an exception if the name doesn't match a requested name" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_name] = "foo" + env[:box_url] = tf.path + + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAddNameMismatch) + end + + it "raises an exception if no matching version" do + box_path = iso_env.box2_file(:vmware) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5", + "providers": [ + { + "name": "vmware", + "url": "#{box_path}" + } + ] + }, + { "version": "1.1" } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_version] = "~> 2.0" + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAddNoMatchingVersion) + end + + it "raises an error if there is no matching provider" do + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + env[:box_provider] = "vmware" + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAddNoMatchingProvider) + end + + it "raises an error if a box already exists" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_url] = tf.path + box_collection.should_receive(:find). + with("foo/bar", "virtualbox", "0.7").and_return(box) + box_collection.should_receive(:add).never + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxAlreadyExists) + end + + it "force adds a box if specified" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + f.close + end + + env[:box_force] = true + env[:box_url] = tf.path + box_collection.stub(find: box) + box_collection.should_receive(:add).with do |path, name, version, **opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:force]).to be_true + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + true + end.and_return(box) + + app.should_receive(:call).with(env) + + subject.call(env) + + expect(env[:box_added]).to equal(box) + end + end +end diff --git a/test/unit/vagrant/action/builtin/box_check_outdated_test.rb b/test/unit/vagrant/action/builtin/box_check_outdated_test.rb new file mode 100644 index 000000000..32dcdac0e --- /dev/null +++ b/test/unit/vagrant/action/builtin/box_check_outdated_test.rb @@ -0,0 +1,146 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::BoxCheckOutdated do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + box_collection: iso_vagrant_env.boxes, + machine: machine, + ui: Vagrant::UI::Silent.new, + } } + + subject { described_class.new(app, env) } + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + isolated_environment.tap do |env| + env.vagrantfile("") + end + end + + let(:iso_vagrant_env) { iso_env.create_vagrant_env } + + let(:box) do + box_dir = iso_env.box3("foo", "1.0", :virtualbox) + Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir).tap do |b| + b.stub(has_update?: nil) + end + end + + let(:machine) do + m = iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy) + m.config.vm.box_check_update = true + m + end + + before do + machine.stub(box: box) + end + + context "disabling outdated checking" do + it "doesn't check" do + machine.config.vm.box_check_update = false + + app.should_receive(:call).with(env).once + + subject.call(env) + + expect(env).to_not have_key(:box_outdated) + end + + it "checks if forced" do + machine.config.vm.box_check_update = false + env[:box_outdated_force] = true + + app.should_receive(:call).with(env).once + + subject.call(env) + + expect(env).to have_key(:box_outdated) + end + end + + context "no box" do + it "raises an exception if the machine doesn't have a box yet" do + machine.stub(box: nil) + + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxOutdatedNoBox) + end + end + + context "with a non-versioned box" do + it "does nothing" do + box.stub(metadata_url: nil) + box.stub(version: "0") + + app.should_receive(:call).once + box.should_receive(:has_update?).never + + subject.call(env) + end + end + + context "with a box" do + it "sets env if no update" do + box.should_receive(:has_update?).and_return(nil) + + app.should_receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_outdated]).to be_false + end + + it "sets env if there is an update" do + md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW + + box.should_receive(:has_update?).with(machine.config.vm.box_version). + and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) + + app.should_receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_outdated]).to be_true + end + + it "raises error if has_update? errors" do + box.should_receive(:has_update?).and_raise(Vagrant::Errors::VagrantError) + + app.should_receive(:call).never + + expect { subject.call(env) }.to raise_error(Vagrant::Errors::VagrantError) + end + + it "doesn't raise an error if ignore errors is on" do + env[:box_outdated_ignore_errors] = true + + box.should_receive(:has_update?).and_raise(Vagrant::Errors::VagrantError) + app.should_receive(:call).with(env).once + + expect { subject.call(env) }.to_not raise_error + end + end +end diff --git a/test/unit/vagrant/action/builtin/box_remove_test.rb b/test/unit/vagrant/action/builtin/box_remove_test.rb new file mode 100644 index 000000000..19a15b3f3 --- /dev/null +++ b/test/unit/vagrant/action/builtin/box_remove_test.rb @@ -0,0 +1,108 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::BoxRemove do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + box_collection: box_collection, + ui: Vagrant::UI::Silent.new, + } } + + subject { described_class.new(app, env) } + + let(:box_collection) { double("box_collection") } + let(:iso_env) { isolated_environment } + + let(:box) do + box_dir = iso_env.box3("foo", "1.0", :virtualbox) + Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) + end + + it "deletes the box if it is the only option" do + box_collection.stub(all: [["foo", "1.0", :virtualbox]]) + + env[:box_name] = "foo" + + box_collection.should_receive(:find).with( + "foo", :virtualbox, "1.0").and_return(box) + box.should_receive(:destroy!).once + app.should_receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_removed]).to equal(box) + end + + it "deletes the box with the specified provider if given" do + box_collection.stub( + all: [ + ["foo", "1.0", :virtualbox], + ["foo", "1.0", :vmware], + ]) + + env[:box_name] = "foo" + env[:box_provider] = "virtualbox" + + box_collection.should_receive(:find).with( + "foo", :virtualbox, "1.0").and_return(box) + box.should_receive(:destroy!).once + app.should_receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_removed]).to equal(box) + end + + it "errors if the box doesn't exist" do + box_collection.stub(all: []) + + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxRemoveNotFound) + end + + it "errors if the specified provider doesn't exist" do + env[:box_name] = "foo" + env[:box_provider] = "bar" + + box_collection.stub(all: [["foo", "1.0", :virtualbox]]) + + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxRemoveProviderNotFound) + end + + it "errors if there are multiple providers" do + env[:box_name] = "foo" + + box_collection.stub( + all: [ + ["foo", "1.0", :virtualbox], + ["foo", "1.0", :vmware], + ]) + + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxRemoveMultiProvider) + end + + it "errors if the specified provider has multiple versions" do + env[:box_name] = "foo" + env[:box_provider] = "virtualbox" + + box_collection.stub( + all: [ + ["foo", "1.0", :virtualbox], + ["foo", "1.1", :virtualbox], + ]) + + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxRemoveMultiVersion) + end +end diff --git a/test/unit/vagrant/action/builtin/handle_box_test.rb b/test/unit/vagrant/action/builtin/handle_box_test.rb new file mode 100644 index 000000000..5ddbf7b7f --- /dev/null +++ b/test/unit/vagrant/action/builtin/handle_box_test.rb @@ -0,0 +1,109 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::HandleBox do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + action_runner: action_runner, + machine: machine, + ui: Vagrant::UI::Silent.new, + } } + + subject { described_class.new(app, env) } + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + isolated_environment.tap do |env| + env.vagrantfile("") + end + end + + let(:iso_vagrant_env) { iso_env.create_vagrant_env } + + let(:action_runner) { double("action_runner") } + let(:box) do + box_dir = iso_env.box3("foo", "1.0", :virtualbox) + Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) + end + let(:machine) { iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy) } + + it "works if there is no box set" do + machine.config.vm.box = nil + machine.config.vm.box_url = nil + + app.should_receive(:call).with(env) + + subject.call(env) + end + + it "doesn't do anything if a box exists" do + machine.stub(box: box) + + action_runner.should_receive(:run).never + app.should_receive(:call).with(env) + + subject.call(env) + end + + context "with a box set and no box_url" do + before do + machine.stub(box: nil) + + machine.config.vm.box = "foo" + end + + it "adds a box that doesn't exist" do + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_name]).to eq(machine.config.vm.box) + expect(opts[:box_url]).to eq(machine.config.vm.box) + expect(opts[:box_provider]).to eq(:dummy) + expect(opts[:box_version]).to eq(machine.config.vm.box_version) + true + end + + app.should_receive(:call).with(env) + + subject.call(env) + end + + it "adds a box using any format the provider allows" do + machine.provider_options[:box_format] = [:foo, :bar] + + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_name]).to eq(machine.config.vm.box) + expect(opts[:box_url]).to eq(machine.config.vm.box) + expect(opts[:box_provider]).to eq([:foo, :bar]) + expect(opts[:box_version]).to eq(machine.config.vm.box_version) + true + end + + app.should_receive(:call).with(env) + + subject.call(env) + end + end + + context "with a box and box_url set" do + before do + machine.stub(box: nil) + + machine.config.vm.box = "foo" + machine.config.vm.box_url = "bar" + end + + it "adds a box that doesn't exist" do + action_runner.should_receive(:run).with do |action, opts| + expect(opts[:box_name]).to eq(machine.config.vm.box) + expect(opts[:box_url]).to eq(machine.config.vm.box_url) + expect(opts[:box_provider]).to eq(:dummy) + expect(opts[:box_version]).to eq(machine.config.vm.box_version) + true + end + + app.should_receive(:call).with(env) + + subject.call(env) + end + end +end diff --git a/test/unit/vagrant/box_collection_test.rb b/test/unit/vagrant/box_collection_test.rb index 9b098f39e..7cdb242bd 100644 --- a/test/unit/vagrant/box_collection_test.rb +++ b/test/unit/vagrant/box_collection_test.rb @@ -8,35 +8,141 @@ describe Vagrant::BoxCollection do let(:box_class) { Vagrant::Box } let(:environment) { isolated_environment } - let(:instance) { described_class.new(environment.boxes_dir) } + + subject { described_class.new(environment.boxes_dir) } it "should tell us the directory it is using" do - instance.directory.should == environment.boxes_dir + subject.directory.should == environment.boxes_dir end - describe "adding" do + describe "#all" do + it "should return an empty array when no boxes are there" do + subject.all.should == [] + end + + it "should return the boxes and their providers" do + # Create some boxes + environment.box3("foo", "1.0", :virtualbox) + environment.box3("foo", "1.0", :vmware) + environment.box3("bar", "0", :ec2) + environment.box3("foo-VAGRANTSLASH-bar", "1.0", :virtualbox) + + # Verify some output + results = subject.all + results.length.should == 4 + results.include?(["foo", "1.0", :virtualbox]).should be + results.include?(["foo", "1.0", :vmware]).should be + results.include?(["bar", "0", :ec2]).should be + results.include?(["foo/bar", "1.0", :virtualbox]).should be + end + + it 'does not raise an exception when a file appears in the boxes dir' do + Tempfile.new('a_file', environment.boxes_dir) + expect { subject.all }.to_not raise_error + end + end + + describe "#find" do + it "returns nil if the box does not exist" do + expect(subject.find("foo", :i_dont_exist, ">= 0")).to be_nil + end + + it "returns a box if the box does exist" do + # Create the "box" + environment.box3("foo", "0", :virtualbox) + + # Actual test + result = subject.find("foo", :virtualbox, ">= 0") + expect(result).to_not be_nil + expect(result).to be_kind_of(box_class) + expect(result.name).to eq("foo") + end + + it "returns latest version matching constraint" do + # Create the "box" + environment.box3("foo", "1.0", :virtualbox) + environment.box3("foo", "1.5", :virtualbox) + + # Actual test + result = subject.find("foo", :virtualbox, ">= 0") + expect(result).to_not be_nil + expect(result).to be_kind_of(box_class) + expect(result.name).to eq("foo") + expect(result.version).to eq("1.5") + end + + it "can satisfy complex constraints" do + # Create the "box" + environment.box3("foo", "0.1", :virtualbox) + environment.box3("foo", "1.0", :virtualbox) + environment.box3("foo", "2.1", :virtualbox) + + # Actual test + result = subject.find("foo", :virtualbox, ">= 0.9, < 1.5") + expect(result).to_not be_nil + expect(result).to be_kind_of(box_class) + expect(result.name).to eq("foo") + expect(result.version).to eq("1.0") + end + + it "returns nil if a box's constraints can't be satisfied" do + # Create the "box" + environment.box3("foo", "0.1", :virtualbox) + environment.box3("foo", "1.0", :virtualbox) + environment.box3("foo", "2.1", :virtualbox) + + # Actual test + result = subject.find("foo", :virtualbox, "> 1.0, < 1.5") + expect(result).to be_nil + end + end + + describe "#add" do it "should add a valid box to the system" do box_path = environment.box2_file(:virtualbox) # Add the box - box = instance.add(box_path, "foo", :virtualbox) - box.should be_kind_of(box_class) - box.name.should == "foo" - box.provider.should == :virtualbox + box = subject.add(box_path, "foo", "1.0", providers: :virtualbox) + expect(box).to be_kind_of(box_class) + expect(box.name).to eq("foo") + expect(box.provider).to eq(:virtualbox) # Verify we can find it as well - box = instance.find("foo", :virtualbox) - box.should_not be_nil + expect(subject.find("foo", :virtualbox, "1.0")).to_not be_nil + end + + it "should add a box with a name with '/' in it" do + box_path = environment.box2_file(:virtualbox) + + # Add the box + box = subject.add(box_path, "foo/bar", "1.0") + expect(box).to be_kind_of(box_class) + expect(box.name).to eq("foo/bar") + expect(box.provider).to eq(:virtualbox) + + # Verify we can find it as well + expect(subject.find("foo/bar", :virtualbox, "1.0")).to_not be_nil end it "should add a box without specifying a provider" do box_path = environment.box2_file(:vmware) # Add the box - box = instance.add(box_path, "foo") - box.should be_kind_of(box_class) - box.name.should == "foo" - box.provider.should == :vmware + box = subject.add(box_path, "foo", "1.0") + expect(box).to be_kind_of(box_class) + expect(box.name).to eq("foo") + expect(box.provider).to eq(:vmware) + end + + it "should store a metadata URL" do + box_path = environment.box2_file(:virtualbox) + + subject.add( + box_path, "foo", "1.0", + metadata_url: "bar") + + box = subject.find("foo", :virtualbox, "1.0") + expect(box.metadata_url).to eq("bar") end it "should add a V1 box" do @@ -44,35 +150,39 @@ describe Vagrant::BoxCollection do box_path = environment.box1_file # Add the box - box = instance.add(box_path, "foo") - box.should be_kind_of(box_class) - box.name.should == "foo" - box.provider.should == :virtualbox + box = subject.add(box_path, "foo", "1.0") + expect(box).to be_kind_of(box_class) + expect(box.name).to eq("foo") + expect(box.provider).to eq(:virtualbox) end it "should raise an exception if the box already exists" do prev_box_name = "foo" prev_box_provider = :virtualbox + prev_box_version = "1.0" # Create the box we're adding - environment.box2(prev_box_name, prev_box_provider) + environment.box3(prev_box_name, "1.0", prev_box_provider) # Attempt to add the box with the same name box_path = environment.box2_file(prev_box_provider) - expect { instance.add(box_path, prev_box_name, prev_box_provider) }. - to raise_error(Vagrant::Errors::BoxAlreadyExists) + expect { + subject.add(box_path, prev_box_name, + prev_box_version, providers: prev_box_provider) + }.to raise_error(Vagrant::Errors::BoxAlreadyExists) end it "should replace the box if force is specified" do prev_box_name = "foo" prev_box_provider = :vmware + prev_box_version = "1.0" # Setup the environment with the box pre-added - environment.box2(prev_box_name, prev_box_provider) + environment.box3(prev_box_name, prev_box_version, prev_box_provider) # Attempt to add the box with the same name box_path = environment.box2_file(prev_box_provider, metadata: { "replaced" => "yes" }) - box = instance.add(box_path, prev_box_name, nil, true) + box = subject.add(box_path, prev_box_name, prev_box_version, force: true) box.metadata["replaced"].should == "yes" end @@ -82,25 +192,13 @@ describe Vagrant::BoxCollection do box_path = environment.box2_file(:vmware) # Add it once, successfully - expect { instance.add(box_path, box_name) }.to_not raise_error + expect { subject.add(box_path, box_name, "1.0") }.to_not raise_error # Add it again, and fail! - expect { instance.add(box_path, box_name) }. + expect { subject.add(box_path, box_name, "1.0") }. to raise_error(Vagrant::Errors::BoxAlreadyExists) end - it "should raise an exception if you're attempting to add a box that exists as a V1 box" do - prev_box_name = "foo" - - # Create the V1 box - environment.box1(prev_box_name) - - # Attempt to add some V2 box with the same name - box_path = environment.box2_file(:vmware) - expect { instance.add(box_path, prev_box_name) }. - to raise_error(Vagrant::Errors::BoxUpgradeRequired) - end - it "should raise an exception and not add the box if the provider doesn't match" do box_name = "foo" good_provider = :virtualbox @@ -111,11 +209,11 @@ describe Vagrant::BoxCollection do # Add the box but with an invalid provider, verify we get the proper # error. - expect { instance.add(box_path, box_name, bad_provider) }. + expect { subject.add(box_path, box_name, "1.0", providers: bad_provider) }. to raise_error(Vagrant::Errors::BoxProviderDoesntMatch) # Verify the box doesn't exist - instance.find(box_name, bad_provider).should be_nil + expect(subject.find(box_name, bad_provider, "1.0")).to be_nil end it "should raise an exception if you add an invalid box file" do @@ -130,7 +228,7 @@ describe Vagrant::BoxCollection do f.write("\0"*CHECKSUM_LENGTH) f.close - expect { instance.add(f.path, "foo", :virtualbox) }. + expect { subject.add(f.path, "foo", "1.0") }. to raise_error(Vagrant::Errors::BoxUnpackageFailure) ensure f.close @@ -139,103 +237,36 @@ describe Vagrant::BoxCollection do end end - describe "listing all" do - it "should return an empty array when no boxes are there" do - instance.all.should == [] + describe "#upgrade_v1_1_v1_5" do + let(:boxes_dir) { environment.boxes_dir } + + before do + # Create all the various box directories + @foo_path = environment.box2("foo", "virtualbox") + @vbox_path = environment.box2("precise64", "virtualbox") + @vmware_path = environment.box2("precise64", "vmware") + @v1_path = environment.box("v1box") end - it "should return the boxes and their providers" do - # Create some boxes - environment.box2("foo", :virtualbox) - environment.box2("foo", :vmware) - environment.box2("bar", :ec2) + it "upgrades the boxes" do + subject.upgrade_v1_1_v1_5 - # Verify some output - results = instance.all - results.length.should == 3 - results.include?(["foo", :virtualbox]).should be - results.include?(["foo", :vmware]).should be - results.include?(["bar", :ec2]).should be - end + # The old paths should not exist anymore + expect(@foo_path).to_not exist + expect(@vbox_path).to_not exist + expect(@vmware_path).to_not exist + expect(@v1_path.join("box.ovf")).to_not exist - it "should return V1 boxes as well" do - # Create some boxes, including a V1 box - environment.box1("bar") - environment.box2("foo", :vmware) + # New paths should exist + foo_path = boxes_dir.join("foo", "0", "virtualbox") + vbox_path = boxes_dir.join("precise64", "0", "virtualbox") + vmware_path = boxes_dir.join("precise64", "0", "vmware") + v1_path = boxes_dir.join("v1box", "0", "virtualbox") - # Verify some output - results = instance.all.sort - results.should == [["bar", :virtualbox, :v1], ["foo", :vmware]] - end - - it 'does not raise an exception when a file appears in the boxes dir' do - Tempfile.new('a_file', environment.boxes_dir) - expect { instance.all }.to_not raise_error - end - end - - describe "finding" do - it "should return nil if the box does not exist" do - instance.find("foo", :i_dont_exist).should be_nil - end - - it "should return a box if the box does exist" do - # Create the "box" - environment.box2("foo", :virtualbox) - - # Actual test - result = instance.find("foo", :virtualbox) - result.should_not be_nil - result.should be_kind_of(box_class) - result.name.should == "foo" - end - - it "should throw an exception if it is a v1 box" do - # Create a V1 box - environment.box1("foo") - - # Test! - expect { instance.find("foo", :virtualbox) }. - to raise_error(Vagrant::Errors::BoxUpgradeRequired) - end - - it "should return nil if there is a V1 box but we're looking for another provider" do - # Create a V1 box - environment.box1("foo") - - # Test - instance.find("foo", :another_provider).should be_nil - end - end - - describe "upgrading" do - it "should upgrade a V1 box to V2" do - # Create a V1 box - environment.box1("foo") - - # Verify that only a V1 box exists - expect { instance.find("foo", :virtualbox) }. - to raise_error(Vagrant::Errors::BoxUpgradeRequired) - - # Upgrade the box - instance.upgrade("foo").should be - - # Verify the box exists - box = instance.find("foo", :virtualbox) - box.should_not be_nil - box.name.should == "foo" - end - - it "should raise a BoxNotFound exception if a non-existent box is upgraded" do - expect { instance.upgrade("i-dont-exist") }. - to raise_error(Vagrant::Errors::BoxNotFound) - end - - it "should return true if we try to upgrade a V2 box" do - # Create a V2 box - environment.box2("foo", :vmware) - - instance.upgrade("foo").should be + expect(foo_path).to exist + expect(vbox_path).to exist + expect(vmware_path).to exist + expect(v1_path).to exist end end end diff --git a/test/unit/vagrant/box_metadata_test.rb b/test/unit/vagrant/box_metadata_test.rb new file mode 100644 index 000000000..4ace29922 --- /dev/null +++ b/test/unit/vagrant/box_metadata_test.rb @@ -0,0 +1,157 @@ +require File.expand_path("../../base", __FILE__) + +require "vagrant/box_metadata" + +describe Vagrant::BoxMetadata do + include_context "unit" + + let(:raw) do + <<-RAW + { + "name": "foo", + "description": "bar", + "versions": [ + { + "version": "1.0.0", + "providers": [ + { "name": "virtualbox" }, + { "name": "vmware" } + ] + }, + { + "version": "1.1.5", + "providers": [ + { "name": "virtualbox" } + ] + }, + { + "version": "1.1.0", + "providers": [ + { "name": "virtualbox" }, + { "name": "vmware" } + ] + } + ] + } + RAW + end + + subject { described_class.new(raw) } + + its(:name) { should eq("foo") } + its(:description) { should eq("bar") } + + context "with poorly formatted JSON" do + let(:raw) { + <<-RAW + { "name": "foo", } + RAW + } + + it "raises an exception" do + expect { subject }. + to raise_error(Vagrant::Errors::BoxMetadataMalformed) + end + end + + describe "#version" do + it "matches an exact version" do + result = subject.version("1.0.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "matches a constraint with latest matching version" do + result = subject.version(">= 1.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.1.5") + end + + it "matches complex constraints" do + result = subject.version(">= 0.9, ~> 1.0.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "matches the constraint that has the given provider" do + result = subject.version(">= 0", provider: :vmware) + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.1.0") + end + end + + describe "#versions" do + it "returns the versions it contained" do + expect(subject.versions).to eq( + ["1.0.0", "1.1.0", "1.1.5"]) + end + end +end + +describe Vagrant::BoxMetadata::Version do + let(:raw) { {} } + + subject { described_class.new(raw) } + + before do + raw["providers"] = [ + { + "name" => "virtualbox", + }, + { + "name" => "vmware", + } + ] + end + + describe "#version" do + it "is the version in the raw data" do + v = "1.0" + raw["version"] = v + expect(subject.version).to eq(v) + end + end + + describe "#provider" do + it "returns nil if a provider isn't supported" do + expect(subject.provider("foo")).to be_nil + end + + it "returns the provider specified" do + result = subject.provider("virtualbox") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Provider) + end + end + + describe "#providers" do + it "returns the providers available" do + expect(subject.providers.sort).to eq( + [:virtualbox, :vmware]) + end + end +end + +describe Vagrant::BoxMetadata::Provider do + let(:raw) { {} } + + subject { described_class.new(raw) } + + describe "#name" do + it "is the name specified" do + raw["name"] = "foo" + expect(subject.name).to eq("foo") + end + end + + describe "#url" do + it "is the URL specified" do + raw["url"] = "bar" + expect(subject.url).to eq("bar") + end + end +end diff --git a/test/unit/vagrant/box_test.rb b/test/unit/vagrant/box_test.rb index 93dbfea17..797115651 100644 --- a/test/unit/vagrant/box_test.rb +++ b/test/unit/vagrant/box_test.rb @@ -1,6 +1,10 @@ require File.expand_path("../../base", __FILE__) require "pathname" +require "stringio" +require "tempfile" + +require "vagrant/box_metadata" describe Vagrant::Box do include_context "unit" @@ -11,21 +15,22 @@ describe Vagrant::Box do let(:name) { "foo" } let(:provider) { :virtualbox } - let(:directory) { environment.box2("foo", :virtualbox) } - let(:instance) { described_class.new(name, provider, directory) } + let(:version) { "1.0" } + let(:directory) { environment.box3("foo", "1.0", :virtualbox) } + subject { described_class.new(name, provider, version, directory) } - subject { described_class.new(name, provider, directory) } + its(:metadata_url) { should be_nil } it "provides the name" do - instance.name.should == name + subject.name.should == name end it "provides the provider" do - instance.provider.should == provider + subject.provider.should == provider end it "provides the directory" do - instance.directory.should == directory + subject.directory.should == directory end it "provides the metadata associated with a box" do @@ -37,7 +42,17 @@ describe Vagrant::Box do end # Verify the metadata - instance.metadata.should == data + subject.metadata.should == data + end + + context "with a metadata URL" do + subject do + described_class.new( + name, provider, version, directory, + metadata_url: "foo") + end + + its(:metadata_url) { should eq("foo") } end context "with a corrupt metadata file" do @@ -64,21 +79,157 @@ describe Vagrant::Box do end end + context "#has_update?" do + subject do + described_class.new( + name, provider, version, directory, + metadata_url: "foo") + end + + it "raises an exception if no metadata_url is set" do + subject = described_class.new( + name, provider, version, directory) + + expect { subject.has_update?("> 0") }. + to raise_error(Vagrant::Errors::BoxUpdateNoMetadata) + end + + it "returns nil if there is no update" do + metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) + { + "name": "foo", + "versions": [ + { "version": "1.0" } + ] + } + RAW + + subject.stub(load_metadata: metadata) + + expect(subject.has_update?).to be_nil + end + + it "returns the updated box info if there is an update available" do + metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW + + subject.stub(load_metadata: metadata) + + result = subject.has_update? + expect(result).to_not be_nil + + expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) + expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) + + expect(result[0].name).to eq("foo") + expect(result[1].version).to eq("1.1") + expect(result[2].url).to eq("bar") + end + + it "returns the updated box info within constraints" do + metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + }, + { + "version": "1.4", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW + + subject.stub(load_metadata: metadata) + + result = subject.has_update?(">= 1.1, < 1.4") + expect(result).to_not be_nil + + expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) + expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) + + expect(result[0].name).to eq("foo") + expect(result[1].version).to eq("1.1") + expect(result[2].url).to eq("bar") + end + end + + context "#load_metadata" do + let(:metadata_url) do + Tempfile.new("vagrant").tap do |f| + f.write(<<-RAW) + { + "name": "foo", + "description": "bar" + } + RAW + f.close + end + end + + subject do + described_class.new( + name, provider, version, directory, + metadata_url: metadata_url.path) + end + + it "loads the url and returns the data" do + result = subject.load_metadata + expect(result.name).to eq("foo") + expect(result.description).to eq("bar") + end + end + describe "destroying" do it "should destroy an existing box" do # Verify that our "box" exists directory.exist?.should be # Destroy it - instance.destroy!.should be + subject.destroy!.should be # Verify that it is "destroyed" directory.exist?.should_not be end it "should not error destroying a non-existent box" do - # Get the instance so that it is instantiated - box = instance + # Get the subject so that it is instantiated + box = subject # Delete the directory directory.rmtree @@ -100,36 +251,51 @@ describe Vagrant::Box do # Repackage our box to some temporary directory box_output_path = temporary_dir.join("package.box") - instance.repackage(box_output_path).should be + expect(subject.repackage(box_output_path)).to be_true # Let's now add this box again under a different name, and then # verify that we get the proper result back. - new_box = box_collection.add(box_output_path, "foo2") + new_box = box_collection.add(box_output_path, "foo2", "1.0") new_box.directory.join("test_file").read.should == test_file_contents end end describe "comparison and ordering" do - it "should be equal if the name and provider match" do - a = described_class.new("a", :foo, directory) - b = described_class.new("a", :foo, directory) + it "should be equal if the name, provider, version match" do + a = described_class.new("a", :foo, "1.0", directory) + b = described_class.new("a", :foo, "1.0", directory) a.should == b end - it "should not be equal if the name and provider do not match" do - a = described_class.new("a", :foo, directory) - b = described_class.new("b", :foo, directory) + it "should not be equal if name doesn't match" do + a = described_class.new("a", :foo, "1.0", directory) + b = described_class.new("b", :foo, "1.0", directory) - a.should_not == b + expect(a).to_not eq(b) end - it "should sort them in order of name then provider" do - a = described_class.new("a", :foo, directory) - b = described_class.new("b", :foo, directory) - c = described_class.new("c", :foo2, directory) + it "should not be equal if provider doesn't match" do + a = described_class.new("a", :foo, "1.0", directory) + b = described_class.new("a", :bar, "1.0", directory) - [c, a, b].sort.should == [a, b, c] + expect(a).to_not eq(b) + end + + it "should not be equal if version doesn't match" do + a = described_class.new("a", :foo, "1.0", directory) + b = described_class.new("a", :foo, "1.1", directory) + + expect(a).to_not eq(b) + end + + it "should sort them in order of name, version, provider" do + a = described_class.new("a", :foo, "1.0", directory) + b = described_class.new("a", :foo2, "1.0", directory) + c = described_class.new("a", :foo2, "1.1", directory) + d = described_class.new("b", :foo2, "1.0", directory) + + [d, c, a, b].sort.should == [a, b, c, d] end end end diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index d686c09ce..2e7758dcd 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -13,7 +13,7 @@ describe Vagrant::Environment do let(:env) do isolated_environment.tap do |e| - e.box2("base", :virtualbox) + e.box3("base", "1.0", :virtualbox) e.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vm.box = "base" @@ -25,6 +25,100 @@ describe Vagrant::Environment do let(:instance) { env.create_vagrant_env } subject { instance } + describe "#home_path" do + it "is set to the home path given" do + Dir.mktmpdir do |dir| + instance = described_class.new(:home_path => dir) + instance.home_path.should == Pathname.new(dir) + end + end + + it "is set to the environmental variable VAGRANT_HOME" do + Dir.mktmpdir do |dir| + instance = with_temp_env("VAGRANT_HOME" => dir) do + described_class.new + end + + instance.home_path.should == Pathname.new(dir) + end + end + + it "throws an exception if inaccessible" do + expect { + described_class.new(:home_path => "/") + }.to raise_error(Vagrant::Errors::HomeDirectoryNotAccessible) + end + + context "with setup version file" do + it "creates a setup version flie" do + path = subject.home_path.join("setup_version") + expect(path).to be_file + expect(path.read).to eq(Vagrant::Environment::CURRENT_SETUP_VERSION) + end + + it "is okay if it has the current version" do + Dir.mktmpdir do |dir| + Pathname.new(dir).join("setup_version").open("w") do |f| + f.write(Vagrant::Environment::CURRENT_SETUP_VERSION) + end + + instance = described_class.new(home_path: dir) + path = instance.home_path.join("setup_version") + expect(path).to be_file + expect(path.read).to eq(Vagrant::Environment::CURRENT_SETUP_VERSION) + end + end + + it "raises an exception if the version is newer than ours" do + Dir.mktmpdir do |dir| + Pathname.new(dir).join("setup_version").open("w") do |f| + f.write("100.5") + end + + expect { described_class.new(home_path: dir) }. + to raise_error(Vagrant::Errors::HomeDirectoryLaterVersion) + end + end + + it "raises an exception if there is an unknown home directory version" do + Dir.mktmpdir do |dir| + Pathname.new(dir).join("setup_version").open("w") do |f| + f.write("0.7") + end + + expect { described_class.new(home_path: dir) }. + to raise_error(Vagrant::Errors::HomeDirectoryUnknownVersion) + end + end + end + + context "upgrading a v1.1 directory structure" do + let(:env) { isolated_environment } + + before do + env.homedir.join("setup_version").open("w") do |f| + f.write("1.1") + end + end + + it "replaces the setup version with the new version" do + expect(subject.home_path.join("setup_version").read). + to eq(Vagrant::Environment::CURRENT_SETUP_VERSION) + end + + it "moves the boxes into the new directory structure" do + # Kind of hacky but avoids two instantiations of BoxCollection + Vagrant::Environment.any_instance.stub(boxes: double("boxes")) + + collection = double("collection") + Vagrant::BoxCollection.should_receive(:new).with( + env.homedir.join("boxes"), anything).and_return(collection) + collection.should_receive(:upgrade_v1_1_v1_5).once + subject + end + end + end + describe "#host" do let(:plugin_hosts) { {} } let(:plugin_host_caps) { {} } @@ -100,6 +194,334 @@ describe Vagrant::Environment do end end + describe "#machine" do + # A helper to register a provider for use in tests. + def register_provider(name, config_class=nil, options=nil) + provider_cls = Class.new(Vagrant.plugin("2", :provider)) + + register_plugin("2") do |p| + p.provider(name, options) { provider_cls } + + if config_class + p.config(name, :provider) { config_class } + end + end + + provider_cls + end + + it "should return a machine object with the correct provider" do + # Create a provider + foo_provider = register_provider("foo") + + # Create the configuration + isolated_env = isolated_environment do |e| + e.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" + config.vm.define "foo" +end +VF + + e.box3("base", "1.0", :foo) + end + + # Verify that we can get the machine + env = isolated_env.create_vagrant_env + machine = env.machine(:foo, :foo) + machine.should be_kind_of(Vagrant::Machine) + machine.name.should == :foo + machine.provider.should be_kind_of(foo_provider) + machine.provider_config.should be_nil + end + + it "should return a machine object with the machine configuration" do + # Create a provider + foo_config = Class.new(Vagrant.plugin("2", :config)) do + attr_accessor :value + end + + foo_provider = register_provider("foo", foo_config) + + # Create the configuration + isolated_env = isolated_environment do |e| + e.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" + config.vm.define "foo" + + config.vm.provider :foo do |fooconfig| + fooconfig.value = 100 + end +end +VF + + e.box3("base", "1.0", :foo) + end + + # Verify that we can get the machine + env = isolated_env.create_vagrant_env + machine = env.machine(:foo, :foo) + machine.should be_kind_of(Vagrant::Machine) + machine.name.should == :foo + machine.provider.should be_kind_of(foo_provider) + machine.provider_config.value.should == 100 + end + + it "should cache the machine objects by name and provider" do + # Create a provider + foo_provider = register_provider("foo") + bar_provider = register_provider("bar") + + # Create the configuration + isolated_env = isolated_environment do |e| + e.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" + config.vm.define "vm1" + config.vm.define "vm2" +end +VF + + e.box3("base", "1.0", :foo) + e.box3("base", "1.0", :bar) + end + + env = isolated_env.create_vagrant_env + vm1_foo = env.machine(:vm1, :foo) + vm1_bar = env.machine(:vm1, :bar) + vm2_foo = env.machine(:vm2, :foo) + + vm1_foo.should eql(env.machine(:vm1, :foo)) + vm1_bar.should eql(env.machine(:vm1, :bar)) + vm1_foo.should_not eql(vm1_bar) + vm2_foo.should eql(env.machine(:vm2, :foo)) + end + + it "should load a machine without a box" do + register_provider("foo") + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "i-dont-exist" +end +VF + end + + env = environment.create_vagrant_env + machine = env.machine(:default, :foo) + machine.box.should be_nil + end + + it "should load the machine configuration" do + register_provider("foo") + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 1 + config.vm.box = "base" + + config.vm.define "vm1" do |inner| + inner.ssh.port = 100 + end +end +VF + + env.box3("base", "1.0", :foo) + end + + env = environment.create_vagrant_env + machine = env.machine(:vm1, :foo) + machine.config.ssh.port.should == 100 + machine.config.vm.box.should == "base" + end + + it "should load the box configuration for a box" do + register_provider("foo") + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" +end +VF + + env.box3("base", "1.0", :foo, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 100 +end +VF + end + + env = environment.create_vagrant_env + machine = env.machine(:default, :foo) + machine.config.ssh.port.should == 100 + end + + it "should load the box configuration for a box and custom Vagrantfile name" do + register_provider("foo") + + environment = isolated_environment do |env| + env.file("some_other_name", <<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" +end +VF + + env.box3("base", "1.0", :foo, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 100 +end +VF + end + + env = with_temp_env("VAGRANT_VAGRANTFILE" => "some_other_name") do + environment.create_vagrant_env + end + + machine = env.machine(:default, :foo) + machine.config.ssh.port.should == 100 + end + + it "should load the box configuration for other formats for a box" do + register_provider("foo", nil, box_format: "bar") + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" +end +VF + + env.box3("base", "1.0", :bar, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 100 +end +VF + end + + env = environment.create_vagrant_env + machine = env.machine(:default, :foo) + machine.config.ssh.port.should == 100 + end + + it "prefer sooner formats when multiple box formats are available" do + register_provider("foo", nil, box_format: ["fA", "fB"]) + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" +end +VF + + env.box3("base", "1.0", :fA, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 100 +end +VF + + env.box3("base", "1.0", :fB, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 200 +end +VF + end + + env = environment.create_vagrant_env + machine = env.machine(:default, :foo) + machine.config.ssh.port.should == 100 + end + + it "should load the proper version of a box" do + register_provider("foo") + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" + config.vm.box_version = "~> 1.2" +end +VF + + env.box3("base", "1.0", :foo, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 100 +end +VF + + env.box3("base", "1.5", :foo, :vagrantfile => <<-VF) +Vagrant.configure("2") do |config| + config.ssh.port = 200 +end +VF + end + + env = environment.create_vagrant_env + machine = env.machine(:default, :foo) + machine.config.ssh.port.should == 200 + end + + it "should load the provider override if set" do + register_provider("bar") + register_provider("foo") + + isolated_env = isolated_environment do |e| + e.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "foo" + + config.vm.provider :foo do |_, c| + c.vm.box = "bar" + end +end +VF + end + + env = isolated_env.create_vagrant_env + foo_vm = env.machine(:default, :foo) + bar_vm = env.machine(:default, :bar) + foo_vm.config.vm.box.should == "bar" + bar_vm.config.vm.box.should == "foo" + end + + it "should reload the cache if refresh is set" do + # Create a provider + foo_provider = register_provider("foo") + + # Create the configuration + isolated_env = isolated_environment do |e| + e.vagrantfile(<<-VF) +Vagrant.configure("2") do |config| + config.vm.box = "base" +end +VF + + e.box3("base", "1.0", :foo) + end + + env = isolated_env.create_vagrant_env + vm1 = env.machine(:default, :foo) + vm2 = env.machine(:default, :foo, true) + vm3 = env.machine(:default, :foo) + + vm1.should_not eql(vm2) + vm2.should eql(vm3) + end + + it "should raise an error if the VM is not found" do + expect { instance.machine("i-definitely-dont-exist", :virtualbox) }. + to raise_error(Vagrant::Errors::MachineNotFound) + end + + it "should raise an error if the provider is not found" do + expect { instance.machine(:default, :lol_no) }. + to raise_error(Vagrant::Errors::ProviderNotFound) + end + end + describe "active machines" do it "should be empty if the machines folder doesn't exist" do folder = instance.local_data_path.join("machines") @@ -211,60 +633,6 @@ describe Vagrant::Environment do end end - describe "home path" do - it "is set to the home path given" do - Dir.mktmpdir do |dir| - instance = described_class.new(:home_path => dir) - instance.home_path.should == Pathname.new(dir) - end - end - - it "is set to the environmental variable VAGRANT_HOME" do - Dir.mktmpdir do |dir| - instance = with_temp_env("VAGRANT_HOME" => dir) do - described_class.new - end - - instance.home_path.should == Pathname.new(dir) - end - end - - context "default home path" do - it "is set to '~/.vagrant.d' by default" do - expected = Vagrant::Util::Platform.fs_real_path("~/.vagrant.d") - described_class.new.home_path.should == expected - end - - it "is set to '~/.vagrant.d' if on Windows but no USERPROFILE" do - Vagrant::Util::Platform.stub(:windows? => true) - - expected = Vagrant::Util::Platform.fs_real_path("~/.vagrant.d") - - with_temp_env("USERPROFILE" => nil) do - described_class.new.home_path.should == expected - end - end - - it "is set to '%USERPROFILE%/.vagrant.d' if on Windows and USERPROFILE is set" do - Vagrant::Util::Platform.stub(:windows? => true) - - Dir.mktmpdir do |dir| - expected = Vagrant::Util::Platform.fs_real_path("#{dir}/.vagrant.d") - - with_temp_env("USERPROFILE" => dir) do - described_class.new.home_path.should == expected - end - end - end - end - - it "throws an exception if inaccessible" do - expect { - described_class.new(:home_path => "/") - }.to raise_error(Vagrant::Errors::HomeDirectoryNotAccessible) - end - end - describe "local data path" do it "is set to the proper default" do default = instance.root_path.join(described_class::DEFAULT_LOCAL_DATA) @@ -429,7 +797,7 @@ Vagrant.configure("2") do |config| end VF - env.box2("base", :virtualbox) + env.box3("base", "1.0", :virtualbox) end env = environment.create_vagrant_env @@ -446,7 +814,7 @@ Vagrant.configure("2") do |config| end VF - env.box2("base", :virtualbox) + env.box3("base", "1.0", :virtualbox) end env = environment.create_vagrant_env @@ -519,305 +887,6 @@ VF end end - describe "getting a machine" do - # A helper to register a provider for use in tests. - def register_provider(name, config_class=nil, options=nil) - provider_cls = Class.new(Vagrant.plugin("2", :provider)) - - register_plugin("2") do |p| - p.provider(name, options) { provider_cls } - - if config_class - p.config(name, :provider) { config_class } - end - end - - provider_cls - end - - it "should return a machine object with the correct provider" do - # Create a provider - foo_provider = register_provider("foo") - - # Create the configuration - isolated_env = isolated_environment do |e| - e.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" - config.vm.define "foo" -end -VF - - e.box2("base", :foo) - end - - # Verify that we can get the machine - env = isolated_env.create_vagrant_env - machine = env.machine(:foo, :foo) - machine.should be_kind_of(Vagrant::Machine) - machine.name.should == :foo - machine.provider.should be_kind_of(foo_provider) - machine.provider_config.should be_nil - end - - it "should return a machine object with the machine configuration" do - # Create a provider - foo_config = Class.new(Vagrant.plugin("2", :config)) do - attr_accessor :value - end - - foo_provider = register_provider("foo", foo_config) - - # Create the configuration - isolated_env = isolated_environment do |e| - e.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" - config.vm.define "foo" - - config.vm.provider :foo do |fooconfig| - fooconfig.value = 100 - end -end -VF - - e.box2("base", :foo) - end - - # Verify that we can get the machine - env = isolated_env.create_vagrant_env - machine = env.machine(:foo, :foo) - machine.should be_kind_of(Vagrant::Machine) - machine.name.should == :foo - machine.provider.should be_kind_of(foo_provider) - machine.provider_config.value.should == 100 - end - - it "should cache the machine objects by name and provider" do - # Create a provider - foo_provider = register_provider("foo") - bar_provider = register_provider("bar") - - # Create the configuration - isolated_env = isolated_environment do |e| - e.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" - config.vm.define "vm1" - config.vm.define "vm2" -end -VF - - e.box2("base", :foo) - e.box2("base", :bar) - end - - env = isolated_env.create_vagrant_env - vm1_foo = env.machine(:vm1, :foo) - vm1_bar = env.machine(:vm1, :bar) - vm2_foo = env.machine(:vm2, :foo) - - vm1_foo.should eql(env.machine(:vm1, :foo)) - vm1_bar.should eql(env.machine(:vm1, :bar)) - vm1_foo.should_not eql(vm1_bar) - vm2_foo.should eql(env.machine(:vm2, :foo)) - end - - it "should load a machine without a box" do - register_provider("foo") - - environment = isolated_environment do |env| - env.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "i-dont-exist" -end -VF - end - - env = environment.create_vagrant_env - machine = env.machine(:default, :foo) - machine.box.should be_nil - end - - it "should load the machine configuration" do - register_provider("foo") - - environment = isolated_environment do |env| - env.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.ssh.port = 1 - config.vm.box = "base" - - config.vm.define "vm1" do |inner| - inner.ssh.port = 100 - end -end -VF - - env.box2("base", :foo) - end - - env = environment.create_vagrant_env - machine = env.machine(:vm1, :foo) - machine.config.ssh.port.should == 100 - machine.config.vm.box.should == "base" - end - - it "should load the box configuration for a V2 box" do - register_provider("foo") - - environment = isolated_environment do |env| - env.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" -end -VF - - env.box2("base", :foo, :vagrantfile => <<-VF) -Vagrant.configure("2") do |config| - config.ssh.port = 100 -end -VF - end - - env = environment.create_vagrant_env - machine = env.machine(:default, :foo) - machine.config.ssh.port.should == 100 - end - - it "should load the box configuration for a V2 box and custom Vagrantfile name" do - register_provider("foo") - - environment = isolated_environment do |env| - env.file("some_other_name", <<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" -end -VF - - env.box2("base", :foo, :vagrantfile => <<-VF) -Vagrant.configure("2") do |config| - config.ssh.port = 100 -end -VF - end - - env = with_temp_env("VAGRANT_VAGRANTFILE" => "some_other_name") do - environment.create_vagrant_env - end - - machine = env.machine(:default, :foo) - machine.config.ssh.port.should == 100 - end - - it "should load the box configuration for other formats for a V2 box" do - register_provider("foo", nil, box_format: "bar") - - environment = isolated_environment do |env| - env.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" -end -VF - - env.box2("base", :bar, :vagrantfile => <<-VF) -Vagrant.configure("2") do |config| - config.ssh.port = 100 -end -VF - end - - env = environment.create_vagrant_env - machine = env.machine(:default, :foo) - machine.config.ssh.port.should == 100 - end - - it "prefer sooner formats when multiple box formats are available" do - register_provider("foo", nil, box_format: ["fA", "fB"]) - - environment = isolated_environment do |env| - env.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" -end -VF - - env.box2("base", :fA, :vagrantfile => <<-VF) -Vagrant.configure("2") do |config| - config.ssh.port = 100 -end -VF - - env.box2("base", :fB, :vagrantfile => <<-VF) -Vagrant.configure("2") do |config| - config.ssh.port = 200 -end -VF - end - - env = environment.create_vagrant_env - machine = env.machine(:default, :foo) - machine.config.ssh.port.should == 100 - end - - it "should load the provider override if set" do - register_provider("bar") - register_provider("foo") - - isolated_env = isolated_environment do |e| - e.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "foo" - - config.vm.provider :foo do |_, c| - c.vm.box = "bar" - end -end -VF - end - - env = isolated_env.create_vagrant_env - foo_vm = env.machine(:default, :foo) - bar_vm = env.machine(:default, :bar) - foo_vm.config.vm.box.should == "bar" - bar_vm.config.vm.box.should == "foo" - end - - it "should reload the cache if refresh is set" do - # Create a provider - foo_provider = register_provider("foo") - - # Create the configuration - isolated_env = isolated_environment do |e| - e.vagrantfile(<<-VF) -Vagrant.configure("2") do |config| - config.vm.box = "base" -end -VF - - e.box2("base", :foo) - end - - env = isolated_env.create_vagrant_env - vm1 = env.machine(:default, :foo) - vm2 = env.machine(:default, :foo, true) - vm3 = env.machine(:default, :foo) - - vm1.should_not eql(vm2) - vm2.should eql(vm3) - end - - it "should raise an error if the VM is not found" do - expect { instance.machine("i-definitely-dont-exist", :virtualbox) }. - to raise_error(Vagrant::Errors::MachineNotFound) - end - - it "should raise an error if the provider is not found" do - expect { instance.machine(:default, :lol_no) }. - to raise_error(Vagrant::Errors::ProviderNotFound) - end - end - describe "getting machine names" do it "should return the default machine if no multi-VM is used" do # Create the config diff --git a/test/unit/vagrant/shared_helpers_test.rb b/test/unit/vagrant/shared_helpers_test.rb index c11408802..cfa9cc46f 100644 --- a/test/unit/vagrant/shared_helpers_test.rb +++ b/test/unit/vagrant/shared_helpers_test.rb @@ -22,6 +22,21 @@ describe Vagrant do end end + describe "#server_url" do + it "defaults to the default value" do + with_temp_env("VAGRANT_SERVER_URL" => nil) do + expect(subject.server_url).to eq( + Vagrant::DEFAULT_SERVER_URL) + end + end + + it "is the VAGRANT_SERVER_URL value" do + with_temp_env("VAGRANT_SERVER_URL" => "foo") do + expect(subject.server_url).to eq("foo") + end + end + end + describe "#user_data_path" do around do |example| env = { diff --git a/test/unit/vagrant/ui_test.rb b/test/unit/vagrant/ui_test.rb index 735d41da8..b8cc122da 100644 --- a/test/unit/vagrant/ui_test.rb +++ b/test/unit/vagrant/ui_test.rb @@ -65,7 +65,7 @@ describe Vagrant::UI::Colored do describe "#detail" do it "colors output nothing by default" do - subject.should_receive(:safe_puts).with("foo", anything) + subject.should_receive(:safe_puts).with("\033[0mfoo\033[0m", anything) subject.detail("foo") end @@ -91,11 +91,16 @@ describe Vagrant::UI::Colored do end describe "#output" do - it "colors output nothing by default" do - subject.should_receive(:safe_puts).with("foo", anything) + it "colors output nothing by default, no bold" do + subject.should_receive(:safe_puts).with("\033[0mfoo\033[0m", anything) subject.output("foo") end + it "bolds output without color if specified" do + subject.should_receive(:safe_puts).with("\033[1mfoo\033[0m", anything) + subject.output("foo", bold: true) + end + it "colors output to color specified in global opts" do subject.opts[:color] = :red @@ -232,6 +237,13 @@ describe Vagrant::UI::Prefixed do subject { described_class.new(ui, prefix) } + describe "#ask" do + it "does not request bolding" do + ui.should_receive(:ask).with(" #{prefix}: foo", bold: false) + subject.ask("foo") + end + end + describe "#detail" do it "prefixes with spaces and the message" do ui.should_receive(:safe_puts).with(" #{prefix}: foo", anything) diff --git a/test/unit/vagrant/util/downloader_test.rb b/test/unit/vagrant/util/downloader_test.rb index 1f73d57b8..6be2ee7f9 100644 --- a/test/unit/vagrant/util/downloader_test.rb +++ b/test/unit/vagrant/util/downloader_test.rb @@ -54,4 +54,22 @@ describe Vagrant::Util::Downloader do pending "tests for a UI" end end + + describe "#head" do + let(:curl_options) { + ["--fail", "--location", "--max-redirs", "10", "--user-agent", described_class::USER_AGENT, source, {}] + } + + it "returns the output" do + subprocess_result.stub(stdout: "foo") + + options = curl_options.dup + options.unshift("-I") + + Vagrant::Util::Subprocess.should_receive(:execute). + with("curl", *options).and_return(subprocess_result) + + expect(subject.head).to eq("foo") + end + end end diff --git a/website/docs/Vagrantfile b/website/docs/Vagrantfile new file mode 100644 index 000000000..bab5d6429 --- /dev/null +++ b/website/docs/Vagrantfile @@ -0,0 +1,23 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +$script = <