From 69d9bc0fe85e65ec21ff6ee8d19b9b77da1ef7e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Nov 2015 11:50:10 -0800 Subject: [PATCH 1/3] Environment can_install_provider and install_provider --- lib/vagrant/environment.rb | 26 ++++++++++++ lib/vagrant/plugin/v2/provider.rb | 15 +++++++ test/unit/vagrant/environment_test.rb | 42 ++++++++++++++++++++ test/unit/vagrant/plugin/v2/provider_test.rb | 4 ++ 4 files changed, 87 insertions(+) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 015e4b95f..034d71163 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -383,6 +383,26 @@ module Vagrant raise Errors::NoDefaultProvider end + # Returns whether or not we know how to install the provider with + # the given name. + # + # @return [Boolean] + def can_install_provider?(name) + host.capability?(provider_install_key(name)) + end + + # Installs the provider with the given name. + # + # This will raise an exception if we don't know how to install the + # provider with the given name. You should guard this call with + # `can_install_provider?` for added safety. + # + # An exception will be raised if there are any failures installing + # the provider. + def install_provider(name) + host.capability(provider_install_key(name)) + end + # Returns the collection of boxes for the environment. # # @return [BoxCollection] @@ -883,6 +903,12 @@ module Vagrant nil end + # Returns the key used for the host capability for provider installs + # of the given name. + def provider_install_key(name) + "provider_install_#{name}".to_sym + 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 diff --git a/lib/vagrant/plugin/v2/provider.rb b/lib/vagrant/plugin/v2/provider.rb index 0ea7a1f13..3e96d86e1 100644 --- a/lib/vagrant/plugin/v2/provider.rb +++ b/lib/vagrant/plugin/v2/provider.rb @@ -24,6 +24,21 @@ module Vagrant true end + # This is called early, before a machine is instantiated, to check + # if this provider is installed. This should return true or false. + # + # If the provider is not installed and Vagrant determines it is + # able to install this provider, then it will do so. Installation + # is done by calling Environment.install_provider. + # + # If Environment.can_install_provider? returns false, then an error + # will be shown to the user. + def self.installed? + # By default return true for backwards compat so all providers + # continue to work. + true + end + # Initialize the provider to represent the given machine. # # @param [Vagrant::Machine] machine The machine that this provider diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index c5cf8370d..6a595fb57 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -25,6 +25,48 @@ describe Vagrant::Environment do let(:instance) { env.create_vagrant_env } subject { instance } + describe "#can_install_provider?" do + let(:plugin_hosts) { {} } + let(:plugin_host_caps) { {} } + + before do + m = Vagrant.plugin("2").manager + m.stub(hosts: plugin_hosts) + m.stub(host_capabilities: plugin_host_caps) + + # Detect the host + env.vagrantfile <<-VF + Vagrant.configure("2") do |config| + config.vagrant.host = nil + end + VF + + # Setup the foo host by default + plugin_hosts[:foo] = [detect_class(true), nil] + end + + it "should return whether it can install or not" do + plugin_host_caps[:foo] = { provider_install_foo: Class } + + expect(subject.can_install_provider?(:foo)).to be_true + expect(subject.can_install_provider?(:bar)).to be_false + end + end + + describe "#install_provider" do + let(:host) { double(:host) } + + before do + allow(subject).to receive(:host).and_return(host) + end + + it "should install the correct provider" do + expect(host).to receive(:capability).with(:provider_install_foo) + + subject.install_provider(:foo) + end + end + describe "#home_path" do it "is set to the home path given" do Dir.mktmpdir do |dir| diff --git a/test/unit/vagrant/plugin/v2/provider_test.rb b/test/unit/vagrant/plugin/v2/provider_test.rb index c27274db1..634b311c6 100644 --- a/test/unit/vagrant/plugin/v2/provider_test.rb +++ b/test/unit/vagrant/plugin/v2/provider_test.rb @@ -12,6 +12,10 @@ describe Vagrant::Plugin::V2::Provider do expect(described_class).to be_usable end + it "should be installed by default" do + expect(described_class).to be_installed + end + it "should return nil by default for actions" do expect(instance.action(:whatever)).to be_nil end From baea923e9cf3327336ad896f5024541339e3b599 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Nov 2015 13:58:15 -0800 Subject: [PATCH 2/3] commands/up: automatically install providers --- lib/vagrant/environment.rb | 2 + plugins/commands/up/command.rb | 85 +++++++++++++++++++----- plugins/providers/virtualbox/provider.rb | 9 +++ templates/locales/en.yml | 7 ++ 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 034d71163..88779a2e9 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -309,6 +309,7 @@ module Vagrant def default_provider(**opts) opts[:exclude] = Set.new(opts[:exclude]) if opts[:exclude] opts[:force_default] = true if !opts.key?(:force_default) + opts[:check_usable] = true if !opts.key?(:check_usable) default = ENV["VAGRANT_DEFAULT_PROVIDER"] default = nil if default == "" @@ -376,6 +377,7 @@ module Vagrant # Find the matching implementation ordered.each do |_, key, impl, _| + return key if !opts[:check_usable] return key if impl.usable?(false) end diff --git a/plugins/commands/up/command.rb b/plugins/commands/up/command.rb index 826bea0ae..b93e859cf 100644 --- a/plugins/commands/up/command.rb +++ b/plugins/commands/up/command.rb @@ -1,4 +1,5 @@ require 'optparse' +require 'set' require "vagrant" @@ -16,6 +17,7 @@ module VagrantPlugins def execute options = {} options[:destroy_on_error] = true + options[:install_provider] = true options[:parallel] = true options[:provision_ignore_sentinel] = false @@ -41,6 +43,11 @@ module VagrantPlugins "Back the machine with a specific provider") do |provider| options[:provider] = provider end + + o.on("--[no-]install-provider", + "If possible, install the provider if it isn't installed") do |p| + options[:install_provider] = p + end end # Parse the options @@ -53,24 +60,32 @@ module VagrantPlugins # Go over each VM and bring it up @logger.debug("'Up' each target VM...") - # Build up the batch job of what we'll do - machines = [] - @env.batch(options[:parallel]) do |batch| - names = argv - if names.empty? - autostart = false - @env.vagrantfile.machine_names_and_options.each do |n, o| - autostart = true if o.key?(:autostart) - o[:autostart] = true if !o.key?(:autostart) - names << n.to_s if o[:autostart] - end - - # If we have an autostart key but no names, it means that - # all machines are autostart: false and we don't start anything. - names = nil if autostart && names.empty? + # Get the names of the machines we want to bring up + names = argv + if names.empty? + autostart = false + @env.vagrantfile.machine_names_and_options.each do |n, o| + autostart = true if o.key?(:autostart) + o[:autostart] = true if !o.key?(:autostart) + names << n.to_s if o[:autostart] end - if names + # If we have an autostart key but no names, it means that + # all machines are autostart: false and we don't start anything. + names = nil if autostart && names.empty? + end + + # Build up the batch job of what we'll do + machines = [] + if names + # If we're installing providers, then do that. We don't + # parallelize this step because it is likely the same provider + # anyways. + if options[:install_provider] + install_providers(names) + end + + @env.batch(options[:parallel]) do |batch| with_target_vms(names, provider: options[:provider]) do |machine| @env.ui.info(I18n.t( "vagrant.commands.up.upping", @@ -106,6 +121,44 @@ module VagrantPlugins # Success, exit status 0 0 end + + protected + + def install_providers(names) + # First create a set of all the providers we need to check for. + # Most likely this will be a set of one. + providers = Set.new + names.each do |name| + providers.add(@env.default_provider(machine: name.to_sym, check_usable: false)) + end + + # Go through and determine if we can install the providers + providers.delete_if do |name| + !@env.can_install_provider?(name) + end + + # Install the providers if we have to + providers.each do |name| + # Find the provider. Ignore if we can't find it, this error + # will pop up later in the process. + parts = Vagrant.plugin("2").manager.providers[name] + next if !parts + + # If the provider is already installed, then our work here is done + cls = parts[0] + next if cls.installed? + + # Some human-friendly output + ui = Vagrant::UI::Prefixed.new(@env.ui, "") + ui.output(I18n.t( + "vagrant.installing_provider", + provider: name.to_s)) + ui.detail(I18n.t("vagrant.installing_provider_detail")) + + # Install the provider + @env.install_provider(name) + end + end end end end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 969fb4718..6bde344d2 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -5,6 +5,15 @@ module VagrantPlugins class Provider < Vagrant.plugin("2", :provider) attr_reader :driver + def self.installed? + Driver::Meta.new + true + rescue Vagrant::Errors::VirtualBoxInvalidVersion + return false + rescue Vagrant::Errors::VirtualBoxNotDetected + return false + end + def self.usable?(raise_error=false) # Instantiate the driver, which will determine the VirtualBox # version and all that, which checks for VirtualBox being present diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2bafd0f55..3b7094310 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -154,6 +154,13 @@ en: Inserting generated public key within guest... inserting_remove_key: |- Removing insecure key from the guest if it's present... + installing_provider: |- + Provider '%{provider}' not found. We'll automatically install it now... + installing_provider_detail: |- + The installation process will start below. Human interaction may be + required at some points. If you're uncomfortable with automatically + installing this provider, you can safely Ctrl-C this process and install + it manually. list_commands: |- Below is a listing of all available Vagrant commands and a brief description of what they do. From c017340f699a199a7043817788cb9f7abecac37f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Nov 2015 13:59:08 -0800 Subject: [PATCH 3/3] website: update docs for --install-provider flag --- website/docs/source/v2/cli/up.html.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/source/v2/cli/up.html.md b/website/docs/source/v2/cli/up.html.md index a0408a9a6..44d320669 100644 --- a/website/docs/source/v2/cli/up.html.md +++ b/website/docs/source/v2/cli/up.html.md @@ -20,6 +20,10 @@ on a day-to-day basis. unexpected error occurs. This will only happen on the first `vagrant up`. By default this is set. +* `--[no-]install-provider` - If the requested provider is not installed, + Vagrant will attempt to automatically install it if it can. By default this + is enabled. + * `--[no-]parallel` - Bring multiple machines up in parallel if the provider supports it. Please consult the provider documentation to see if this feature is supported.