From e537e02d9d829e71a0f818a8659a805b93d9de14 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Jan 2014 14:50:55 -0800 Subject: [PATCH] core: BoxCheckOutdated can check if a box is outdated --- lib/vagrant/action.rb | 9 + .../action/builtin/box_check_outdated.rb | 65 +++++++ lib/vagrant/errors.rb | 8 + plugins/commands/box/command/outdated.rb | 29 +++- templates/locales/en.yml | 15 ++ .../action/builtin/box_check_outdated_test.rb | 159 ++++++++++++++++++ 6 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 lib/vagrant/action/builtin/box_check_outdated.rb create mode 100644 test/unit/vagrant/action/builtin/box_check_outdated_test.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 627be5ab2..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" @@ -45,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_check_outdated.rb b/lib/vagrant/action/builtin/box_check_outdated.rb new file mode 100644 index 000000000..c33e02605 --- /dev/null +++ b/lib/vagrant/action/builtin/box_check_outdated.rb @@ -0,0 +1,65 @@ +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" + +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 !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 + + if !machine.box.metadata_url + # This box doesn't have a metadata URL, so we can't + # possibly check the version information. + raise Errors::BoxOutdatedNoMetadata, name: machine.box.name + end + + md = machine.box.load_metadata + newer = md.version( + "> #{machine.box.version}", provider: machine.box.provider) + if !newer + env[:ui].success(I18n.t( + "vagrant.box_up_to_date_single", + name: machine.box.name, + version: machine.box.version)) + + env[:box_outdated] = false + return @app.call(env) + end + + env[:ui].warn(I18n.t( + "vagrant.box_outdated_single", + name: machine.box.name, + current: machine.box.version, + latest: newer.version)) + env[:box_outdated] = true + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index e42a7d402..516caea49 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -172,6 +172,14 @@ module Vagrant error_key(:box_metadata_malformed) end + class BoxOutdatedNoBox < VagrantError + error_key(:box_outdated_no_box) + end + + class BoxOutdatedNoMetadata < VagrantError + error_key(:box_outdated_no_metadata) + end + class BoxProviderDoesntMatch < VagrantError error_key(:box_provider_doesnt_match) end diff --git a/plugins/commands/box/command/outdated.rb b/plugins/commands/box/command/outdated.rb index 818a478a9..00bb0c848 100644 --- a/plugins/commands/box/command/outdated.rb +++ b/plugins/commands/box/command/outdated.rb @@ -5,10 +5,34 @@ module VagrantPlugins module Command class Outdated < Vagrant.plugin("2", :command) def execute - OptionParser.new do |o| + options = {} + + opts = OptionParser.new do |o| o.banner = "Usage: vagrant box outdated" + o.separator "" + + o.on("--global", "Check all boxes installed.") do |g| + options[:global] = g + end end + argv = parse_options(opts) + + # 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_refresh: true, + machine: machine, + }) + end + end + + def outdated_global boxes = {} @env.boxes.all.reverse.each do |name, version, provider| next if boxes[name] @@ -49,9 +73,6 @@ module VagrantPlugins latest: latest.to_s,)) end end - - # Success, exit status 0 - 0 end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 43398e490..1a0c7d7df 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -30,12 +30,18 @@ en: Loading metadata for box '%{name}' box_outdated: |- * '%{name}' is outdated! Current: %{current}. Latest: %{latest} + 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_up_to_date: |- * '%{name}' (v%{version}) is up to date + box_up_to_date_single: |- + Your box '%{name}' (v%{version}) is running the latest version. cfengine_bootstrapping: |- Bootstrapping CFEngine with policy server: %{policy_server}... cfengine_bootstrapping_policy_hub: |- @@ -357,6 +363,15 @@ en: that this issue can be fixed. %{error} + box_outdated_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. + 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. 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..651134b6c --- /dev/null +++ b/test/unit/vagrant/action/builtin/box_check_outdated_test.rb @@ -0,0 +1,159 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::BoxCheckOutdated do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + machine: machine, + } } + + 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) + end + let(:machine) { iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy) } + + 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 "box with no metadata_url" do + let(:box) do + box_dir = iso_env.box3("foo", "1.0", :virtualbox) + Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) + end + + before do + machine.stub(box: box) + end + + it "raises an exception" do + app.should_receive(:call).never + + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::BoxOutdatedNoMetadata) + end + end + + context "with a metadata URL" do + let(:metadata_url) do + Tempfile.new("vagrant").tap do |f| + f.close + end + end + + let(:box_dir) { iso_env.box3("foo", "1.0", :virtualbox) } + + it "isn't outdated if it isn't" do + File.open(metadata_url.path, "w") do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "1.0", + "providers": [ + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + } + ] + } + ] + } + RAW + end + + box = Vagrant::Box.new( + "foo", :virtualbox, "1.0", box_dir, metadata_url: metadata_url.path) + machine.stub(box: box) + + subject.call(env) + + expect(env[:box_outdated]).to be_false + end + + it "is outdated if it is" do + File.open(metadata_url.path, "w") do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.5", + "providers": [ + { + "name": "virtualbox", + "url": "#{iso_env.box2_file(:virtualbox)}" + } + ] + } + ] + } + RAW + end + + box = Vagrant::Box.new( + "foo", :virtualbox, "1.0", box_dir, metadata_url: metadata_url.path) + machine.stub(box: box) + + subject.call(env) + + expect(env[:box_outdated]).to be_true + end + + it "isn't outdated if the newer box is for another provider" do + File.open(metadata_url.path, "w") do |f| + f.write(<<-RAW) + { + "name": "foo/bar", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.5", + "providers": [ + { + "name": "vmware", + "url": "#{iso_env.box2_file(:vmware)}" + } + ] + } + ] + } + RAW + end + + box = Vagrant::Box.new( + "foo", :virtualbox, "1.0", box_dir, metadata_url: metadata_url.path) + machine.stub(box: box) + + subject.call(env) + + expect(env[:box_outdated]).to be_false + end + end +end