diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index 5adfa80ce..f48020d24 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -1,6 +1,7 @@ require "digest/sha1" require "log4r" +require "vagrant/box_metadata" require "vagrant/util/downloader" require "vagrant/util/file_checksum" require "vagrant/util/platform" @@ -19,6 +20,120 @@ module Vagrant def call(env) @download_interrupted = false + provider = env[:box_provider] + url = env[:box_url] + version = env[:box_version] + + metadata = nil + if File.file?(url) + # TODO: What if file isn't valid JSON + # TODO: What if file is old-style box + # TODO: What if URL is in the "file:" format + File.open(url) do |f| + metadata = BoxMetadata.new(f) + end + end + + # TODO: provider that is in an earlier version + # TODO: multiple providers + metadata_version = metadata.version(version || ">= 0") + if !metadata_version + raise Errors::BoxAddNoMatchingVersion, + constraints: version || ">= 0", + url: url, + versions: metadata.versions.join(", ") + end + + metadata_provider = nil + if provider + # If a provider was specified, make sure we get that specific + # version. + metadata_provider = metadata_version.provider(provider) + if !metadata_provider + raise Errors::BoxAddNoMatchingProvider, + providers: metadata_version.providers.join(", "), + requested: provider, + url: url + 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) + end + + # Now we have a URL, we have to download this URL. + box_url = download(metadata_provider.url, env) + + # Add the box! + env[:box_collection].add( + box_url, metadata.name, metadata_version.version) + + @app.call(env) + end + + def download(url, env) + temp_path = env[:tmp_path].join("box" + Digest::SHA1.hexdigest(url)) + @logger.info("Downloading box: #{url} => #{temp_path}") + + if File.file?(url) || url !~ /^[a-z0-9]+:.*$/i + @logger.info("URL is a file or protocol not found and assuming file.") + file_path = File.expand_path(url) + file_path = Util::Platform.cygwin_windows_path(file_path) + 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? + delete = false + if env[:box_clean] + @logger.info("Cleaning existing temp box file.") + delete = true + elsif temp_path.mtime.to_i < (Time.now.to_i - 6 * 60 * 60) + @logger.info("Existing temp file is too old. Removing.") + delete = true + end + + temp_path.unlink if delete + end + + # 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")) + end + + begin + downloader = Util::Downloader.new(url, temp_path, downloader_options) + downloader.download! + rescue Errors::DownloaderInterrupted + # The downloader was interrupted, so just return, because that + # means we were interrupted as well. + @download_interrupted = true + 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? + raise + end + + temp_path + end +=begin + @download_interrupted = false + box_name = env[:box_name] box_formats = env[:box_provider] if box_formats @@ -209,6 +324,7 @@ module Vagrant f.write(JSON.dump(info)) end end +=end end end end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 099c925a4..36d278a69 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -120,6 +120,14 @@ module Vagrant error_key(:batch_multi_error) end + class BoxAddNoMatchingProvider < VagrantError + error_key(:box_add_no_matching_provider) + end + + class BoxAddNoMatchingVersion < VagrantError + error_key(:box_add_no_matching_version) + end + class BoxAlreadyExists < VagrantError error_key(:already_exists, "vagrant.actions.box.unpackage") end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index dbc2eedfa..5106aab2c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -211,6 +211,23 @@ 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. + + Box: %{url} + Requested provider: %{requested} + Available providers: %{providers} + 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: %{url} + Constraints: %{constraints} + Available versions: %{versions} 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 diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index 659c90665..19a850b5f 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -1,8 +1,256 @@ +require "digest/sha1" +require "pathname" +require "tempfile" +require "tmpdir" + 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) { {} } + 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 } + + # Helper to quickly SHA1 checksum a path + def checksum(path) + FileChecksum.new(path, Digest::SHA1).checksum + end + + context "with box metadata" do + 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| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + true + end + + 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| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + true + end + + app.should_receive(:call).with(env) + + subject.call(env) + 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| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.5") + true + end + + app.should_receive(:call).with(env) + + subject.call(env) + 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| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.5") + true + end + + app.should_receive(:call).with(env) + + subject.call(env) + 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 + end end