From 3b82f2efc4604b608816df9945c62bbff3eac187 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 14 Jul 2012 16:57:54 -0700 Subject: [PATCH 01/60] Create the basic provider plugin interface. Non-functional at this point. --- lib/vagrant.rb | 1 + lib/vagrant/plugin/v1.rb | 1 + lib/vagrant/plugin/v1/provider.rb | 11 +++++++++++ test/unit/vagrant_test.rb | 1 + 4 files changed, 14 insertions(+) create mode 100644 lib/vagrant/plugin/v1/provider.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 747214e85..82ed04d27 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -91,6 +91,7 @@ module Vagrant c.register([:"1", :config]) { Plugin::V1::Config } c.register([:"1", :guest]) { Plugin::V1::Guest } c.register([:"1", :host]) { Plugin::V1::Host } + c.register([:"1", :provider]) { Plugin::V1::Provider } c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } # Returns a `Vagrant::Registry` object that contains all the built-in diff --git a/lib/vagrant/plugin/v1.rb b/lib/vagrant/plugin/v1.rb index 6c4c6d339..c43e8d687 100644 --- a/lib/vagrant/plugin/v1.rb +++ b/lib/vagrant/plugin/v1.rb @@ -10,6 +10,7 @@ module Vagrant autoload :Guest, "vagrant/plugin/v1/guest" autoload :Host, "vagrant/plugin/v1/host" autoload :Plugin, "vagrant/plugin/v1/plugin" + autoload :Provider, "vagrant/plugin/v1/provider" autoload :Provisioner, "vagrant/plugin/v1/provisioner" end end diff --git a/lib/vagrant/plugin/v1/provider.rb b/lib/vagrant/plugin/v1/provider.rb new file mode 100644 index 000000000..f4650af9b --- /dev/null +++ b/lib/vagrant/plugin/v1/provider.rb @@ -0,0 +1,11 @@ +module Vagrant + module Plugin + module V1 + # This is the base class for a provider for the V1 API. A provider + # is responsible for creating compute resources to match the needs + # of a Vagrant-configured system. + class Provider + end + end + end +end diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index 4985761fa..6e555af1f 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -16,6 +16,7 @@ describe Vagrant do described_class.plugin("1", :config).should == Vagrant::Plugin::V1::Config described_class.plugin("1", :guest).should == Vagrant::Plugin::V1::Guest described_class.plugin("1", :host).should == Vagrant::Plugin::V1::Host + described_class.plugin("1", :provider).should == Vagrant::Plugin::V1::Provider described_class.plugin("1", :provisioner).should == Vagrant::Plugin::V1::Provisioner end end From 436da57cc47c2fed6111b9f62dfe330a031cafff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 14 Jul 2012 17:04:06 -0700 Subject: [PATCH 02/60] Add the #action API to the provider plugin --- lib/vagrant/plugin/v1/provider.rb | 8 ++++++++ test/unit/vagrant/plugin/v1/provider_test.rb | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 test/unit/vagrant/plugin/v1/provider_test.rb diff --git a/lib/vagrant/plugin/v1/provider.rb b/lib/vagrant/plugin/v1/provider.rb index f4650af9b..df974cfd8 100644 --- a/lib/vagrant/plugin/v1/provider.rb +++ b/lib/vagrant/plugin/v1/provider.rb @@ -5,6 +5,14 @@ module Vagrant # is responsible for creating compute resources to match the needs # of a Vagrant-configured system. class Provider + # This should return an action callable for the given name. + # + # @param [Symbol] name Name of the action. + # @return [Object] A callable action sequence object, whether it + # is a proc, object, etc. + def action(name) + nil + end end end end diff --git a/test/unit/vagrant/plugin/v1/provider_test.rb b/test/unit/vagrant/plugin/v1/provider_test.rb new file mode 100644 index 000000000..578715e49 --- /dev/null +++ b/test/unit/vagrant/plugin/v1/provider_test.rb @@ -0,0 +1,9 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Plugin::V1::Provider do + let(:instance) { described_class.new } + + it "should return nil by default for actions" do + instance.action(:whatever).should be_nil + end +end From 3519bf0372421db7ce722bc83e74730f1750d080 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jul 2012 11:17:58 -0700 Subject: [PATCH 03/60] Add the "provider" API to the V1 plugin. --- lib/vagrant/plugin/v1/plugin.rb | 13 ++++++++++ plugins/providers/virtualbox/plugin.rb | 18 ++++++++++++++ plugins/providers/virtualbox/provider.rb | 6 +++++ test/unit/vagrant/plugin/v1/plugin_test.rb | 28 ++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 plugins/providers/virtualbox/plugin.rb create mode 100644 plugins/providers/virtualbox/provider.rb diff --git a/lib/vagrant/plugin/v1/plugin.rb b/lib/vagrant/plugin/v1/plugin.rb index decc7aaae..abb503549 100644 --- a/lib/vagrant/plugin/v1/plugin.rb +++ b/lib/vagrant/plugin/v1/plugin.rb @@ -181,6 +181,19 @@ module Vagrant data[:hosts] end + # Registers additional providers to be available. + # + # @param [Symbol] name Name of the provider. + def self.provider(name=UNSET_VALUE, &block) + data[:providers] ||= Registry.new + + # Register a new provider class only if a name was given + data[:providers].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:providers] + end + # Registers additional provisioners to be available. # # @param [String] name Name of the provisioner. diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb new file mode 100644 index 000000000..487bc4ec8 --- /dev/null +++ b/plugins/providers/virtualbox/plugin.rb @@ -0,0 +1,18 @@ +require "vagrant" + +module VagrantPlugins + module ProviderVirtualBox + class Plugin < Vagrant.plugin("1") + name "virtualbox provider" + description <<-EOF + The VirtualBox provider allows Vagrant to manage and control + VirtualBox-based virtual machines. + EOF + + provider("virtualbox") do + require File.expand_path("../provider", __FILE__) + Provider + end + end + end +end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb new file mode 100644 index 000000000..8cbd51f74 --- /dev/null +++ b/plugins/providers/virtualbox/provider.rb @@ -0,0 +1,6 @@ +module VagrantPlugins + module ProviderVirtualBox + class Provider < Vagrant.plugin("1", :provider) + end + end +end diff --git a/test/unit/vagrant/plugin/v1/plugin_test.rb b/test/unit/vagrant/plugin/v1/plugin_test.rb index e79f75b9a..6a20f14b8 100644 --- a/test/unit/vagrant/plugin/v1/plugin_test.rb +++ b/test/unit/vagrant/plugin/v1/plugin_test.rb @@ -195,6 +195,34 @@ describe Vagrant::Plugin::V1::Plugin do end end + describe "providers" do + it "should register provider classes" do + plugin = Class.new(described_class) do + provider("foo") { "bar" } + end + + plugin.provider[:foo].should == "bar" + end + + it "should lazily register provider classes" do + # Below would raise an error if the value of the config class was + # evaluated immediately. By asserting that this does not raise an + # error, we verify that the value is actually lazily loaded + plugin = nil + expect { + plugin = Class.new(described_class) do + provider("foo") { raise StandardError, "FAIL!" } + end + }.to_not raise_error + + # Now verify when we actually get the configuration key that + # a proper error is raised. + expect { + plugin.provider[:foo] + }.to raise_error(StandardError) + end + end + describe "provisioners" do it "should register provisioner classes" do plugin = Class.new(described_class) do From 353610021c54728a8877a8abd464bb67a1b74277 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Jul 2012 10:28:42 -0700 Subject: [PATCH 04/60] Vagrant::Machine This is the class that will represent a machine that is managed by Vagrant. The class has a number of attributes associated with it and is meant to be a single API for managing the machines. --- lib/vagrant.rb | 1 + lib/vagrant/machine.rb | 44 +++++++++++++++++++++++++++++++ test/unit/vagrant/machine_test.rb | 31 ++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 lib/vagrant/machine.rb create mode 100644 test/unit/vagrant/machine_test.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 82ed04d27..5fa92e82b 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -76,6 +76,7 @@ module Vagrant autoload :Errors, 'vagrant/errors' autoload :Guest, 'vagrant/guest' autoload :Hosts, 'vagrant/hosts' + autoload :Machine, 'vagrant/machine' autoload :Plugin, 'vagrant/plugin' autoload :SSH, 'vagrant/ssh' autoload :TestHelpers, 'vagrant/test_helpers' diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb new file mode 100644 index 000000000..33e140ef2 --- /dev/null +++ b/lib/vagrant/machine.rb @@ -0,0 +1,44 @@ +require "log4r" + +module Vagrant + # This represents a machine that Vagrant manages. This provides a singular + # API for querying the state and making state changes to the machine, which + # is backed by any sort of provider (VirtualBox, VMWare, etc.). + class Machine + # The box that is backing this machine. + # + # @return [Box] + attr_reader :box + + # Configuration for the machine. + # + # @return [Object] + attr_reader :config + + # The environment that this machine is a part of. + # + # @return [Environment] + attr_reader :env + + # Name of the machine. This is assigned by the Vagrantfile. + # + # @return [String] + attr_reader :name + + # Initialize a new machine. + # + # @param [String] name Name of the virtual machine. + # @param [Object] provider The provider backing this machine. This is + # currently expected to be a V1 `provider` plugin. + # @param [Object] config The configuration for this machine. + # @param [Box] box The box that is backing this virtual machine. + # @param [Environment] env The environment that this machine is a + # part of. + def initialize(name, provider, config, box, env) + @name = name + @config = config + @box = box + @env = env + end + end +end diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb new file mode 100644 index 000000000..034b695b1 --- /dev/null +++ b/test/unit/vagrant/machine_test.rb @@ -0,0 +1,31 @@ +require File.expand_path("../../base", __FILE__) + +describe Vagrant::Machine do + include_context "unit" + + let(:name) { "foo" } + let(:provider) { Object.new } + let(:box) { Object.new } + let(:config) { Object.new } + let(:environment) { isolated_environment } + + let(:instance) { described_class.new(name, provider, config, box, environment) } + + describe "attributes" do + it "should provide access to the name" do + instance.name.should == name + end + + it "should provide access to the configuration" do + instance.config.should eql(config) + end + + it "should provide access to the box" do + instance.box.should eql(box) + end + + it "should provide access to the environment" do + instance.env.should eql(environment) + end + end +end From 8fc5591b8ec3cb234fa55af127bd5f0df4949034 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Jul 2012 10:56:39 -0700 Subject: [PATCH 05/60] Machine queries state from the provider --- lib/vagrant/machine.rb | 21 +++++++++++----- test/unit/vagrant/machine_test.rb | 40 +++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 33e140ef2..534f5b5a5 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -28,17 +28,26 @@ module Vagrant # Initialize a new machine. # # @param [String] name Name of the virtual machine. - # @param [Object] provider The provider backing this machine. This is + # @param [Class] provider The provider backing this machine. This is # currently expected to be a V1 `provider` plugin. # @param [Object] config The configuration for this machine. # @param [Box] box The box that is backing this virtual machine. # @param [Environment] env The environment that this machine is a # part of. - def initialize(name, provider, config, box, env) - @name = name - @config = config - @box = box - @env = env + def initialize(name, provider_cls, config, box, env) + @name = name + @box = box + @config = config + @env = env + @provider = provider_cls.new(self) + end + + # Returns the state of this machine. The state is queried from the + # backing provider, so it can be any arbitrary symbol. + # + # @return [Symbol] + def state + @provider.state end end end diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 034b695b1..2895e8ab7 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -4,12 +4,39 @@ describe Vagrant::Machine do include_context "unit" let(:name) { "foo" } - let(:provider) { Object.new } + let(:provider) { double("provider") } + let(:provider_cls) do + obj = double("provider_cls") + obj.stub(:new => provider) + obj + end let(:box) { Object.new } let(:config) { Object.new } let(:environment) { isolated_environment } - let(:instance) { described_class.new(name, provider, config, box, environment) } + let(:instance) { described_class.new(name, provider_cls, config, box, environment) } + + describe "initialization" do + it "should initialize the provider with the machine object" do + received_machine = nil + + provider_cls = double("provider_cls") + provider_cls.should_receive(:new) do |machine| + # Store this for later so we can verify that it is the + # one we expected to receive. + received_machine = machine + + # Verify the machine is fully ready to be used. + machine.name.should == name + machine.config.should eql(config) + machine.box.should eql(box) + machine.env.should eql(environment) + end + + instance = described_class.new(name, provider_cls, config, box, environment) + received_machine.should eql(instance) + end + end describe "attributes" do it "should provide access to the name" do @@ -28,4 +55,13 @@ describe Vagrant::Machine do instance.env.should eql(environment) end end + + describe "state" do + it "should query state from the provider" do + state = :running + + provider.should_receive(:state).and_return(state) + instance.state.should == state + end + end end From 8f0375d7f3e94bcfb3ea97c457afb97113f73586 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Jul 2012 14:12:58 -0700 Subject: [PATCH 06/60] Machines can call actions on their providers. --- lib/vagrant/errors.rb | 4 +++ lib/vagrant/machine.rb | 20 ++++++++++++++ templates/locales/en.yml | 5 ++++ test/unit/vagrant/machine_test.rb | 43 +++++++++++++++++++++++++++---- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 0b57d7c45..6097de946 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -376,6 +376,10 @@ module Vagrant error_key(:ui_expects_tty) end + class UnimplementedProviderAction < VagrantError + error_key(:unimplemented_provider_action) + end + class VagrantInterrupt < VagrantError status_code(40) error_key(:interrupted) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 534f5b5a5..e807a65a1 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -42,6 +42,26 @@ module Vagrant @provider = provider_cls.new(self) end + # This calls an action on the provider. The provider may or may not + # actually implement the action. + # + # @param [Symbol] name Name of the action to run. + def action(name) + # Get the callable from the provider. + callable = @provider.action(name) + + # If this action doesn't exist on the provider, then an exception + # must be raised. + if callable.nil? + raise Errors::UnimplementedProviderAction, + :action => name, + :provider => @provider.to_s + end + + # Run the action with the action runner on the environment + @env.action_runner.run(callable, :machine => self) + end + # Returns the state of this machine. The state is queried from the # backing provider, so it can be any arbitrary symbol. # diff --git a/templates/locales/en.yml b/templates/locales/en.yml index d5325b4cd..de3d55e31 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -182,6 +182,11 @@ en: a TTY. Most actions in Vagrant that require a TTY have configuration switches to disable this requirement. Please do that or run Vagrant with TTY. + unimplemented_provider_action: |- + Vagrant attempted to call the action '%{action}' on the provider + '%{provider}', but this provider doesn't support this action. This + is probably a bug in either the provider or the plugin calling this + action, and should be reported. vagrantfile_exists: |- `Vagrantfile` already exists in this directory. Remove it before running `vagrant init`. diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 2895e8ab7..f941c349b 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -12,9 +12,10 @@ describe Vagrant::Machine do end let(:box) { Object.new } let(:config) { Object.new } - let(:environment) { isolated_environment } + let(:env) { test_env.create_vagrant_env } + let(:test_env) { isolated_environment } - let(:instance) { described_class.new(name, provider_cls, config, box, environment) } + let(:instance) { described_class.new(name, provider_cls, config, box, env) } describe "initialization" do it "should initialize the provider with the machine object" do @@ -30,10 +31,10 @@ describe Vagrant::Machine do machine.name.should == name machine.config.should eql(config) machine.box.should eql(box) - machine.env.should eql(environment) + machine.env.should eql(env) end - instance = described_class.new(name, provider_cls, config, box, environment) + instance = described_class.new(name, provider_cls, config, box, env) received_machine.should eql(instance) end end @@ -52,7 +53,39 @@ describe Vagrant::Machine do end it "should provide access to the environment" do - instance.env.should eql(environment) + instance.env.should eql(env) + end + end + + describe "actions" do + it "should be able to run an action that exists" do + action_name = :up + called = false + callable = lambda { |_env| called = true } + + provider.should_receive(:action).with(action_name).and_return(callable) + instance.action(:up) + called.should be + end + + it "should provide the machine in the environment" do + action_name = :up + machine = nil + callable = lambda { |env| machine = env[:machine] } + + provider.stub(:action).with(action_name).and_return(callable) + instance.action(:up) + + machine.should eql(instance) + end + + it "should raise an exception if the action is not implemented" do + action_name = :up + + provider.stub(:action).with(action_name).and_return(nil) + + expect { instance.action(action_name) }. + to raise_error(Vagrant::Errors::UnimplementedProviderAction) end end From aef2c5f48e3e8e854f312c65a6c5dbb0a8a73b84 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Jul 2012 14:21:04 -0700 Subject: [PATCH 07/60] Logging statements to Vagrant::Machine --- lib/vagrant/machine.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index e807a65a1..78a74b2d3 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -35,6 +35,11 @@ module Vagrant # @param [Environment] env The environment that this machine is a # part of. def initialize(name, provider_cls, config, box, env) + @logger = Log4r::Logger.new("vagrant::machine") + @logger.debug("Initializing machine: #{name}") + @logger.debug(" - Provider: #{provider_cls}") + @logger.debug(" - Box: #{box}") + @name = name @box = box @config = config @@ -47,6 +52,8 @@ module Vagrant # # @param [Symbol] name Name of the action to run. def action(name) + @logger.debug("Calling action: #{name} on provider #{@provider}") + # Get the callable from the provider. callable = @provider.action(name) From 912998ef31132506257ebb6e92e406adf55cd92b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Jul 2012 15:24:51 -0700 Subject: [PATCH 08/60] Fill in the provider API a bit more to what it is. --- lib/vagrant/plugin/v1/provider.rb | 15 +++++++++++++++ test/unit/vagrant/plugin/v1/provider_test.rb | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/vagrant/plugin/v1/provider.rb b/lib/vagrant/plugin/v1/provider.rb index df974cfd8..09fe39962 100644 --- a/lib/vagrant/plugin/v1/provider.rb +++ b/lib/vagrant/plugin/v1/provider.rb @@ -5,6 +5,13 @@ module Vagrant # is responsible for creating compute resources to match the needs # of a Vagrant-configured system. class Provider + # Initialize the provider to represent the given machine. + # + # @param [Vagrant::Machine] machine The machine that this provider + # is responsible for. + def initialize(machine) + end + # This should return an action callable for the given name. # # @param [Symbol] name Name of the action. @@ -13,6 +20,14 @@ module Vagrant def action(name) nil end + + # This should return the state of the machine within this provider. + # The state can be any symbol. + # + # @return [Symbol] + def state + nil + end end end end diff --git a/test/unit/vagrant/plugin/v1/provider_test.rb b/test/unit/vagrant/plugin/v1/provider_test.rb index 578715e49..b92bd3385 100644 --- a/test/unit/vagrant/plugin/v1/provider_test.rb +++ b/test/unit/vagrant/plugin/v1/provider_test.rb @@ -1,7 +1,8 @@ require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Provider do - let(:instance) { described_class.new } + let(:machine) { Object.new } + let(:instance) { described_class.new(machine) } it "should return nil by default for actions" do instance.action(:whatever).should be_nil From 44b4b9dfefa5374933014b9dd2653b1af5908b6e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 24 Jul 2012 21:32:38 -0700 Subject: [PATCH 09/60] Move drivers to the VirtualBox plugin. Use Machine class. This starts the transition of replacing VM with Machine. Machine still isn't ready to fully replace VM but by moving it now, I'm able to find the spots that need to be fixed. At this point `vagrant status` works with the new provider interface. --- lib/vagrant/driver.rb | 7 - lib/vagrant/driver/virtualbox.rb | 140 ------ lib/vagrant/driver/virtualbox_4_0.rb | 474 ----------------- lib/vagrant/driver/virtualbox_4_1.rb | 474 ----------------- lib/vagrant/driver/virtualbox_base.rb | 326 ------------ lib/vagrant/environment.rb | 17 +- lib/vagrant/machine.rb | 39 ++ plugins/providers/virtualbox/driver/base.rb | 327 ++++++++++++ plugins/providers/virtualbox/driver/meta.rb | 139 +++++ .../virtualbox/driver/version_4_0.rb | 476 ++++++++++++++++++ .../virtualbox/driver/version_4_1.rb | 476 ++++++++++++++++++ plugins/providers/virtualbox/plugin.rb | 10 +- plugins/providers/virtualbox/provider.rb | 13 + test/unit/vagrant/machine_test.rb | 33 +- 14 files changed, 1524 insertions(+), 1427 deletions(-) delete mode 100644 lib/vagrant/driver.rb delete mode 100644 lib/vagrant/driver/virtualbox.rb delete mode 100644 lib/vagrant/driver/virtualbox_4_0.rb delete mode 100644 lib/vagrant/driver/virtualbox_4_1.rb delete mode 100644 lib/vagrant/driver/virtualbox_base.rb create mode 100644 plugins/providers/virtualbox/driver/base.rb create mode 100644 plugins/providers/virtualbox/driver/meta.rb create mode 100644 plugins/providers/virtualbox/driver/version_4_0.rb create mode 100644 plugins/providers/virtualbox/driver/version_4_1.rb diff --git a/lib/vagrant/driver.rb b/lib/vagrant/driver.rb deleted file mode 100644 index 9f114f80e..000000000 --- a/lib/vagrant/driver.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Vagrant - module Driver - autoload :VirtualBox, 'vagrant/driver/virtualbox' - autoload :VirtualBox_4_0, 'vagrant/driver/virtualbox_4_0' - autoload :VirtualBox_4_1, 'vagrant/driver/virtualbox_4_1' - end -end diff --git a/lib/vagrant/driver/virtualbox.rb b/lib/vagrant/driver/virtualbox.rb deleted file mode 100644 index baf462b87..000000000 --- a/lib/vagrant/driver/virtualbox.rb +++ /dev/null @@ -1,140 +0,0 @@ -require 'forwardable' - -require 'log4r' - -require 'vagrant/driver/virtualbox_base' - -module Vagrant - module Driver - # This class contains the logic to drive VirtualBox. - # - # Read the VirtualBoxBase source for documentation on each method. - class VirtualBox < VirtualBoxBase - # This is raised if the VM is not found when initializing a driver - # with a UUID. - class VMNotFound < StandardError; end - - # We use forwardable to do all our driver forwarding - extend Forwardable - - # The UUID of the virtual machine we represent - attr_reader :uuid - - # The version of virtualbox that is running. - attr_reader :version - - def initialize(uuid=nil) - # Setup the base - super() - - @logger = Log4r::Logger.new("vagrant::driver::virtualbox") - @uuid = uuid - - # Read and assign the version of VirtualBox we know which - # specific driver to instantiate. - begin - @version = read_version || "" - rescue Subprocess::LaunchError - # This means that VirtualBox was not found, so we raise this - # error here. - raise Errors::VirtualBoxNotDetected - end - - # Instantiate the proper version driver for VirtualBox - @logger.debug("Finding driver for VirtualBox version: #{@version}") - driver_map = { - "4.0" => VirtualBox_4_0, - "4.1" => VirtualBox_4_1 - } - - driver_klass = nil - driver_map.each do |key, klass| - if @version.start_with?(key) - driver_klass = klass - break - end - end - - if !driver_klass - supported_versions = driver_map.keys.sort.join(", ") - raise Errors::VirtualBoxInvalidVersion, :supported_versions => supported_versions - end - - @logger.info("Using VirtualBox driver: #{driver_klass}") - @driver = driver_klass.new(@uuid) - - if @uuid - # Verify the VM exists, and if it doesn't, then don't worry - # about it (mark the UUID as nil) - raise VMNotFound if !@driver.vm_exists?(@uuid) - end - end - - def_delegators :@driver, :clear_forwarded_ports, - :clear_shared_folders, - :create_dhcp_server, - :create_host_only_network, - :delete, - :delete_unused_host_only_networks, - :discard_saved_state, - :enable_adapters, - :execute_command, - :export, - :forward_ports, - :halt, - :import, - :read_forwarded_ports, - :read_bridged_interfaces, - :read_guest_additions_version, - :read_host_only_interfaces, - :read_mac_address, - :read_mac_addresses, - :read_machine_folder, - :read_network_interfaces, - :read_state, - :read_used_ports, - :read_vms, - :set_mac_address, - :set_name, - :share_folders, - :ssh_port, - :start, - :suspend, - :verify!, - :verify_image, - :vm_exists? - - protected - - # This returns the version of VirtualBox that is running. - # - # @return [String] - def read_version - # The version string is usually in one of the following formats: - # - # * 4.1.8r1234 - # * 4.1.8r1234_OSE - # * 4.1.8_MacPortsr1234 - # - # Below accounts for all of these. - - # Note: We split this into multiple lines because apparently "".split("_") - # is [], so we have to check for an empty array in between. - output = execute("--version") - if output =~ /vboxdrv kernel module is not loaded/ - raise Errors::VirtualBoxKernelModuleNotLoaded - elsif output =~ /Please install/ - # Check for installation incomplete warnings, for example: - # "WARNING: The character device /dev/vboxdrv does not - # exist. Please install the virtualbox-ose-dkms package and - # the appropriate headers, most likely linux-headers-generic." - raise Errors::VirtualBoxInstallIncomplete - end - - parts = output.split("_") - return nil if parts.empty? - parts[0].split("r")[0] - end - end - end -end diff --git a/lib/vagrant/driver/virtualbox_4_0.rb b/lib/vagrant/driver/virtualbox_4_0.rb deleted file mode 100644 index 23546e2f0..000000000 --- a/lib/vagrant/driver/virtualbox_4_0.rb +++ /dev/null @@ -1,474 +0,0 @@ -require 'log4r' - -require 'vagrant/driver/virtualbox_base' - -module Vagrant - module Driver - # Driver for VirtualBox 4.0.x - class VirtualBox_4_0 < VirtualBoxBase - def initialize(uuid) - super() - - @logger = Log4r::Logger.new("vagrant::driver::virtualbox_4_0") - @uuid = uuid - end - - def clear_forwarded_ports - args = [] - read_forwarded_ports(@uuid).each do |nic, name, _, _| - args.concat(["--natpf#{nic}", "delete", name]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def clear_shared_folders - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if name = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] - execute("sharedfolder", "remove", @uuid, "--name", name) - end - end - end - - def create_dhcp_server(network, options) - execute("dhcpserver", "add", "--ifname", network, - "--ip", options[:dhcp_ip], - "--netmask", options[:netmask], - "--lowerip", options[:dhcp_lower], - "--upperip", options[:dhcp_upper], - "--enable") - end - - def create_host_only_network(options) - # Create the interface - interface = execute("hostonlyif", "create") - name = interface[/^Interface '(.+?)' was successfully created$/, 1] - - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) - - # Return the details - return { - :name => name, - :ip => options[:adapter_ip], - :netmask => options[:netmask], - :dhcp => nil - } - end - - def delete - execute("unregistervm", @uuid, "--delete") - end - - def delete_unused_host_only_networks - networks = [] - execute("list", "hostonlyifs").split("\n").each do |line| - if network_name = line[/^Name:\s+(.+?)$/, 1] - networks << network_name - end - end - - execute("list", "vms").split("\n").each do |line| - if vm_name = line[/^".+?"\s+\{(.+?)\}$/, 1] - info = execute("showvminfo", vm_name, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] - networks.delete(network_name) - end - end - end - end - - networks.each do |name| - # First try to remove any DHCP servers attached. We use `raw` because - # it is okay if this fails. It usually means that a DHCP server was - # never attached. - raw("dhcpserver", "remove", "--ifname", name) - - # Delete the actual host only network interface. - execute("hostonlyif", "remove", name) - end - end - - def discard_saved_state - execute("discardstate", @uuid) - end - - def enable_adapters(adapters) - args = [] - adapters.each do |adapter| - args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) - - if adapter[:bridge] - args.concat(["--bridgeadapter#{adapter[:adapter]}", - adapter[:bridge]]) - end - - if adapter[:hostonly] - args.concat(["--hostonlyadapter#{adapter[:adapter]}", - adapter[:hostonly]]) - end - - if adapter[:mac_address] - args.concat(["--macaddress#{adapter[:adapter]}", - adapter[:mac_address]]) - end - - if adapter[:nic_type] - args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) - end - end - - execute("modifyvm", @uuid, *args) - end - - def execute_command(command) - raw(*command) - end - - def export(path) - # TODO: Progress - execute("export", @uuid, "--output", path.to_s) - end - - def forward_ports(ports) - args = [] - ports.each do |options| - pf_builder = [options[:name], - options[:protocol] || "tcp", - "", - options[:hostport], - "", - options[:guestport]] - - args.concat(["--natpf#{options[:adapter] || 1}", - pf_builder.join(",")]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def halt - execute("controlvm", @uuid, "poweroff") - end - - def import(ovf) - output = "" - total = "" - last = 0 - execute("import", ovf) do |type, data| - if type == :stdout - # Keep track of the stdout so that we can get the VM name - output << data - elsif type == :stderr - # Append the data so we can see the full view - total << data - - # Break up the lines. We can't get the progress until we see an "OK" - lines = total.split("\n") - if lines.include?("OK.") - # The progress of the import will be in the last line. Do a greedy - # regular expression to find what we're looking for. - if current = lines.last[/.+(\d{2})%/, 1] - current = current.to_i - if current > last - last = current - yield current if block_given? - end - end - end - end - end - - # Find the name of the VM name - name = output[/Suggested VM name "(.+?)"/, 1] - if !name - @logger.error("Couldn't find VM name in the output.") - return nil - end - - output = execute("list", "vms") - if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] - return existing_vm - end - - nil - end - - def read_forwarded_ports(uuid=nil, active_only=false) - uuid ||= @uuid - - @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") - - results = [] - current_nic = nil - info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - # This is how we find the nic that a FP is attached to, - # since this comes first. - if nic = line[/^nic(\d+)=".+?"$/, 1] - current_nic = nic.to_i - end - - # If we care about active VMs only, then we check the state - # to verify the VM is running. - if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") - return [] - end - - # Parse out the forwarded port information - if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) - result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] - @logger.debug(" - #{result.inspect}") - results << result - end - end - - results - end - - def read_bridged_interfaces - execute("list", "bridgedifs").split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Return the info to build up the results - info - end - end - - def read_guest_additions_version - output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", - :retryable => true) - if value = output[/^Value: (.+?)$/, 1] - # Split the version by _ since some distro versions modify it - # to look like this: 4.1.2_ubuntu, and the distro part isn't - # too important. - return value.split("_").first - end - - return nil - end - - def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] - info[:network] = network - elsif ip = line[/^IP:\s+(.+?)$/, 1] - info[:ip] = ip - elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] - info[:lower] = lower - elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] - info[:upper] = upper - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - - execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - - info - end - end - - def read_mac_address - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if mac = line[/^macaddress1="(.+?)"$/, 1] - return mac - end - end - - nil - end - - def read_mac_addresses - macs = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - mac = matcher[2].to_s - macs[adapter] = mac - end - end - macs - end - - def read_machine_folder - execute("list", "systemproperties", :retryable => true).split("\n").each do |line| - if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] - return folder - end - end - - nil - end - - def read_network_interfaces - nics = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^nic(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - type = matcher[2].to_sym - - nics[adapter] ||= {} - nics[adapter][:type] = type - elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:hostonly] = network - elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:bridge] = network - end - end - - nics - end - - def read_state - output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - if output =~ /^name=""$/ - return :inaccessible - elsif state = output[/^VMState="(.+?)"$/, 1] - return state.to_sym - end - - nil - end - - def read_used_ports - ports = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if uuid = line[/^".+?" \{(.+?)\}$/, 1] - # Ignore our own used ports - next if uuid == @uuid - - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport - end - end - end - - ports - end - - def read_vms - results = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if vm = line[/^".+?" \{(.+?)\}$/, 1] - results << vm - end - end - - results - end - - def set_mac_address(mac) - execute("modifyvm", @uuid, "--macaddress1", mac) - end - - def set_name(name) - execute("modifyvm", @uuid, "--name", name) - end - - def share_folders(folders) - folders.each do |folder| - args = ["--name", - folder[:name], - "--hostpath", - folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - execute("sharedfolder", "add", @uuid, *args) - end - end - - def ssh_port(expected_port) - @logger.debug("Searching for SSH port: #{expected_port.inspect}") - - # Look for the forwarded port only by comparing the guest port - read_forwarded_ports.each do |_, _, hostport, guestport| - return hostport if guestport == expected_port - end - - nil - end - - def start(mode) - command = ["startvm", @uuid, "--type", mode.to_s] - r = raw(*command) - - if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ - # Some systems return an exit code 1 for some reason. For that - # we depend on the output. - return true - end - - # If we reached this point then it didn't work out. - raise Errors::VBoxManageError, :command => command.inspect - end - - def suspend - execute("controlvm", @uuid, "savestate") - end - - def verify! - # This command sometimes fails if kernel drivers aren't properly loaded - # so we just run the command and verify that it succeeded. - execute("list", "hostonlyifs") - end - - def verify_image(path) - r = raw("import", path.to_s, "--dry-run") - return r.exit_code == 0 - end - - def vm_exists?(uuid) - raw("showvminfo", uuid).exit_code == 0 - end - end - end -end diff --git a/lib/vagrant/driver/virtualbox_4_1.rb b/lib/vagrant/driver/virtualbox_4_1.rb deleted file mode 100644 index 3983a1112..000000000 --- a/lib/vagrant/driver/virtualbox_4_1.rb +++ /dev/null @@ -1,474 +0,0 @@ -require 'log4r' - -require 'vagrant/driver/virtualbox_base' - -module Vagrant - module Driver - # Driver for VirtualBox 4.1.x - class VirtualBox_4_1 < VirtualBoxBase - def initialize(uuid) - super() - - @logger = Log4r::Logger.new("vagrant::driver::virtualbox_4_1") - @uuid = uuid - end - - def clear_forwarded_ports - args = [] - read_forwarded_ports(@uuid).each do |nic, name, _, _| - args.concat(["--natpf#{nic}", "delete", name]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def clear_shared_folders - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if folder = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] - execute("sharedfolder", "remove", @uuid, "--name", folder) - end - end - end - - def create_dhcp_server(network, options) - execute("dhcpserver", "add", "--ifname", network, - "--ip", options[:dhcp_ip], - "--netmask", options[:netmask], - "--lowerip", options[:dhcp_lower], - "--upperip", options[:dhcp_upper], - "--enable") - end - - def create_host_only_network(options) - # Create the interface - interface = execute("hostonlyif", "create") - name = interface[/^Interface '(.+?)' was successfully created$/, 1] - - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) - - # Return the details - return { - :name => name, - :ip => options[:adapter_ip], - :netmask => options[:netmask], - :dhcp => nil - } - end - - def delete - execute("unregistervm", @uuid, "--delete") - end - - def delete_unused_host_only_networks - networks = [] - execute("list", "hostonlyifs").split("\n").each do |line| - if network = line[/^Name:\s+(.+?)$/, 1] - networks << network - end - end - - execute("list", "vms").split("\n").each do |line| - if vm = line[/^".+?"\s+\{(.+?)\}$/, 1] - info = execute("showvminfo", vm, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] - networks.delete(adapter) - end - end - end - end - - networks.each do |name| - # First try to remove any DHCP servers attached. We use `raw` because - # it is okay if this fails. It usually means that a DHCP server was - # never attached. - raw("dhcpserver", "remove", "--ifname", name) - - # Delete the actual host only network interface. - execute("hostonlyif", "remove", name) - end - end - - def discard_saved_state - execute("discardstate", @uuid) - end - - def enable_adapters(adapters) - args = [] - adapters.each do |adapter| - args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) - - if adapter[:bridge] - args.concat(["--bridgeadapter#{adapter[:adapter]}", - adapter[:bridge]]) - end - - if adapter[:hostonly] - args.concat(["--hostonlyadapter#{adapter[:adapter]}", - adapter[:hostonly]]) - end - - if adapter[:mac_address] - args.concat(["--macaddress#{adapter[:adapter]}", - adapter[:mac_address]]) - end - - if adapter[:nic_type] - args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) - end - end - - execute("modifyvm", @uuid, *args) - end - - def execute_command(command) - raw(*command) - end - - def export(path) - # TODO: Progress - execute("export", @uuid, "--output", path.to_s) - end - - def forward_ports(ports) - args = [] - ports.each do |options| - pf_builder = [options[:name], - options[:protocol] || "tcp", - "", - options[:hostport], - "", - options[:guestport]] - - args.concat(["--natpf#{options[:adapter] || 1}", - pf_builder.join(",")]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def halt - execute("controlvm", @uuid, "poweroff") - end - - def import(ovf) - output = "" - total = "" - last = 0 - execute("import", ovf) do |type, data| - if type == :stdout - # Keep track of the stdout so that we can get the VM name - output << data - elsif type == :stderr - # Append the data so we can see the full view - total << data - - # Break up the lines. We can't get the progress until we see an "OK" - lines = total.split("\n") - if lines.include?("OK.") - # The progress of the import will be in the last line. Do a greedy - # regular expression to find what we're looking for. - if current = lines.last[/.+(\d{2})%/, 1] - current = current.to_i - if current > last - last = current - yield current if block_given? - end - end - end - end - end - - # Find the name of the VM name - name = output[/Suggested VM name "(.+?)"/, 1] - if !name - @logger.error("Couldn't find VM name in the output.") - return nil - end - - output = execute("list", "vms") - if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] - return existing_vm - end - - nil - end - - def read_forwarded_ports(uuid=nil, active_only=false) - uuid ||= @uuid - - @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") - - results = [] - current_nic = nil - info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - # This is how we find the nic that a FP is attached to, - # since this comes first. - if nic = line[/^nic(\d+)=".+?"$/, 1] - current_nic = nic.to_i - end - - # If we care about active VMs only, then we check the state - # to verify the VM is running. - if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") - return [] - end - - # Parse out the forwarded port information - if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) - result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] - @logger.debug(" - #{result.inspect}") - results << result - end - end - - results - end - - def read_bridged_interfaces - execute("list", "bridgedifs").split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Return the info to build up the results - info - end - end - - def read_guest_additions_version - output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", - :retryable => true) - if value = output[/^Value: (.+?)$/, 1] - # Split the version by _ since some distro versions modify it - # to look like this: 4.1.2_ubuntu, and the distro part isn't - # too important. - return value.split("_").first - end - - return nil - end - - def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] - info[:network] = network - elsif ip = line[/^IP:\s+(.+?)$/, 1] - info[:ip] = ip - elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] - info[:lower] = lower - elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] - info[:upper] = upper - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - - execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - - info - end - end - - def read_mac_address - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if mac = line[/^macaddress1="(.+?)"$/, 1] - return mac - end - end - - nil - end - - def read_mac_addresses - macs = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - mac = matcher[2].to_s - macs[adapter] = mac - end - end - macs - end - - def read_machine_folder - execute("list", "systemproperties", :retryable => true).split("\n").each do |line| - if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] - return folder - end - end - - nil - end - - def read_network_interfaces - nics = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^nic(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - type = matcher[2].to_sym - - nics[adapter] ||= {} - nics[adapter][:type] = type - elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:hostonly] = network - elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:bridge] = network - end - end - - nics - end - - def read_state - output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - if output =~ /^name=""$/ - return :inaccessible - elsif state = output[/^VMState="(.+?)"$/, 1] - return state.to_sym - end - - nil - end - - def read_used_ports - ports = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if uuid = line[/^".+?" \{(.+?)\}$/, 1] - # Ignore our own used ports - next if uuid == @uuid - - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport - end - end - end - - ports - end - - def read_vms - results = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if vm = line[/^".+?" \{(.+?)\}$/, 1] - results << vm - end - end - - results - end - - def set_mac_address(mac) - execute("modifyvm", @uuid, "--macaddress1", mac) - end - - def set_name(name) - execute("modifyvm", @uuid, "--name", name) - end - - def share_folders(folders) - folders.each do |folder| - args = ["--name", - folder[:name], - "--hostpath", - folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - execute("sharedfolder", "add", @uuid, *args) - end - end - - def ssh_port(expected_port) - @logger.debug("Searching for SSH port: #{expected_port.inspect}") - - # Look for the forwarded port only by comparing the guest port - read_forwarded_ports.each do |_, _, hostport, guestport| - return hostport if guestport == expected_port - end - - nil - end - - def start(mode) - command = ["startvm", @uuid, "--type", mode.to_s] - r = raw(*command) - - if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ - # Some systems return an exit code 1 for some reason. For that - # we depend on the output. - return true - end - - # If we reached this point then it didn't work out. - raise Errors::VBoxManageError, :command => command.inspect - end - - def suspend - execute("controlvm", @uuid, "savestate") - end - - def verify! - # This command sometimes fails if kernel drivers aren't properly loaded - # so we just run the command and verify that it succeeded. - execute("list", "hostonlyifs") - end - - def verify_image(path) - r = raw("import", path.to_s, "--dry-run") - return r.exit_code == 0 - end - - def vm_exists?(uuid) - raw("showvminfo", uuid).exit_code == 0 - end - end - end -end diff --git a/lib/vagrant/driver/virtualbox_base.rb b/lib/vagrant/driver/virtualbox_base.rb deleted file mode 100644 index 9e7cd1868..000000000 --- a/lib/vagrant/driver/virtualbox_base.rb +++ /dev/null @@ -1,326 +0,0 @@ -require 'log4r' - -require 'vagrant/util/busy' -require 'vagrant/util/platform' -require 'vagrant/util/retryable' -require 'vagrant/util/subprocess' - -module Vagrant - module Driver - # Base class for all VirtualBox drivers. - # - # This class provides useful tools for things such as executing - # VBoxManage and handling SIGINTs and so on. - class VirtualBoxBase - # Include this so we can use `Subprocess` more easily. - include Vagrant::Util - include Vagrant::Util::Retryable - - def initialize - @logger = Log4r::Logger.new("vagrant::driver::virtualbox_base") - - # This flag is used to keep track of interrupted state (SIGINT) - @interrupted = false - - # Set the path to VBoxManage - @vboxmanage_path = "VBoxManage" - - if Util::Platform.windows? - @logger.debug("Windows. Trying VBOX_INSTALL_PATH for VBoxManage") - - # On Windows, we use the VBOX_INSTALL_PATH environmental - # variable to find VBoxManage. - if ENV.has_key?("VBOX_INSTALL_PATH") - # Get the path. - path = ENV["VBOX_INSTALL_PATH"] - @logger.debug("VBOX_INSTALL_PATH value: #{path}") - - # There can actually be multiple paths in here, so we need to - # split by the separator ";" and see which is a good one. - path.split(";").each do |single| - # Make sure it ends with a \ - single += "\\" if !single.end_with?("\\") - - # If the executable exists, then set it as the main path - # and break out - vboxmanage = "#{path}VBoxManage.exe" - if File.file?(vboxmanage) - @vboxmanage_path = vboxmanage - break - end - end - end - end - - @logger.info("VBoxManage path: #{@vboxmanage_path}") - end - - # Clears the forwarded ports that have been set on the virtual machine. - def clear_forwarded_ports - end - - # Clears the shared folders that have been set on the virtual machine. - def clear_shared_folders - end - - # Creates a DHCP server for a host only network. - # - # @param [String] network Name of the host-only network. - # @param [Hash] options Options for the DHCP server. - def create_dhcp_server(network, options) - end - - # Creates a host only network with the given options. - # - # @param [Hash] options Options to create the host only network. - # @return [Hash] The details of the host only network, including - # keys `:name`, `:ip`, and `:netmask` - def create_host_only_network(options) - end - - # Deletes the virtual machine references by this driver. - def delete - end - - # Deletes any host only networks that aren't being used for anything. - def delete_unused_host_only_networks - end - - # Discards any saved state associated with this VM. - def discard_saved_state - end - - # Enables network adapters on the VM. - # - # The format of each adapter specification should be like so: - # - # { - # :type => :hostonly, - # :hostonly => "vboxnet0", - # :mac_address => "tubes" - # } - # - # This must support setting up both host only and bridged networks. - # - # @param [Array] adapters Array of adapters to enable. - def enable_adapters(adapters) - end - - # Execute a raw command straight through to VBoxManage. - # - # @param [Array] command Command to execute. - def execute_command(command) - end - - # Exports the virtual machine to the given path. - # - # @param [String] path Path to the OVF file. - # @yield [progress] Yields the block with the progress of the export. - def export(path) - end - - # Forwards a set of ports for a VM. - # - # This will not affect any previously set forwarded ports, - # so be sure to delete those if you need to. - # - # The format of each port hash should be the following: - # - # { - # :name => "foo", - # :hostport => 8500, - # :guestport => 80, - # :adapter => 1, - # :protocol => "tcp" - # } - # - # Note that "adapter" and "protocol" are optional and will default - # to 1 and "tcp" respectively. - # - # @param [Array] ports An array of ports to set. See documentation - # for more information on the format. - def forward_ports(ports) - end - - # Halts the virtual machine (pulls the plug). - def halt - end - - # Imports the VM from an OVF file. - # - # @param [String] ovf Path to the OVF file. - # @return [String] UUID of the imported VM. - def import(ovf) - end - - # Returns a list of forwarded ports for a VM. - # - # @param [String] uuid UUID of the VM to read from, or `nil` if this - # VM. - # @param [Boolean] active_only If true, only VMs that are running will - # be checked. - # @return [Array] - def read_forwarded_ports(uuid=nil, active_only=false) - end - - # Returns a list of bridged interfaces. - # - # @return [Hash] - def read_bridged_interfaces - end - - # Returns the guest additions version that is installed on this VM. - # - # @return [String] - def read_guest_additions_version - end - - # Returns a list of available host only interfaces. - # - # @return [Hash] - def read_host_only_interfaces - end - - # Returns the MAC address of the first network interface. - # - # @return [String] - def read_mac_address - end - - # Returns the folder where VirtualBox places it's VMs. - # - # @return [String] - def read_machine_folder - end - - # Returns a list of network interfaces of the VM. - # - # @return [Hash] - def read_network_interfaces - end - - # Returns the current state of this VM. - # - # @return [Symbol] - def read_state - end - - # Returns a list of all forwarded ports in use by active - # virtual machines. - # - # @return [Array] - def read_used_ports - end - - # Returns a list of all UUIDs of virtual machines currently - # known by VirtualBox. - # - # @return [Array] - def read_vms - end - - # Sets the MAC address of the first network adapter. - # - # @param [String] mac MAC address without any spaces/hyphens. - def set_mac_address(mac) - end - - # Share a set of folders on this VM. - # - # @param [Array] folders - def share_folders(folders) - end - - # Reads the SSH port of this VM. - # - # @param [Integer] expected Expected guest port of SSH. - def ssh_port(expected) - end - - # Starts the virtual machine. - # - # @param [String] mode Mode to boot the VM. Either "headless" - # or "gui" - def start(mode) - end - - # Suspend the virtual machine. - def suspend - end - - # Verifies that the driver is ready to accept work. - # - # This should raise a VagrantError if things are not ready. - def verify! - end - - # Verifies that an image can be imported properly. - # - # @param [String] path Path to an OVF file. - # @return [Boolean] - def verify_image(path) - end - - # Checks if a VM with the given UUID exists. - # - # @return [Boolean] - def vm_exists?(uuid) - end - - # Execute the given subcommand for VBoxManage and return the output. - def execute(*command, &block) - # Get the options hash if it exists - opts = {} - opts = command.pop if command.last.is_a?(Hash) - - tries = 0 - tries = 3 if opts[:retryable] - - # Variable to store our execution result - r = nil - - retryable(:on => Errors::VBoxManageError, :tries => tries, :sleep => 1) do - # Execute the command - r = raw(*command, &block) - - # If the command was a failure, then raise an exception that is - # nicely handled by Vagrant. - if r.exit_code != 0 - if @interrupted - @logger.info("Exit code != 0, but interrupted. Ignoring.") - else - raise Errors::VBoxManageError, :command => command.inspect - end - else - # Sometimes, VBoxManage fails but doesn't actual return a non-zero - # exit code. For this we inspect the output and determine if an error - # occurred. - if r.stderr =~ /VBoxManage: error:/ - @logger.info("VBoxManage error text found, assuming error.") - raise Errors::VBoxManageError, :command => command.inspect - end - end - end - - # Return the output, making sure to replace any Windows-style - # newlines with Unix-style. - r.stdout.gsub("\r\n", "\n") - end - - # Executes a command and returns the raw result object. - def raw(*command, &block) - int_callback = lambda do - @interrupted = true - @logger.info("Interrupted.") - end - - # Append in the options for subprocess - command << { :notify => [:stdout, :stderr] } - - Util::Busy.busy(int_callback) do - Subprocess.execute(@vboxmanage_path, *command, &block) - end - end - end - end -end diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 90137cd2f..9238dc283 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -114,7 +114,7 @@ module Vagrant # @return [Pathname] def dotfile_path return nil if !root_path - root_path.join(File.expand_path(config.global.vagrant.dotfile_name)) + root_path.join(config.global.vagrant.dotfile_name) end # Returns the collection of boxes for the environment. @@ -445,11 +445,22 @@ module Vagrant # Loads the persisted VM (if it exists) for this environment. def load_vms! - result = {} + # This is hardcoded for now. + provider = nil + Vagrant.plugin("1").registered.each do |plugin| + provider = plugin.provider.get(:virtualbox) + break if provider + end + + raise "VirtualBox provider not found." if !provider # Load all the virtual machine instances. + result = {} config.vms.each do |name| - result[name] = Vagrant::VM.new(name, self, config.for_vm(name)) + vm_config = config.for_vm(name) + box = boxes.find(vm_config.vm.box, :virtualbox) + + result[name] = Vagrant::Machine.new(name, provider, vm_config, box, self) end result diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 78a74b2d3..3347e956c 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -20,6 +20,13 @@ module Vagrant # @return [Environment] attr_reader :env + # ID of the machine. This ID comes from the provider and is not + # guaranteed to be of any particular format except that it is + # a string. + # + # @return [String] + attr_reader :id + # Name of the machine. This is assigned by the Vagrantfile. # # @return [String] @@ -45,6 +52,10 @@ module Vagrant @config = config @env = env @provider = provider_cls.new(self) + + # Read the ID, which is usually in local storage + @id = nil + @id = @env.local_data[:active][@name] if @env.local_data[:active] end # This calls an action on the provider. The provider may or may not @@ -69,6 +80,34 @@ module Vagrant @env.action_runner.run(callable, :machine => self) end + # This sets the unique ID associated with this machine. This will + # persist this ID so that in the future Vagrant will be able to find + # this machine again. The unique ID must be absolutely unique to the + # virtual machine, and can be used by providers for finding the + # actual machine associated with this instance. + # + # **WARNING:** Only providers should ever use this method. + # + # @param [String] value The ID. + def id=(value) + @env.local_data[:active] ||= {} + + if value + # Set the value + @env.local_data[:active][@name] = value + else + # Delete it from the active hash + @env.local_data[:active].delete(@name) + end + + # Commit the local data so that the next time Vagrant is initialized, + # it realizes the VM exists (or doesn't). + @env.local_data.commit + + # Store the ID locally + @id = value + end + # Returns the state of this machine. The state is queried from the # backing provider, so it can be any arbitrary symbol. # diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb new file mode 100644 index 000000000..1b503e950 --- /dev/null +++ b/plugins/providers/virtualbox/driver/base.rb @@ -0,0 +1,327 @@ +require 'log4r' + +require 'vagrant/util/busy' +require 'vagrant/util/platform' +require 'vagrant/util/retryable' +require 'vagrant/util/subprocess' + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Base class for all VirtualBox drivers. + # + # This class provides useful tools for things such as executing + # VBoxManage and handling SIGINTs and so on. + class Base + # Include this so we can use `Subprocess` more easily. + include Vagrant::Util::Retryable + + def initialize + @logger = Log4r::Logger.new("vagrant::provider::virtualbox::base") + + # This flag is used to keep track of interrupted state (SIGINT) + @interrupted = false + + # Set the path to VBoxManage + @vboxmanage_path = "VBoxManage" + + if Vagrant::Util::Platform.windows? + @logger.debug("Windows. Trying VBOX_INSTALL_PATH for VBoxManage") + + # On Windows, we use the VBOX_INSTALL_PATH environmental + # variable to find VBoxManage. + if ENV.has_key?("VBOX_INSTALL_PATH") + # Get the path. + path = ENV["VBOX_INSTALL_PATH"] + @logger.debug("VBOX_INSTALL_PATH value: #{path}") + + # There can actually be multiple paths in here, so we need to + # split by the separator ";" and see which is a good one. + path.split(";").each do |single| + # Make sure it ends with a \ + single += "\\" if !single.end_with?("\\") + + # If the executable exists, then set it as the main path + # and break out + vboxmanage = "#{path}VBoxManage.exe" + if File.file?(vboxmanage) + @vboxmanage_path = vboxmanage + break + end + end + end + end + + @logger.info("VBoxManage path: #{@vboxmanage_path}") + end + + # Clears the forwarded ports that have been set on the virtual machine. + def clear_forwarded_ports + end + + # Clears the shared folders that have been set on the virtual machine. + def clear_shared_folders + end + + # Creates a DHCP server for a host only network. + # + # @param [String] network Name of the host-only network. + # @param [Hash] options Options for the DHCP server. + def create_dhcp_server(network, options) + end + + # Creates a host only network with the given options. + # + # @param [Hash] options Options to create the host only network. + # @return [Hash] The details of the host only network, including + # keys `:name`, `:ip`, and `:netmask` + def create_host_only_network(options) + end + + # Deletes the virtual machine references by this driver. + def delete + end + + # Deletes any host only networks that aren't being used for anything. + def delete_unused_host_only_networks + end + + # Discards any saved state associated with this VM. + def discard_saved_state + end + + # Enables network adapters on the VM. + # + # The format of each adapter specification should be like so: + # + # { + # :type => :hostonly, + # :hostonly => "vboxnet0", + # :mac_address => "tubes" + # } + # + # This must support setting up both host only and bridged networks. + # + # @param [Array] adapters Array of adapters to enable. + def enable_adapters(adapters) + end + + # Execute a raw command straight through to VBoxManage. + # + # @param [Array] command Command to execute. + def execute_command(command) + end + + # Exports the virtual machine to the given path. + # + # @param [String] path Path to the OVF file. + # @yield [progress] Yields the block with the progress of the export. + def export(path) + end + + # Forwards a set of ports for a VM. + # + # This will not affect any previously set forwarded ports, + # so be sure to delete those if you need to. + # + # The format of each port hash should be the following: + # + # { + # :name => "foo", + # :hostport => 8500, + # :guestport => 80, + # :adapter => 1, + # :protocol => "tcp" + # } + # + # Note that "adapter" and "protocol" are optional and will default + # to 1 and "tcp" respectively. + # + # @param [Array] ports An array of ports to set. See documentation + # for more information on the format. + def forward_ports(ports) + end + + # Halts the virtual machine (pulls the plug). + def halt + end + + # Imports the VM from an OVF file. + # + # @param [String] ovf Path to the OVF file. + # @return [String] UUID of the imported VM. + def import(ovf) + end + + # Returns a list of forwarded ports for a VM. + # + # @param [String] uuid UUID of the VM to read from, or `nil` if this + # VM. + # @param [Boolean] active_only If true, only VMs that are running will + # be checked. + # @return [Array] + def read_forwarded_ports(uuid=nil, active_only=false) + end + + # Returns a list of bridged interfaces. + # + # @return [Hash] + def read_bridged_interfaces + end + + # Returns the guest additions version that is installed on this VM. + # + # @return [String] + def read_guest_additions_version + end + + # Returns a list of available host only interfaces. + # + # @return [Hash] + def read_host_only_interfaces + end + + # Returns the MAC address of the first network interface. + # + # @return [String] + def read_mac_address + end + + # Returns the folder where VirtualBox places it's VMs. + # + # @return [String] + def read_machine_folder + end + + # Returns a list of network interfaces of the VM. + # + # @return [Hash] + def read_network_interfaces + end + + # Returns the current state of this VM. + # + # @return [Symbol] + def read_state + end + + # Returns a list of all forwarded ports in use by active + # virtual machines. + # + # @return [Array] + def read_used_ports + end + + # Returns a list of all UUIDs of virtual machines currently + # known by VirtualBox. + # + # @return [Array] + def read_vms + end + + # Sets the MAC address of the first network adapter. + # + # @param [String] mac MAC address without any spaces/hyphens. + def set_mac_address(mac) + end + + # Share a set of folders on this VM. + # + # @param [Array] folders + def share_folders(folders) + end + + # Reads the SSH port of this VM. + # + # @param [Integer] expected Expected guest port of SSH. + def ssh_port(expected) + end + + # Starts the virtual machine. + # + # @param [String] mode Mode to boot the VM. Either "headless" + # or "gui" + def start(mode) + end + + # Suspend the virtual machine. + def suspend + end + + # Verifies that the driver is ready to accept work. + # + # This should raise a VagrantError if things are not ready. + def verify! + end + + # Verifies that an image can be imported properly. + # + # @param [String] path Path to an OVF file. + # @return [Boolean] + def verify_image(path) + end + + # Checks if a VM with the given UUID exists. + # + # @return [Boolean] + def vm_exists?(uuid) + end + + # Execute the given subcommand for VBoxManage and return the output. + def execute(*command, &block) + # Get the options hash if it exists + opts = {} + opts = command.pop if command.last.is_a?(Hash) + + tries = 0 + tries = 3 if opts[:retryable] + + # Variable to store our execution result + r = nil + + retryable(:on => Vagrant::Errors::VBoxManageError, :tries => tries, :sleep => 1) do + # Execute the command + r = raw(*command, &block) + + # If the command was a failure, then raise an exception that is + # nicely handled by Vagrant. + if r.exit_code != 0 + if @interrupted + @logger.info("Exit code != 0, but interrupted. Ignoring.") + else + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + else + # Sometimes, VBoxManage fails but doesn't actual return a non-zero + # exit code. For this we inspect the output and determine if an error + # occurred. + if r.stderr =~ /VBoxManage: error:/ + @logger.info("VBoxManage error text found, assuming error.") + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + end + end + + # Return the output, making sure to replace any Windows-style + # newlines with Unix-style. + r.stdout.gsub("\r\n", "\n") + end + + # Executes a command and returns the raw result object. + def raw(*command, &block) + int_callback = lambda do + @interrupted = true + @logger.info("Interrupted.") + end + + # Append in the options for subprocess + command << { :notify => [:stdout, :stderr] } + + Vagrant::Util::Busy.busy(int_callback) do + Vagrant::Util::Subprocess.execute(@vboxmanage_path, *command, &block) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb new file mode 100644 index 000000000..1cf76436e --- /dev/null +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -0,0 +1,139 @@ +require "forwardable" + +require "log4r" + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + class Meta < Base + # This is raised if the VM is not found when initializing a driver + # with a UUID. + class VMNotFound < StandardError; end + + # We use forwardable to do all our driver forwarding + extend Forwardable + + # The UUID of the virtual machine we represent + attr_reader :uuid + + # The version of virtualbox that is running. + attr_reader :version + + def initialize(uuid=nil) + # Setup the base + super() + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox::meta") + @uuid = uuid + + # Read and assign the version of VirtualBox we know which + # specific driver to instantiate. + begin + @version = read_version || "" + rescue Vagrant::Util::Subprocess::LaunchError + # This means that VirtualBox was not found, so we raise this + # error here. + raise Vagrant::Errors::VirtualBoxNotDetected + end + + # Instantiate the proper version driver for VirtualBox + @logger.debug("Finding driver for VirtualBox version: #{@version}") + driver_map = { + "4.0" => Version_4_0, + "4.1" => Version_4_1 + } + + driver_klass = nil + driver_map.each do |key, klass| + if @version.start_with?(key) + driver_klass = klass + break + end + end + + if !driver_klass + supported_versions = driver_map.keys.sort.join(", ") + raise Vagrant::Errors::VirtualBoxInvalidVersion, :supported_versions => supported_versions + end + + @logger.info("Using VirtualBox driver: #{driver_klass}") + @driver = driver_klass.new(@uuid) + + if @uuid + # Verify the VM exists, and if it doesn't, then don't worry + # about it (mark the UUID as nil) + raise VMNotFound if !@driver.vm_exists?(@uuid) + end + end + + def_delegators :@driver, :clear_forwarded_ports, + :clear_shared_folders, + :create_dhcp_server, + :create_host_only_network, + :delete, + :delete_unused_host_only_networks, + :discard_saved_state, + :enable_adapters, + :execute_command, + :export, + :forward_ports, + :halt, + :import, + :read_forwarded_ports, + :read_bridged_interfaces, + :read_guest_additions_version, + :read_host_only_interfaces, + :read_mac_address, + :read_mac_addresses, + :read_machine_folder, + :read_network_interfaces, + :read_state, + :read_used_ports, + :read_vms, + :set_mac_address, + :set_name, + :share_folders, + :ssh_port, + :start, + :suspend, + :verify!, + :verify_image, + :vm_exists? + + protected + + # This returns the version of VirtualBox that is running. + # + # @return [String] + def read_version + # The version string is usually in one of the following formats: + # + # * 4.1.8r1234 + # * 4.1.8r1234_OSE + # * 4.1.8_MacPortsr1234 + # + # Below accounts for all of these. + + # Note: We split this into multiple lines because apparently "".split("_") + # is [], so we have to check for an empty array in between. + output = execute("--version") + if output =~ /vboxdrv kernel module is not loaded/ + raise Vagrant::Errors::VirtualBoxKernelModuleNotLoaded + elsif output =~ /Please install/ + # Check for installation incomplete warnings, for example: + # "WARNING: The character device /dev/vboxdrv does not + # exist. Please install the virtualbox-ose-dkms package and + # the appropriate headers, most likely linux-headers-generic." + raise Vagrant::Errors::VirtualBoxInstallIncomplete + end + + parts = output.split("_") + return nil if parts.empty? + parts[0].split("r")[0] + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/version_4_0.rb b/plugins/providers/virtualbox/driver/version_4_0.rb new file mode 100644 index 000000000..bbf0cde83 --- /dev/null +++ b/plugins/providers/virtualbox/driver/version_4_0.rb @@ -0,0 +1,476 @@ +require 'log4r' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Driver for VirtualBox 4.0.x + class Version_4_0 < Base + def initialize(uuid) + super() + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_0") + @uuid = uuid + end + + def clear_forwarded_ports + args = [] + read_forwarded_ports(@uuid).each do |nic, name, _, _| + args.concat(["--natpf#{nic}", "delete", name]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def clear_shared_folders + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if name = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] + execute("sharedfolder", "remove", @uuid, "--name", name) + end + end + end + + def create_dhcp_server(network, options) + execute("dhcpserver", "add", "--ifname", network, + "--ip", options[:dhcp_ip], + "--netmask", options[:netmask], + "--lowerip", options[:dhcp_lower], + "--upperip", options[:dhcp_upper], + "--enable") + end + + def create_host_only_network(options) + # Create the interface + interface = execute("hostonlyif", "create") + name = interface[/^Interface '(.+?)' was successfully created$/, 1] + + # Configure it + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + + # Return the details + return { + :name => name, + :ip => options[:adapter_ip], + :netmask => options[:netmask], + :dhcp => nil + } + end + + def delete + execute("unregistervm", @uuid, "--delete") + end + + def delete_unused_host_only_networks + networks = [] + execute("list", "hostonlyifs").split("\n").each do |line| + if network_name = line[/^Name:\s+(.+?)$/, 1] + networks << network_name + end + end + + execute("list", "vms").split("\n").each do |line| + if vm_name = line[/^".+?"\s+\{(.+?)\}$/, 1] + info = execute("showvminfo", vm_name, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] + networks.delete(network_name) + end + end + end + end + + networks.each do |name| + # First try to remove any DHCP servers attached. We use `raw` because + # it is okay if this fails. It usually means that a DHCP server was + # never attached. + raw("dhcpserver", "remove", "--ifname", name) + + # Delete the actual host only network interface. + execute("hostonlyif", "remove", name) + end + end + + def discard_saved_state + execute("discardstate", @uuid) + end + + def enable_adapters(adapters) + args = [] + adapters.each do |adapter| + args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) + + if adapter[:bridge] + args.concat(["--bridgeadapter#{adapter[:adapter]}", + adapter[:bridge]]) + end + + if adapter[:hostonly] + args.concat(["--hostonlyadapter#{adapter[:adapter]}", + adapter[:hostonly]]) + end + + if adapter[:mac_address] + args.concat(["--macaddress#{adapter[:adapter]}", + adapter[:mac_address]]) + end + + if adapter[:nic_type] + args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) + end + end + + execute("modifyvm", @uuid, *args) + end + + def execute_command(command) + raw(*command) + end + + def export(path) + # TODO: Progress + execute("export", @uuid, "--output", path.to_s) + end + + def forward_ports(ports) + args = [] + ports.each do |options| + pf_builder = [options[:name], + options[:protocol] || "tcp", + "", + options[:hostport], + "", + options[:guestport]] + + args.concat(["--natpf#{options[:adapter] || 1}", + pf_builder.join(",")]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def halt + execute("controlvm", @uuid, "poweroff") + end + + def import(ovf) + output = "" + total = "" + last = 0 + execute("import", ovf) do |type, data| + if type == :stdout + # Keep track of the stdout so that we can get the VM name + output << data + elsif type == :stderr + # Append the data so we can see the full view + total << data + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + if lines.include?("OK.") + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if current = lines.last[/.+(\d{2})%/, 1] + current = current.to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + # Find the name of the VM name + name = output[/Suggested VM name "(.+?)"/, 1] + if !name + @logger.error("Couldn't find VM name in the output.") + return nil + end + + output = execute("list", "vms") + if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] + return existing_vm + end + + nil + end + + def read_forwarded_ports(uuid=nil, active_only=false) + uuid ||= @uuid + + @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") + + results = [] + current_nic = nil + info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + # This is how we find the nic that a FP is attached to, + # since this comes first. + if nic = line[/^nic(\d+)=".+?"$/, 1] + current_nic = nic.to_i + end + + # If we care about active VMs only, then we check the state + # to verify the VM is running. + if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") + return [] + end + + # Parse out the forwarded port information + if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) + result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] + @logger.debug(" - #{result.inspect}") + results << result + end + end + + results + end + + def read_bridged_interfaces + execute("list", "bridgedifs").split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Return the info to build up the results + info + end + end + + def read_guest_additions_version + output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", + :retryable => true) + if value = output[/^Value: (.+?)$/, 1] + # Split the version by _ since some distro versions modify it + # to look like this: 4.1.2_ubuntu, and the distro part isn't + # too important. + return value.split("_").first + end + + return nil + end + + def read_host_only_interfaces + dhcp = {} + execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| + info = {} + + block.split("\n").each do |line| + if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] + info[:network] = network + elsif ip = line[/^IP:\s+(.+?)$/, 1] + info[:ip] = ip + elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] + info[:lower] = lower + elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] + info[:upper] = upper + end + end + + # Set the DHCP info + dhcp[info[:network]] = info + end + + execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Set the DHCP info if it exists + info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] + + info + end + end + + def read_mac_address + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if mac = line[/^macaddress1="(.+?)"$/, 1] + return mac + end + end + + nil + end + + def read_mac_addresses + macs = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + mac = matcher[2].to_s + macs[adapter] = mac + end + end + macs + end + + def read_machine_folder + execute("list", "systemproperties", :retryable => true).split("\n").each do |line| + if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] + return folder + end + end + + nil + end + + def read_network_interfaces + nics = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^nic(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + type = matcher[2].to_sym + + nics[adapter] ||= {} + nics[adapter][:type] = type + elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:hostonly] = network + elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:bridge] = network + end + end + + nics + end + + def read_state + output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + if output =~ /^name=""$/ + return :inaccessible + elsif state = output[/^VMState="(.+?)"$/, 1] + return state.to_sym + end + + nil + end + + def read_used_ports + ports = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if uuid = line[/^".+?" \{(.+?)\}$/, 1] + # Ignore our own used ports + next if uuid == @uuid + + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + end + end + + ports + end + + def read_vms + results = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if vm = line[/^".+?" \{(.+?)\}$/, 1] + results << vm + end + end + + results + end + + def set_mac_address(mac) + execute("modifyvm", @uuid, "--macaddress1", mac) + end + + def set_name(name) + execute("modifyvm", @uuid, "--name", name) + end + + def share_folders(folders) + folders.each do |folder| + args = ["--name", + folder[:name], + "--hostpath", + folder[:hostpath]] + args << "--transient" if folder.has_key?(:transient) && folder[:transient] + execute("sharedfolder", "add", @uuid, *args) + end + end + + def ssh_port(expected_port) + @logger.debug("Searching for SSH port: #{expected_port.inspect}") + + # Look for the forwarded port only by comparing the guest port + read_forwarded_ports.each do |_, _, hostport, guestport| + return hostport if guestport == expected_port + end + + nil + end + + def start(mode) + command = ["startvm", @uuid, "--type", mode.to_s] + r = raw(*command) + + if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ + # Some systems return an exit code 1 for some reason. For that + # we depend on the output. + return true + end + + # If we reached this point then it didn't work out. + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + + def suspend + execute("controlvm", @uuid, "savestate") + end + + def verify! + # This command sometimes fails if kernel drivers aren't properly loaded + # so we just run the command and verify that it succeeded. + execute("list", "hostonlyifs") + end + + def verify_image(path) + r = raw("import", path.to_s, "--dry-run") + return r.exit_code == 0 + end + + def vm_exists?(uuid) + raw("showvminfo", uuid).exit_code == 0 + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/version_4_1.rb b/plugins/providers/virtualbox/driver/version_4_1.rb new file mode 100644 index 000000000..e4a1268a8 --- /dev/null +++ b/plugins/providers/virtualbox/driver/version_4_1.rb @@ -0,0 +1,476 @@ +require 'log4r' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Driver for VirtualBox 4.1.x + class Version_4_1 < Base + def initialize(uuid) + super() + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_1") + @uuid = uuid + end + + def clear_forwarded_ports + args = [] + read_forwarded_ports(@uuid).each do |nic, name, _, _| + args.concat(["--natpf#{nic}", "delete", name]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def clear_shared_folders + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if folder = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] + execute("sharedfolder", "remove", @uuid, "--name", folder) + end + end + end + + def create_dhcp_server(network, options) + execute("dhcpserver", "add", "--ifname", network, + "--ip", options[:dhcp_ip], + "--netmask", options[:netmask], + "--lowerip", options[:dhcp_lower], + "--upperip", options[:dhcp_upper], + "--enable") + end + + def create_host_only_network(options) + # Create the interface + interface = execute("hostonlyif", "create") + name = interface[/^Interface '(.+?)' was successfully created$/, 1] + + # Configure it + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + + # Return the details + return { + :name => name, + :ip => options[:adapter_ip], + :netmask => options[:netmask], + :dhcp => nil + } + end + + def delete + execute("unregistervm", @uuid, "--delete") + end + + def delete_unused_host_only_networks + networks = [] + execute("list", "hostonlyifs").split("\n").each do |line| + if network = line[/^Name:\s+(.+?)$/, 1] + networks << network + end + end + + execute("list", "vms").split("\n").each do |line| + if vm = line[/^".+?"\s+\{(.+?)\}$/, 1] + info = execute("showvminfo", vm, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] + networks.delete(adapter) + end + end + end + end + + networks.each do |name| + # First try to remove any DHCP servers attached. We use `raw` because + # it is okay if this fails. It usually means that a DHCP server was + # never attached. + raw("dhcpserver", "remove", "--ifname", name) + + # Delete the actual host only network interface. + execute("hostonlyif", "remove", name) + end + end + + def discard_saved_state + execute("discardstate", @uuid) + end + + def enable_adapters(adapters) + args = [] + adapters.each do |adapter| + args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) + + if adapter[:bridge] + args.concat(["--bridgeadapter#{adapter[:adapter]}", + adapter[:bridge]]) + end + + if adapter[:hostonly] + args.concat(["--hostonlyadapter#{adapter[:adapter]}", + adapter[:hostonly]]) + end + + if adapter[:mac_address] + args.concat(["--macaddress#{adapter[:adapter]}", + adapter[:mac_address]]) + end + + if adapter[:nic_type] + args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) + end + end + + execute("modifyvm", @uuid, *args) + end + + def execute_command(command) + raw(*command) + end + + def export(path) + # TODO: Progress + execute("export", @uuid, "--output", path.to_s) + end + + def forward_ports(ports) + args = [] + ports.each do |options| + pf_builder = [options[:name], + options[:protocol] || "tcp", + "", + options[:hostport], + "", + options[:guestport]] + + args.concat(["--natpf#{options[:adapter] || 1}", + pf_builder.join(",")]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def halt + execute("controlvm", @uuid, "poweroff") + end + + def import(ovf) + output = "" + total = "" + last = 0 + execute("import", ovf) do |type, data| + if type == :stdout + # Keep track of the stdout so that we can get the VM name + output << data + elsif type == :stderr + # Append the data so we can see the full view + total << data + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + if lines.include?("OK.") + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if current = lines.last[/.+(\d{2})%/, 1] + current = current.to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + # Find the name of the VM name + name = output[/Suggested VM name "(.+?)"/, 1] + if !name + @logger.error("Couldn't find VM name in the output.") + return nil + end + + output = execute("list", "vms") + if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] + return existing_vm + end + + nil + end + + def read_forwarded_ports(uuid=nil, active_only=false) + uuid ||= @uuid + + @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") + + results = [] + current_nic = nil + info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + # This is how we find the nic that a FP is attached to, + # since this comes first. + if nic = line[/^nic(\d+)=".+?"$/, 1] + current_nic = nic.to_i + end + + # If we care about active VMs only, then we check the state + # to verify the VM is running. + if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") + return [] + end + + # Parse out the forwarded port information + if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) + result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] + @logger.debug(" - #{result.inspect}") + results << result + end + end + + results + end + + def read_bridged_interfaces + execute("list", "bridgedifs").split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Return the info to build up the results + info + end + end + + def read_guest_additions_version + output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", + :retryable => true) + if value = output[/^Value: (.+?)$/, 1] + # Split the version by _ since some distro versions modify it + # to look like this: 4.1.2_ubuntu, and the distro part isn't + # too important. + return value.split("_").first + end + + return nil + end + + def read_host_only_interfaces + dhcp = {} + execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| + info = {} + + block.split("\n").each do |line| + if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] + info[:network] = network + elsif ip = line[/^IP:\s+(.+?)$/, 1] + info[:ip] = ip + elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] + info[:lower] = lower + elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] + info[:upper] = upper + end + end + + # Set the DHCP info + dhcp[info[:network]] = info + end + + execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Set the DHCP info if it exists + info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] + + info + end + end + + def read_mac_address + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if mac = line[/^macaddress1="(.+?)"$/, 1] + return mac + end + end + + nil + end + + def read_mac_addresses + macs = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + mac = matcher[2].to_s + macs[adapter] = mac + end + end + macs + end + + def read_machine_folder + execute("list", "systemproperties", :retryable => true).split("\n").each do |line| + if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] + return folder + end + end + + nil + end + + def read_network_interfaces + nics = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^nic(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + type = matcher[2].to_sym + + nics[adapter] ||= {} + nics[adapter][:type] = type + elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:hostonly] = network + elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:bridge] = network + end + end + + nics + end + + def read_state + output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + if output =~ /^name=""$/ + return :inaccessible + elsif state = output[/^VMState="(.+?)"$/, 1] + return state.to_sym + end + + nil + end + + def read_used_ports + ports = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if uuid = line[/^".+?" \{(.+?)\}$/, 1] + # Ignore our own used ports + next if uuid == @uuid + + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + end + end + + ports + end + + def read_vms + results = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if vm = line[/^".+?" \{(.+?)\}$/, 1] + results << vm + end + end + + results + end + + def set_mac_address(mac) + execute("modifyvm", @uuid, "--macaddress1", mac) + end + + def set_name(name) + execute("modifyvm", @uuid, "--name", name) + end + + def share_folders(folders) + folders.each do |folder| + args = ["--name", + folder[:name], + "--hostpath", + folder[:hostpath]] + args << "--transient" if folder.has_key?(:transient) && folder[:transient] + execute("sharedfolder", "add", @uuid, *args) + end + end + + def ssh_port(expected_port) + @logger.debug("Searching for SSH port: #{expected_port.inspect}") + + # Look for the forwarded port only by comparing the guest port + read_forwarded_ports.each do |_, _, hostport, guestport| + return hostport if guestport == expected_port + end + + nil + end + + def start(mode) + command = ["startvm", @uuid, "--type", mode.to_s] + r = raw(*command) + + if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ + # Some systems return an exit code 1 for some reason. For that + # we depend on the output. + return true + end + + # If we reached this point then it didn't work out. + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + + def suspend + execute("controlvm", @uuid, "savestate") + end + + def verify! + # This command sometimes fails if kernel drivers aren't properly loaded + # so we just run the command and verify that it succeeded. + execute("list", "hostonlyifs") + end + + def verify_image(path) + r = raw("import", path.to_s, "--dry-run") + return r.exit_code == 0 + end + + def vm_exists?(uuid) + raw("showvminfo", uuid).exit_code == 0 + end + end + end + end +end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index 487bc4ec8..d5c3c130a 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -3,7 +3,7 @@ require "vagrant" module VagrantPlugins module ProviderVirtualBox class Plugin < Vagrant.plugin("1") - name "virtualbox provider" + name "VirtualBox provider" description <<-EOF The VirtualBox provider allows Vagrant to manage and control VirtualBox-based virtual machines. @@ -14,5 +14,13 @@ module VagrantPlugins Provider end end + + # Drop some autoloads in here to optimize the performance of loading + # our drivers only when they are needed. + module Driver + autoload :Meta, File.expand_path("../driver/meta", __FILE__) + autoload :Version_4_0, File.expand_path("../driver/version_4_0", __FILE__) + autoload :Version_4_1, File.expand_path("../driver/version_4_1", __FILE__) + end end end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 8cbd51f74..eda1de5bb 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -1,6 +1,19 @@ module VagrantPlugins module ProviderVirtualBox class Provider < Vagrant.plugin("1", :provider) + def initialize(machine) + @machine = machine + @driver = Driver::Meta.new(@machine.id) + end + + # Return the state of VirtualBox virtual machine by actually + # querying VBoxManage. + def state + return :not_created if !@driver.uuid + state = @driver.read_state + return :unknown if !state + state + end end end end diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index f941c349b..6817c034f 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -12,10 +12,23 @@ describe Vagrant::Machine do end let(:box) { Object.new } let(:config) { Object.new } - let(:env) { test_env.create_vagrant_env } + let(:env) do + # We need to create a Vagrantfile so that this test environment + # has a proper root path + test_env.vagrantfile("") + + # Create the Vagrant::Environment instance + test_env.create_vagrant_env + end + let(:test_env) { isolated_environment } - let(:instance) { described_class.new(name, provider_cls, config, box, env) } + let(:instance) { new_instance } + + # Returns a new instance with the test data + def new_instance + described_class.new(name, provider_cls, config, box, env) + end describe "initialization" do it "should initialize the provider with the machine object" do @@ -89,6 +102,22 @@ describe Vagrant::Machine do end end + describe "setting the ID" do + it "should not have an ID by default" do + instance.id.should be_nil + end + + it "should set an ID" do + instance.id = "bar" + instance.id.should == "bar" + end + + it "should persist the ID" do + instance.id = "foo" + new_instance.id.should == "foo" + end + end + describe "state" do it "should query state from the provider" do state = :running From 2ef20586da66ccbf6ba875b856a6c130c97e92d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jul 2012 21:57:11 -0700 Subject: [PATCH 10/60] Remove the action registry feature. This can be removed since in the future all actions will come from the providers or something. There are still issues with box actions but we'll get back to that later... --- lib/vagrant.rb | 6 ------ lib/vagrant/action/runner.rb | 26 +------------------------- lib/vagrant/environment.rb | 12 +----------- 3 files changed, 2 insertions(+), 42 deletions(-) diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 5fa92e82b..bd021743d 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -95,12 +95,6 @@ module Vagrant c.register([:"1", :provider]) { Plugin::V1::Provider } c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } - # Returns a `Vagrant::Registry` object that contains all the built-in - # middleware stacks. - def self.actions - @actions ||= Vagrant::Action::Builtin.new - end - # The source root is the path to the root directory of # the Vagrant gem. def self.source_root diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index a28551b44..832a38872 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -10,8 +10,7 @@ module Vagrant class Runner @@reported_interrupt = false - def initialize(registry, globals=nil, &block) - @registry = registry + def initialize(globals=nil, &block) @globals = globals || {} @lazy_globals = block @logger = Log4r::Logger.new("vagrant::action::runner") @@ -20,7 +19,6 @@ module Vagrant def run(callable_id, options=nil) callable = callable_id callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class) - callable = registry_sequence(callable_id) if callable_id.kind_of?(Symbol) raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call) # Create the initial environment with the options given @@ -48,28 +46,6 @@ module Vagrant @logger.info("Running action: #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } end - - protected - - def registry_sequence(id) - # Attempt to get the sequence - seq = @registry.get(id) - return nil if !seq - - # Go through all the registered plugins and get all the hooks - # for this sequence. - Vagrant.plugin("1").registered.each do |plugin| - hooks = plugin.action_hook(Vagrant::Plugin::V1::Plugin::ALL_ACTIONS) - hooks += plugin.action_hook(id) - - hooks.each do |hook| - hook.call(seq) - end - end - - # Return the sequence - seq - end end end end diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 9238dc283..532d1d030 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -205,7 +205,7 @@ module Vagrant # # @return [Action::Runner] def action_runner - @action_runner ||= Action::Runner.new(action_registry) do + @action_runner ||= Action::Runner.new do { :action_runner => action_runner, :box_collection => boxes, @@ -218,16 +218,6 @@ module Vagrant end end - # Action registry for registering new actions with this environment. - # - # @return [Registry] - def action_registry - # For now we return the global built-in actions registry. In the future - # we may want to create an isolated registry that inherits from this - # global one, but for now there isn't a use case that calls for it. - Vagrant.actions - end - # Loads on initial access and reads data from the global data store. # The global data store is global to Vagrant everywhere (in every environment), # so it can be used to store system-wide information. Note that "system-wide" From 5f8a6543932a8b4f1fcdb02a7b1308e06aef0564 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jul 2012 22:00:25 -0700 Subject: [PATCH 11/60] Goodbye, instance_eval on Vagrant::Action::Builder! --- lib/vagrant/action/builder.rb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/vagrant/action/builder.rb b/lib/vagrant/action/builder.rb index da95af792..fdd7d9a6e 100644 --- a/lib/vagrant/action/builder.rb +++ b/lib/vagrant/action/builder.rb @@ -9,20 +9,14 @@ module Vagrant # # Building an action sequence is very easy: # - # app = Vagrant::Action::Builder.new do - # use MiddlewareA - # use MiddlewareB + # app = Vagrant::Action::Builder.new.tap do |b| + # b.use MiddlewareA + # b.use MiddlewareB # end # # Vagrant::Action.run(app) # class Builder - # Initializes the builder. An optional block can be passed which - # will be evaluated in the context of the instance. - def initialize(&block) - instance_eval(&block) if block_given? - end - # Returns a mergeable version of the builder. If `use` is called with # the return value of this method, then the stack will merge, instead # of being treated as a separate single middleware. From 5eed3b84179f8c811651df299e2f7d7728110d6f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jul 2012 22:39:27 -0700 Subject: [PATCH 12/60] Building up the `destroy` action again using new provider API. This shows me moving the built-in middleware sequences to the provider and how I'm organizing all that. --- .../action/general/check_virtualbox.rb | 28 ------------------- lib/vagrant/action/general/validate.rb | 5 +++- plugins/commands/destroy/command.rb | 3 ++ plugins/providers/virtualbox/action.rb | 20 +++++++++++++ .../virtualbox/action}/check_accessible.rb | 10 +++---- .../virtualbox/action/check_virtualbox.rb | 22 +++++++++++++++ plugins/providers/virtualbox/plugin.rb | 2 ++ plugins/providers/virtualbox/provider.rb | 22 +++++++++++++++ test/unit/vagrant/action/runner_test.rb | 12 ++------ test/unit/vagrant/environment_test.rb | 10 ------- 10 files changed, 81 insertions(+), 53 deletions(-) delete mode 100644 lib/vagrant/action/general/check_virtualbox.rb create mode 100644 plugins/providers/virtualbox/action.rb rename {lib/vagrant/action/vm => plugins/providers/virtualbox/action}/check_accessible.rb (73%) create mode 100644 plugins/providers/virtualbox/action/check_virtualbox.rb diff --git a/lib/vagrant/action/general/check_virtualbox.rb b/lib/vagrant/action/general/check_virtualbox.rb deleted file mode 100644 index cbaecc8ec..000000000 --- a/lib/vagrant/action/general/check_virtualbox.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Vagrant - module Action - module General - # Checks that virtualbox is installed and ready to be used. - class CheckVirtualbox - def initialize(app, env) - @app = app - end - - def call(env) - # Certain actions may not actually have a VM, and thus no - # driver, so we have to be clever about obtaining an instance - # of the driver. - driver = nil - driver = env[:vm].driver if env[:vm] - driver = Driver::VirtualBox.new(nil) if !driver - - # Verify that it is ready to go! This will raise an exception - # if anything goes wrong. - driver.verify! - - # Carry on. - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/general/validate.rb b/lib/vagrant/action/general/validate.rb index 5045918dd..f566b793f 100644 --- a/lib/vagrant/action/general/validate.rb +++ b/lib/vagrant/action/general/validate.rb @@ -9,7 +9,10 @@ module Vagrant end def call(env) - env[:vm].config.validate!(env[:vm].env) if !env.has_key?("validate") || env["validate"] + if !env.has_key?(:validate) || env[:validate] + env[:machine].config.validate!(env[:machine].env) + end + @app.call(env) end end diff --git a/plugins/commands/destroy/command.rb b/plugins/commands/destroy/command.rb index 9c18dafae..1fcd14f98 100644 --- a/plugins/commands/destroy/command.rb +++ b/plugins/commands/destroy/command.rb @@ -19,6 +19,9 @@ module VagrantPlugins @logger.debug("'Destroy' each target VM...") with_target_vms(argv, :reverse => true) do |vm| + vm.action(:destroy) + next + if vm.created? # Boolean whether we should actually go through with the destroy # or not. This is true only if the "--force" flag is set or if the diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb new file mode 100644 index 000000000..29b5e7a54 --- /dev/null +++ b/plugins/providers/virtualbox/action.rb @@ -0,0 +1,20 @@ +require "vagrant/action/builder" + +module VagrantPlugins + module ProviderVirtualBox + module Action + autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) + autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) + + # This is the action that is primarily responsible for completely + # freeing the resources of the underlying virtual machine. + def self.action_destroy + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use CheckAccessible + end + end + end + end +end diff --git a/lib/vagrant/action/vm/check_accessible.rb b/plugins/providers/virtualbox/action/check_accessible.rb similarity index 73% rename from lib/vagrant/action/vm/check_accessible.rb rename to plugins/providers/virtualbox/action/check_accessible.rb index 69df47194..f000825b1 100644 --- a/lib/vagrant/action/vm/check_accessible.rb +++ b/plugins/providers/virtualbox/action/check_accessible.rb @@ -1,18 +1,18 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class CheckAccessible def initialize(app, env) @app = app end def call(env) - if env[:vm].state == :inaccessible + if env[:machine].state == :inaccessible # The VM we are attempting to manipulate is inaccessible. This # is a very bad situation and can only be fixed by the user. It # also prohibits us from actually doing anything with the virtual # machine, so we raise an error. - raise Errors::VMInaccessible + raise Vagrant::Errors::VMInaccessible end @app.call(env) diff --git a/plugins/providers/virtualbox/action/check_virtualbox.rb b/plugins/providers/virtualbox/action/check_virtualbox.rb new file mode 100644 index 000000000..1d590fe4f --- /dev/null +++ b/plugins/providers/virtualbox/action/check_virtualbox.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + # Checks that VirtualBox is installed and ready to be used. + class CheckVirtualbox + def initialize(app, env) + @app = app + end + + def call(env) + # This verifies that VirtualBox is installed and the driver is + # ready to function. If not, then an exception will be raised + # which will break us out of execution of the middleware sequence. + Driver::Meta.new.verify! + + # Carry on. + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index d5c3c130a..d136209c0 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -15,6 +15,8 @@ module VagrantPlugins end end + autoload :Action, File.expand_path("../action", __FILE__) + # Drop some autoloads in here to optimize the performance of loading # our drivers only when they are needed. module Driver diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index eda1de5bb..9bbe2a090 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -6,8 +6,30 @@ module VagrantPlugins @driver = Driver::Meta.new(@machine.id) end + # @see Vagrant::Plugin::V1::Provider#action + def action(name) + # Attempt to get the action method from the Action class if it + # exists, otherwise return nil to show that we don't support the + # given action. + action_method = "action_#{name}" + return Action.send(action_method) if Action.respond_to?(action_method) + nil + end + + # Returns a human-friendly string version of this provider which + # includes the machine's ID that this provider represents, if it + # has one. + # + # @return [String] + def to_s + id = @machine.id ? @machine.id : "new VM" + "VirtualBox (#{id})" + end + # Return the state of VirtualBox virtual machine by actually # querying VBoxManage. + # + # @return [Symbol] def state return :not_created if !@driver.uuid state = @driver.read_state diff --git a/test/unit/vagrant/action/runner_test.rb b/test/unit/vagrant/action/runner_test.rb index fedd89fc4..3d4a6af13 100644 --- a/test/unit/vagrant/action/runner_test.rb +++ b/test/unit/vagrant/action/runner_test.rb @@ -1,13 +1,7 @@ require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Runner do - let(:registry) do - d = double("registry") - d.stub(:get) - d - end - - let(:instance) { described_class.new(registry) } + let(:instance) { described_class.new } it "should raise an error if an invalid callable is given" do expect { instance.run(7) }.to raise_error(ArgumentError, /must be a callable/) @@ -47,7 +41,7 @@ describe Vagrant::Action::Runner do result = env["data"] end - instance = described_class.new(registry, "data" => "bar") + instance = described_class.new("data" => "bar") instance.run(callable) result.should == "bar" end @@ -58,7 +52,7 @@ describe Vagrant::Action::Runner do result = env["data"] end - instance = described_class.new(registry) { { "data" => "bar" } } + instance = described_class.new { { "data" => "bar" } } instance.run(callable) result.should == "bar" end diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index bba1fa57b..4a79ced92 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -93,16 +93,6 @@ describe Vagrant::Environment do end end - describe "action registry" do - it "has an action registry" do - instance.action_registry.should be_kind_of(Vagrant::Registry) - end - - it "should have the built-in actions in the registry" do - instance.action_registry.get(:provision).should_not be_nil - end - end - describe "primary VM" do it "should be the only VM if not a multi-VM environment" do instance.primary_vm.should == instance.vms.values.first From 90517a0f9bc28b70c3819f2d6d86596e9779f0ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jul 2012 23:56:47 -0700 Subject: [PATCH 13/60] The `Call` built-in middleware allows for conditional MW sequences. Read the documentation for more information. --- lib/vagrant/action.rb | 7 +- lib/vagrant/action/builtin/call.rb | 54 +++++++++++++++ lib/vagrant/action/runner.rb | 4 ++ plugins/providers/virtualbox/action.rb | 16 ++++- .../providers/virtualbox/action/created.rb | 20 ++++++ test/unit/vagrant/action/builtin/call_test.rb | 66 +++++++++++++++++++ test/unit/vagrant/action/runner_test.rb | 12 ++++ 7 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 lib/vagrant/action/builtin/call.rb create mode 100644 plugins/providers/virtualbox/action/created.rb create mode 100644 test/unit/vagrant/action/builtin/call_test.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 8b816ae3d..aeb44ce29 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -2,7 +2,6 @@ require 'vagrant/action/builder' module Vagrant module Action - autoload :Builtin, 'vagrant/action/builtin' autoload :Environment, 'vagrant/action/environment' autoload :Runner, 'vagrant/action/runner' autoload :Warden, 'vagrant/action/warden' @@ -13,6 +12,12 @@ module Vagrant autoload :Verify, 'vagrant/action/box/verify' end + # Builtin contains middleware classes that are shipped with Vagrant-core + # and are thus available to all plugins as a "standard library" of sorts. + module Builtin + autoload :Call, "vagrant/action/builtin/call" + end + module Env autoload :Set, 'vagrant/action/env/set' end diff --git a/lib/vagrant/action/builtin/call.rb b/lib/vagrant/action/builtin/call.rb new file mode 100644 index 000000000..1b7065be7 --- /dev/null +++ b/lib/vagrant/action/builtin/call.rb @@ -0,0 +1,54 @@ +module Vagrant + module Action + module Builtin + # This middleware class allows a sort of "conditional" run within + # a single middlware sequence. It takes another middleware runnable, + # runs it with the same environment, then yields the result to a block, + # allowing that block to determine the next course of action in the + # middleware sequence. + # + # The first argument to this middleware sequence is anywhere middleware + # runnable, whether it be a class, lambda, or something else that + # responds to `call`. This middleware runnable is run with the same + # environment as this class. The "result" of the run is expected to be + # placed in `env[:result]`. + # + # After running, {Call} takes `env[:result]` and yields it to a block + # given to initialize the class, along with an instance of {Builder}. + # The result is used to build up a new sequence on the given builder. + # This builder is then run. + class Call + # For documentation, read the description of the {Call} class. + # + # @param [Object] callable A valid middleware runnable object. This + # can be a class, a lambda, or an object that responds to `call`. + # @yield [result, builder] This block is expected to build on `builder` + # which is the next middleware sequence that will be run. + def initialize(app, env, callable, &block) + raise ArgumentError, "A block must be given to Call" if !block + + @app = app + @callable = callable + @block = block + end + + def call(env) + runner = Runner.new + + # Run our callable with our environment + new_env = runner.run(@callable, env) + + # Build our new builder based on the result + builder = Builder.new + @block.call(new_env[:result], builder) + + # Run the result with our new environment + final_env = runner.run(builder, new_env) + + # Call the next step using our final environment + @app.call(final_env) + end + end + end + end +end diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index 832a38872..b25796b38 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -45,6 +45,10 @@ module Vagrant # We place a process lock around every action that is called @logger.info("Running action: #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } + + # Return the environment in case there are things in there that + # the caller wants to use. + environment end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 29b5e7a54..701868529 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -5,14 +5,26 @@ module VagrantPlugins module Action autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) + autoload :Created, File.expand_path("../action/created", __FILE__) + + # Include the built-in modules so that we can use them as top-level + # things. + include Vagrant::Action::Builtin # This is the action that is primarily responsible for completely # freeing the resources of the underlying virtual machine. def self.action_destroy Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox - b.use Vagrant::Action::General::Validate - b.use CheckAccessible + b.use Call, Created do |result, b2| + # `result` is a boolean true/false of whether the VM is created or + # not. So if the VM _is_ created, then we continue with the + # destruction. + if result + b2.use Vagrant::Action::General::Validate + b2.use CheckAccessible + end + end end end end diff --git a/plugins/providers/virtualbox/action/created.rb b/plugins/providers/virtualbox/action/created.rb new file mode 100644 index 000000000..804801459 --- /dev/null +++ b/plugins/providers/virtualbox/action/created.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Created + def initialize(app, env) + @app = app + end + + def call(env) + # Set the result to be true if the machine is created. + env[:result] = env[:machine].state != :not_created + + # Call the next if we have one (but we shouldn't, since this + # middleware is built to run with the Call-type middlewares) + @app.call(env) + end + end + end + end +end diff --git a/test/unit/vagrant/action/builtin/call_test.rb b/test/unit/vagrant/action/builtin/call_test.rb new file mode 100644 index 000000000..1dd8ea1cd --- /dev/null +++ b/test/unit/vagrant/action/builtin/call_test.rb @@ -0,0 +1,66 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::Call do + let(:app) { lambda { |env| } } + let(:env) { {} } + + it "should yield the result to the block" do + received = nil + + callable = lambda do |env| + env[:result] = "value" + end + + described_class.new(app, env, callable) do |result, builder| + received = result + end.call({}) + + received.should == "value" + end + + it "should give a nil result if no result is given" do + received = 42 + callable = lambda { |env| } + + described_class.new(app, env, callable) do |result, builder| + received = result + end.call({}) + + received.should be_nil + end + + it "should call the callable with the original environment" do + received = nil + callable = lambda { |env| received = env[:foo] } + + described_class.new(app, env, callable) do |result, builder| + # Nothing. + end.call({ :foo => :bar }) + + received.should == :bar + end + + it "should call the next builder" do + received = nil + callable = lambda { |env| } + next_step = lambda { |env| received = "value" } + + described_class.new(app, env, callable) do |result, builder| + builder.use next_step + end.call({}) + + received.should == "value" + end + + it "should call the next builder with the original environment" do + received = nil + callable = lambda { |env| } + next_step = lambda { |env| received = env[:foo] } + + described_class.new(app, env, callable) do |result, builder| + builder.use next_step + end.call({ :foo => :bar }) + + received.should == :bar + end +end diff --git a/test/unit/vagrant/action/runner_test.rb b/test/unit/vagrant/action/runner_test.rb index 3d4a6af13..2f5ab95dd 100644 --- a/test/unit/vagrant/action/runner_test.rb +++ b/test/unit/vagrant/action/runner_test.rb @@ -25,6 +25,18 @@ describe Vagrant::Action::Runner do expect { instance.run(callable) }.to raise_error(Exception, "BOOM") end + it "should return the resulting environment" do + callable = lambda do |env| + env[:data] = "value" + + # Return nil so we can make sure it isn't using this return value + nil + end + + result = instance.run(callable) + result[:data].should == "value" + end + it "should pass options into hash given to callable" do result = nil callable = lambda do |env| From 556a53d48dee2dcece2de5d584d63d73c9600c0b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jul 2012 19:05:35 -0700 Subject: [PATCH 14/60] You can no longer set env vars on Builders via `use` --- lib/vagrant/action/builder.rb | 5 ----- test/unit/vagrant/action/builder_test.rb | 12 ------------ 2 files changed, 17 deletions(-) diff --git a/lib/vagrant/action/builder.rb b/lib/vagrant/action/builder.rb index fdd7d9a6e..00743e8b5 100644 --- a/lib/vagrant/action/builder.rb +++ b/lib/vagrant/action/builder.rb @@ -32,11 +32,6 @@ module Vagrant # # @param [Class] middleware The middleware class def use(middleware, *args, &block) - # Prepend with a environment setter if args are given - if !args.empty? && args.first.is_a?(Hash) && middleware != Env::Set - self.use(Env::Set, args.shift, &block) - end - if middleware.kind_of?(Builder) # Merge in the other builder's stack into our own self.stack.concat(middleware.stack) diff --git a/test/unit/vagrant/action/builder_test.rb b/test/unit/vagrant/action/builder_test.rb index 046166cfa..411f22b99 100644 --- a/test/unit/vagrant/action/builder_test.rb +++ b/test/unit/vagrant/action/builder_test.rb @@ -50,18 +50,6 @@ describe Vagrant::Action::Builder do two.call(data) data[:one].should == true end - - it "should be able to set additional variables when using" do - data = { } - proc1 = Proc.new { |env| env[:data] += 1 } - - # Build the first builder - one = described_class.new - one.use proc1, :data => 5 - one.call(data) - - data[:data].should == 6 - end end context "inserting" do From e20326d5778a3a21c6a44e7e2867247244543c54 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jul 2012 19:16:44 -0700 Subject: [PATCH 15/60] Added the Confirm built-in middleware This middleware asks the user a question and expects a Y/N answer. This middleware can be used with the Call middleware. --- lib/vagrant/action.rb | 1 + lib/vagrant/action/builtin/confirm.rb | 28 +++++++++++++++++++ .../vagrant/action/builtin/confirm_test.rb | 21 ++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 lib/vagrant/action/builtin/confirm.rb create mode 100644 test/unit/vagrant/action/builtin/confirm_test.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index aeb44ce29..9cc2641c4 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -16,6 +16,7 @@ module Vagrant # and are thus available to all plugins as a "standard library" of sorts. module Builtin autoload :Call, "vagrant/action/builtin/call" + autoload :Confirm, "vagrant/action/builtin/confirm" end module Env diff --git a/lib/vagrant/action/builtin/confirm.rb b/lib/vagrant/action/builtin/confirm.rb new file mode 100644 index 000000000..d877f9dea --- /dev/null +++ b/lib/vagrant/action/builtin/confirm.rb @@ -0,0 +1,28 @@ +module Vagrant + module Action + module Builtin + # This class asks the user to confirm some sort of question with + # a "Y/N" question. The only parameter is the text to ask the user. + # The result is placed in `env[:result]` so that it can be used + # with the {Call} class. + class Confirm + # For documentation, read the description of the {Confirm} class. + # + # @param [String] message The message to ask the user. + def initialize(app, env, message) + @app = app + @message = message + end + + def call(env) + # Ask the user the message and store the result + choice = nil + choice = env[:ui].ask(@message) + env[:result] = choice && choice.upcase == "Y" + + @app.call(env) + end + end + end + end +end diff --git a/test/unit/vagrant/action/builtin/confirm_test.rb b/test/unit/vagrant/action/builtin/confirm_test.rb new file mode 100644 index 000000000..7c290e172 --- /dev/null +++ b/test/unit/vagrant/action/builtin/confirm_test.rb @@ -0,0 +1,21 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::Confirm do + let(:app) { lambda { |env| } } + let(:env) { { :ui => double("ui") } } + let(:message) { "foo" } + + ["y", "Y"].each do |valid| + it "should set the result to true if '#{valid}' is given" do + env[:ui].should_receive(:ask).with(message).and_return(valid) + described_class.new(app, env, message).call(env) + env[:result].should be + end + end + + it "should set result to false if anything else is given" do + env[:ui].should_receive(:ask).with(message).and_return("nope") + described_class.new(app, env, message).call(env) + env[:result].should_not be + end +end From 118377e6f0de6121eb1fa9f737d729344a742533 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jul 2012 19:29:40 -0700 Subject: [PATCH 16/60] Destroy sequence asks the user for confirmation. --- lib/vagrant/action/builder.rb | 9 ++++ lib/vagrant/action/builtin/call.rb | 2 +- lib/vagrant/action/runner.rb | 2 +- plugins/commands/destroy/command.rb | 43 +------------------ plugins/providers/virtualbox/action.rb | 26 ++++++++--- test/unit/vagrant/action/builder_test.rb | 12 ++++++ test/unit/vagrant/action/builtin/call_test.rb | 21 ++++++--- 7 files changed, 59 insertions(+), 56 deletions(-) diff --git a/lib/vagrant/action/builder.rb b/lib/vagrant/action/builder.rb index 00743e8b5..a13912403 100644 --- a/lib/vagrant/action/builder.rb +++ b/lib/vagrant/action/builder.rb @@ -17,6 +17,15 @@ module Vagrant # Vagrant::Action.run(app) # class Builder + # This is a shortcut for a middleware sequence with only one item + # in it. For a description of the arguments and the documentation, please + # see {#use} instead. + # + # @return [Builder] + def self.build(middleware, *args, &block) + new.use(middleware, *args, &block) + end + # Returns a mergeable version of the builder. If `use` is called with # the return value of this method, then the stack will merge, instead # of being treated as a separate single middleware. diff --git a/lib/vagrant/action/builtin/call.rb b/lib/vagrant/action/builtin/call.rb index 1b7065be7..f12c43954 100644 --- a/lib/vagrant/action/builtin/call.rb +++ b/lib/vagrant/action/builtin/call.rb @@ -40,7 +40,7 @@ module Vagrant # Build our new builder based on the result builder = Builder.new - @block.call(new_env[:result], builder) + @block.call(new_env, new_env[:result], builder) # Run the result with our new environment final_env = runner.run(builder, new_env) diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index b25796b38..b92d2726c 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -18,7 +18,7 @@ module Vagrant def run(callable_id, options=nil) callable = callable_id - callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class) + callable = Builder.build(callable_id) if callable_id.kind_of?(Class) raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call) # Create the initial environment with the options given diff --git a/plugins/commands/destroy/command.rb b/plugins/commands/destroy/command.rb index 1fcd14f98..278e3bda3 100644 --- a/plugins/commands/destroy/command.rb +++ b/plugins/commands/destroy/command.rb @@ -20,52 +20,11 @@ module VagrantPlugins @logger.debug("'Destroy' each target VM...") with_target_vms(argv, :reverse => true) do |vm| vm.action(:destroy) - next - - if vm.created? - # Boolean whether we should actually go through with the destroy - # or not. This is true only if the "--force" flag is set or if the - # user confirms it. - do_destroy = false - - if options[:force] - do_destroy = true - else - choice = nil - - begin - choice = @env.ui.ask(I18n.t("vagrant.commands.destroy.confirmation", - :name => vm.name)) - rescue Interrupt - # Ctrl-C was pressed (or SIGINT). We just exit immediately - # with a non-zero exit status. - return 1 - rescue Vagrant::Errors::UIExpectsTTY - # We raise a more specific error but one which basically - # means the same thing. - raise Vagrant::Errors::DestroyRequiresForce - end - - do_destroy = choice.upcase == "Y" - end - - if do_destroy - @logger.info("Destroying: #{vm.name}") - vm.destroy - else - @logger.info("Not destroying #{vm.name} since confirmation was declined.") - @env.ui.success(I18n.t("vagrant.commands.destroy.will_not_destroy", - :name => vm.name), :prefix => false) - end - else - @logger.info("Not destroying #{vm.name}, since it isn't created.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 701868529..bab80fd4a 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -16,13 +16,25 @@ module VagrantPlugins def self.action_destroy Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox - b.use Call, Created do |result, b2| - # `result` is a boolean true/false of whether the VM is created or - # not. So if the VM _is_ created, then we continue with the - # destruction. - if result - b2.use Vagrant::Action::General::Validate - b2.use CheckAccessible + b.use Call, Created do |env, created, b2| + if created + # If the VM is created, then we confirm that we want to + # destroy it. + message = I18n.t("vagrant.commands.destroy.confirmation", + :name => env[:machine].name) + confirm = Vagrant::Action::Builder.build(Confirm, message) + + b2.use Call, confirm do |_env, confirmed, b3| + if confirmed + b3.use Vagrant::Action::General::Validate + b3.use CheckAccessible + else + env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", + :name => env[:machine.name]) + end + end + else + env[:ui].info I18n.t("vagrant.commands.common.vm_not_created") end end end diff --git a/test/unit/vagrant/action/builder_test.rb b/test/unit/vagrant/action/builder_test.rb index 411f22b99..ee00e0818 100644 --- a/test/unit/vagrant/action/builder_test.rb +++ b/test/unit/vagrant/action/builder_test.rb @@ -10,6 +10,18 @@ describe Vagrant::Action::Builder do Proc.new { |env| env[:data] << data } end + context "build" do + it "should provide build as a shortcut for basic sequences" do + data = {} + proc = Proc.new { |env| env[:data] = true } + + instance = described_class.build(proc) + instance.call(data) + + data[:data].should == true + end + end + context "basic `use`" do it "should add items to the stack and make them callable" do data = {} diff --git a/test/unit/vagrant/action/builtin/call_test.rb b/test/unit/vagrant/action/builtin/call_test.rb index 1dd8ea1cd..1618773df 100644 --- a/test/unit/vagrant/action/builtin/call_test.rb +++ b/test/unit/vagrant/action/builtin/call_test.rb @@ -11,7 +11,7 @@ describe Vagrant::Action::Builtin::Call do env[:result] = "value" end - described_class.new(app, env, callable) do |result, builder| + described_class.new(app, env, callable) do |_env, result, builder| received = result end.call({}) @@ -22,7 +22,7 @@ describe Vagrant::Action::Builtin::Call do received = 42 callable = lambda { |env| } - described_class.new(app, env, callable) do |result, builder| + described_class.new(app, env, callable) do |_env, result, builder| received = result end.call({}) @@ -33,7 +33,7 @@ describe Vagrant::Action::Builtin::Call do received = nil callable = lambda { |env| received = env[:foo] } - described_class.new(app, env, callable) do |result, builder| + described_class.new(app, env, callable) do |_env, result, builder| # Nothing. end.call({ :foo => :bar }) @@ -45,7 +45,7 @@ describe Vagrant::Action::Builtin::Call do callable = lambda { |env| } next_step = lambda { |env| received = "value" } - described_class.new(app, env, callable) do |result, builder| + described_class.new(app, env, callable) do |_env, result, builder| builder.use next_step end.call({}) @@ -57,10 +57,21 @@ describe Vagrant::Action::Builtin::Call do callable = lambda { |env| } next_step = lambda { |env| received = env[:foo] } - described_class.new(app, env, callable) do |result, builder| + described_class.new(app, env, callable) do |_env, result, builder| builder.use next_step end.call({ :foo => :bar }) received.should == :bar end + + it "should yield the environment" do + received = nil + callable = lambda { |env| env[:foo] = "value" } + + described_class.new(app, env, callable) do |env, _result, _builder| + received = env[:foo] + end.call({}) + + received.should == "value" + end end From e5f250121a1e3e191d9d201d6e2a058ef50c3169 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jul 2012 19:34:46 -0700 Subject: [PATCH 17/60] Call now only yields the environment --- lib/vagrant/action/builtin/call.rb | 9 +++-- plugins/providers/virtualbox/action.rb | 12 +++---- test/unit/vagrant/action/builtin/call_test.rb | 34 ++++--------------- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/lib/vagrant/action/builtin/call.rb b/lib/vagrant/action/builtin/call.rb index f12c43954..2d29a68de 100644 --- a/lib/vagrant/action/builtin/call.rb +++ b/lib/vagrant/action/builtin/call.rb @@ -3,17 +3,16 @@ module Vagrant module Builtin # This middleware class allows a sort of "conditional" run within # a single middlware sequence. It takes another middleware runnable, - # runs it with the same environment, then yields the result to a block, + # runs it with the same environment, then yields the resulting env to a block, # allowing that block to determine the next course of action in the # middleware sequence. # # The first argument to this middleware sequence is anywhere middleware # runnable, whether it be a class, lambda, or something else that # responds to `call`. This middleware runnable is run with the same - # environment as this class. The "result" of the run is expected to be - # placed in `env[:result]`. + # environment as this class. # - # After running, {Call} takes `env[:result]` and yields it to a block + # After running, {Call} takes the environment and yields it to a block # given to initialize the class, along with an instance of {Builder}. # The result is used to build up a new sequence on the given builder. # This builder is then run. @@ -40,7 +39,7 @@ module Vagrant # Build our new builder based on the result builder = Builder.new - @block.call(new_env, new_env[:result], builder) + @block.call(new_env, builder) # Run the result with our new environment final_env = runner.run(builder, new_env) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index bab80fd4a..1e1125e15 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -16,25 +16,25 @@ module VagrantPlugins def self.action_destroy Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox - b.use Call, Created do |env, created, b2| - if created + b.use Call, Created do |env1, b2| + if env1[:result] # If the VM is created, then we confirm that we want to # destroy it. message = I18n.t("vagrant.commands.destroy.confirmation", :name => env[:machine].name) confirm = Vagrant::Action::Builder.build(Confirm, message) - b2.use Call, confirm do |_env, confirmed, b3| - if confirmed + b2.use Call, confirm do |env2, b3| + if env2[:result] b3.use Vagrant::Action::General::Validate b3.use CheckAccessible else - env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", + env2[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", :name => env[:machine.name]) end end else - env[:ui].info I18n.t("vagrant.commands.common.vm_not_created") + env1[:ui].info I18n.t("vagrant.commands.common.vm_not_created") end end end diff --git a/test/unit/vagrant/action/builtin/call_test.rb b/test/unit/vagrant/action/builtin/call_test.rb index 1618773df..c2ef76bdc 100644 --- a/test/unit/vagrant/action/builtin/call_test.rb +++ b/test/unit/vagrant/action/builtin/call_test.rb @@ -4,36 +4,25 @@ describe Vagrant::Action::Builtin::Call do let(:app) { lambda { |env| } } let(:env) { {} } - it "should yield the result to the block" do + it "should yield the env to the block" do received = nil callable = lambda do |env| env[:result] = "value" end - described_class.new(app, env, callable) do |_env, result, builder| - received = result + described_class.new(app, env, callable) do |env, builder| + received = env[:result] end.call({}) received.should == "value" end - it "should give a nil result if no result is given" do - received = 42 - callable = lambda { |env| } - - described_class.new(app, env, callable) do |_env, result, builder| - received = result - end.call({}) - - received.should be_nil - end - it "should call the callable with the original environment" do received = nil callable = lambda { |env| received = env[:foo] } - described_class.new(app, env, callable) do |_env, result, builder| + described_class.new(app, env, callable) do |_env, _builder| # Nothing. end.call({ :foo => :bar }) @@ -45,7 +34,7 @@ describe Vagrant::Action::Builtin::Call do callable = lambda { |env| } next_step = lambda { |env| received = "value" } - described_class.new(app, env, callable) do |_env, result, builder| + described_class.new(app, env, callable) do |_env, builder| builder.use next_step end.call({}) @@ -57,21 +46,10 @@ describe Vagrant::Action::Builtin::Call do callable = lambda { |env| } next_step = lambda { |env| received = env[:foo] } - described_class.new(app, env, callable) do |_env, result, builder| + described_class.new(app, env, callable) do |_env, builder| builder.use next_step end.call({ :foo => :bar }) received.should == :bar end - - it "should yield the environment" do - received = nil - callable = lambda { |env| env[:foo] = "value" } - - described_class.new(app, env, callable) do |env, _result, _builder| - received = env[:foo] - end.call({}) - - received.should == "value" - end end From 31a3a3f2e24307d3094d042b47b8283046fdf5b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Jul 2012 10:43:16 -0700 Subject: [PATCH 18/60] Start moving the halt commands over to the new provider interface --- plugins/commands/halt/command.rb | 9 +--- plugins/providers/virtualbox/action.rb | 49 +++++++++++++------ .../virtualbox/action/message_not_created.rb | 16 ++++++ 3 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 plugins/providers/virtualbox/action/message_not_created.rb diff --git a/plugins/commands/halt/command.rb b/plugins/commands/halt/command.rb index 150b7eb37..1bda63eea 100644 --- a/plugins/commands/halt/command.rb +++ b/plugins/commands/halt/command.rb @@ -22,13 +22,8 @@ module VagrantPlugins @logger.debug("Halt command: #{argv.inspect} #{options.inspect}") with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Halting #{vm.name}") - vm.halt(:force => options[:force]) - else - @logger.info("Not halting #{vm.name}, since not created.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + # XXX: "force" + vm.action(:halt) end # Success, exit status 0 diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 1e1125e15..65f929d26 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -6,6 +6,7 @@ module VagrantPlugins autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) + autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) # Include the built-in modules so that we can use them as top-level # things. @@ -17,24 +18,42 @@ module VagrantPlugins Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env1, b2| - if env1[:result] - # If the VM is created, then we confirm that we want to - # destroy it. - message = I18n.t("vagrant.commands.destroy.confirmation", - :name => env[:machine].name) - confirm = Vagrant::Action::Builder.build(Confirm, message) + if !env1[:result] + b2.use MessageNotCreated + next + end - b2.use Call, confirm do |env2, b3| - if env2[:result] - b3.use Vagrant::Action::General::Validate - b3.use CheckAccessible - else - env2[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", - :name => env[:machine.name]) - end + # If the VM is created, then we confirm that we want to + # destroy it. + message = I18n.t("vagrant.commands.destroy.confirmation", + :name => env[:machine].name) + confirm = Vagrant::Action::Builder.build(Confirm, message) + + b2.use Call, confirm do |env2, b3| + if env2[:result] + b3.use Vagrant::Action::General::Validate + b3.use CheckAccessible + else + env2[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", + :name => env[:machine.name]) end + end + end + end + end + + # This is the action that is primarily responsible for halting + # the virtual machine, gracefully or by force. + def self.action_halt + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use CheckAccessible + b2.use DiscardState + b2.use Halt else - env1[:ui].info I18n.t("vagrant.commands.common.vm_not_created") + b2.use MessageNotCreated end end end diff --git a/plugins/providers/virtualbox/action/message_not_created.rb b/plugins/providers/virtualbox/action/message_not_created.rb new file mode 100644 index 000000000..1bd792c9e --- /dev/null +++ b/plugins/providers/virtualbox/action/message_not_created.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MessageNotCreated + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.commands.common.vm_not_created") + @app.call(env) + end + end + end + end +end From 1e997f87d746a336ef5c1b304e4860a3cdefdc83 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Jul 2012 19:58:10 -0700 Subject: [PATCH 19/60] Clean up the actions a bit, move logic into actual middleware. This is a good idea because in the future it will allow plugins to properly override these behaviors. --- plugins/providers/virtualbox/action.rb | 13 ++++--------- .../virtualbox/action/destroy_confirm.rb | 16 ++++++++++++++++ .../action/message_will_not_destroy.rb | 17 +++++++++++++++++ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 plugins/providers/virtualbox/action/destroy_confirm.rb create mode 100644 plugins/providers/virtualbox/action/message_will_not_destroy.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 65f929d26..722817cab 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -6,7 +6,9 @@ module VagrantPlugins autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) + autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) + autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) # Include the built-in modules so that we can use them as top-level # things. @@ -23,19 +25,12 @@ module VagrantPlugins next end - # If the VM is created, then we confirm that we want to - # destroy it. - message = I18n.t("vagrant.commands.destroy.confirmation", - :name => env[:machine].name) - confirm = Vagrant::Action::Builder.build(Confirm, message) - - b2.use Call, confirm do |env2, b3| + b2.use Call, DestroyConfirm do |env2, b3| if env2[:result] b3.use Vagrant::Action::General::Validate b3.use CheckAccessible else - env2[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", - :name => env[:machine.name]) + b3.use MessageWillNotDestroy end end end diff --git a/plugins/providers/virtualbox/action/destroy_confirm.rb b/plugins/providers/virtualbox/action/destroy_confirm.rb new file mode 100644 index 000000000..797f9b737 --- /dev/null +++ b/plugins/providers/virtualbox/action/destroy_confirm.rb @@ -0,0 +1,16 @@ +require "vagrant/action/builtin/confirm" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class DestroyConfirm < Vagrant::Action::Builtin::Confirm + def initialize(app, env) + message = I18n.t("vagrant.commands.destroy.confirmation", + :name => env[:machine].name) + + super(app, env, message) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/message_will_not_destroy.rb b/plugins/providers/virtualbox/action/message_will_not_destroy.rb new file mode 100644 index 000000000..a80bea3c9 --- /dev/null +++ b/plugins/providers/virtualbox/action/message_will_not_destroy.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MessageWillNotDestroy + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", + :name => env[:machine].name) + @app.call(env) + end + end + end + end +end From d8cb02d55dbc1fb3d63a9723e8d3ffc878fbf9d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Aug 2012 14:22:17 -0700 Subject: [PATCH 20/60] Load the machine ID prior to initializing the provider --- lib/vagrant/machine.rb | 13 +++--- test/unit/vagrant/machine_test.rb | 75 +++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 3347e956c..16fd03584 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -43,19 +43,22 @@ module Vagrant # part of. def initialize(name, provider_cls, config, box, env) @logger = Log4r::Logger.new("vagrant::machine") - @logger.debug("Initializing machine: #{name}") - @logger.debug(" - Provider: #{provider_cls}") - @logger.debug(" - Box: #{box}") + @logger.info("Initializing machine: #{name}") + @logger.info(" - Provider: #{provider_cls}") + @logger.info(" - Box: #{box}") @name = name @box = box @config = config @env = env - @provider = provider_cls.new(self) # Read the ID, which is usually in local storage @id = nil - @id = @env.local_data[:active][@name] if @env.local_data[:active] + @id = @env.local_data[:active][@name.to_s] if @env.local_data[:active] + + # Initializes the provider last so that it has access to all the + # state we setup on this machine. + @provider = provider_cls.new(self) end # This calls an action on the provider. The provider may or may not diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 6817c034f..d21300530 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -31,24 +31,71 @@ describe Vagrant::Machine do end describe "initialization" do - it "should initialize the provider with the machine object" do - received_machine = nil + describe "provider initialization" do + # This is a helper that generates a test for provider intialization. + # This is a separate helper method because it takes a block that can + # be used to have additional tests on the received machine. + # + # @yield [machine] Yields the machine that the provider initialization + # method received so you can run additional tests on it. + def provider_init_test + received_machine = nil - provider_cls = double("provider_cls") - provider_cls.should_receive(:new) do |machine| - # Store this for later so we can verify that it is the - # one we expected to receive. - received_machine = machine + provider_cls = double("provider_cls") + provider_cls.should_receive(:new) do |machine| + # Store this for later so we can verify that it is the + # one we expected to receive. + received_machine = machine - # Verify the machine is fully ready to be used. - machine.name.should == name - machine.config.should eql(config) - machine.box.should eql(box) - machine.env.should eql(env) + # Sanity check + machine.should be + + # Yield our machine if we want to do additional tests + yield machine if block_given? + end + + # Initialize a new machine and verify that we properly receive + # the machine we expect. + instance = described_class.new(name, provider_cls, config, box, env) + received_machine.should eql(instance) end - instance = described_class.new(name, provider_cls, config, box, env) - received_machine.should eql(instance) + it "should initialize with the machine object" do + # Just run the blank test + provider_init_test + end + + it "should have the machine name setup" do + provider_init_test do |machine| + machine.name.should == name + end + end + + it "should have the machine configuration" do + provider_init_test do |machine| + machine.config.should eql(config) + end + end + + it "should have the box" do + provider_init_test do |machine| + machine.box.should eql(box) + end + end + + it "should have the environment" do + provider_init_test do |machine| + machine.env.should eql(env) + end + end + + it "should have access to the ID" do + instance.id = "foo" + + provider_init_test do |machine| + machine.id.should == "foo" + end + end end end From 9db982f7a4073854fe7398d6e7f80441bc318f93 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Aug 2012 11:16:31 -0700 Subject: [PATCH 21/60] Expose the provider via the machine object. --- lib/vagrant/machine.rb | 5 +++ plugins/providers/virtualbox/action.rb | 2 ++ .../virtualbox/action/discard_state.rb | 20 +++++++++++ plugins/providers/virtualbox/action/halt.rb | 35 +++++++++++++++++++ plugins/providers/virtualbox/provider.rb | 2 ++ test/unit/vagrant/machine_test.rb | 10 ++++++ 6 files changed, 74 insertions(+) create mode 100644 plugins/providers/virtualbox/action/discard_state.rb create mode 100644 plugins/providers/virtualbox/action/halt.rb diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 16fd03584..294e4983b 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -32,6 +32,11 @@ module Vagrant # @return [String] attr_reader :name + # The provider backing this machine. + # + # @return [Object] + attr_reader :provider + # Initialize a new machine. # # @param [String] name Name of the virtual machine. diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 722817cab..dcba609a6 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -7,6 +7,8 @@ module VagrantPlugins autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) + autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) + autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) diff --git a/plugins/providers/virtualbox/action/discard_state.rb b/plugins/providers/virtualbox/action/discard_state.rb new file mode 100644 index 000000000..b19ccbfea --- /dev/null +++ b/plugins/providers/virtualbox/action/discard_state.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class DiscardState + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].provider.state == :saved + env[:ui].info I18n.t("vagrant.actions.vm.discard_state.discarding") + env[:machine].provider.driver.discard_saved_state + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/halt.rb b/plugins/providers/virtualbox/action/halt.rb new file mode 100644 index 000000000..b4f2d8bba --- /dev/null +++ b/plugins/providers/virtualbox/action/halt.rb @@ -0,0 +1,35 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Halt + def initialize(app, env) + @app = app + end + + def call(env) + current_state = env[:machine].provider.state + if current_state == :running || current_state == :gurumeditation + # If the VM is running and we're not forcing, we can + # attempt a graceful shutdown + if current_state == :running && !env["force"] + env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") + env[:vm].guest.halt + end + + # If we're not powered off now, then force it + if env[:vm].state != :poweroff + env[:ui].info I18n.t("vagrant.actions.vm.halt.force") + env[:vm].driver.halt + end + + # Sleep for a second to verify that the VM properly + # cleans itself up + sleep 1 if !env["vagrant.test"] + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 9bbe2a090..a3180b7d4 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -1,6 +1,8 @@ module VagrantPlugins module ProviderVirtualBox class Provider < Vagrant.plugin("1", :provider) + attr_reader :driver + def initialize(machine) @machine = machine @driver = Driver::Meta.new(@machine.id) diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index d21300530..2c6d6a09c 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -96,6 +96,12 @@ describe Vagrant::Machine do machine.id.should == "foo" end end + + it "should NOT have access to the provider" do + provider_init_test do |machine| + machine.provider.should be_nil + end + end end end @@ -115,6 +121,10 @@ describe Vagrant::Machine do it "should provide access to the environment" do instance.env.should eql(env) end + + it "should provide access to the provider" do + instance.provider.should eql(provider) + end end describe "actions" do From 1a2a8b49c0d9bce90acf71fa22db823568956387 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Aug 2012 12:41:05 -0700 Subject: [PATCH 22/60] Provider API to return SSH info, must implement `ssh_info`. Since SSH is such a critical part of Vagrant, each provider must implement a method that returns the proper way to SSH into the machine. --- lib/vagrant/machine.rb | 54 +++++++++++++++ lib/vagrant/plugin/v1/provider.rb | 25 +++++++ test/unit/vagrant/machine_test.rb | 69 +++++++++++++++++++- test/unit/vagrant/plugin/v1/provider_test.rb | 8 +++ 4 files changed, 155 insertions(+), 1 deletion(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 294e4983b..600f7ad05 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -116,6 +116,60 @@ module Vagrant @id = value end + # This returns the SSH info for accessing this machine. This SSH info + # is queried from the underlying provider. This method returns `nil` if + # the machine is not ready for SSH communication. + # + # The structure of the resulting hash is guaranteed to contain the + # following structure, although it may return other keys as well + # not documented here: + # + # { + # :host => "1.2.3.4", + # :port => "22", + # :username => "mitchellh", + # :private_key_path => "/path/to/my/key" + # } + # + # Note that Vagrant makes no guarantee that this info works or is + # correct. This is simply the data that the provider gives us or that + # is configured via a Vagrantfile. It is still possible after this + # point when attempting to connect via SSH to get authentication + # errors. + # + # @return [Hash] SSH information. + def ssh_info + # First, ask the provider for their information. If the provider + # returns nil, then the machine is simply not ready for SSH, and + # we return nil as well. + info = @provider.ssh_info + return nil if info.nil? + + # Delete out the nil entries. + info.dup.each do |key, value| + info.delete(key) if value.nil? + end + + # Next, we default some fields if they weren't given to us by + # the provider. + info[:host] = @config.ssh.host if @config.ssh.host + info[:port] = @config.ssh.port if @config.ssh.port + info[:username] = @config.ssh.username if @config.ssh.username + + # We also set some fields that are purely controlled by Varant + info[:forward_agent] = @config.ssh.forward_agent + info[:forward_x11] = @config.ssh.forward_x11 + + # Set the private key path. If a specific private key is given in + # the Vagrantfile we set that. Otherwise, we use the default (insecure) + # private key, but only if the provider didn't give us one. + info[:private_key_path] = @config.ssh.private_key_path if @config.ssh.private_key_path + info[:private_key_path] = @env.default_private_key_path if !info[:private_key_path] + + # Return the final compiled SSH info data + info + end + # Returns the state of this machine. The state is queried from the # backing provider, so it can be any arbitrary symbol. # diff --git a/lib/vagrant/plugin/v1/provider.rb b/lib/vagrant/plugin/v1/provider.rb index 09fe39962..513b4c2d8 100644 --- a/lib/vagrant/plugin/v1/provider.rb +++ b/lib/vagrant/plugin/v1/provider.rb @@ -21,6 +21,31 @@ module Vagrant nil end + # This should return a hash of information that explains how to + # SSH into the machine. If the machine is not at a point where + # SSH is even possible, then `nil` should be returned. + # + # The general structure of this returned hash should be the + # following: + # + # { + # :host => "1.2.3.4", + # :port => "22", + # :username => "mitchellh", + # :private_key_path => "/path/to/my/key" + # } + # + # **Note:** Vagrant only supports private key based authentication, + # mainly for the reason that there is no easy way to exec into an + # `ssh` prompt with a password, whereas we can pass a private key + # via commandline. + # + # @return [Hash] SSH information. For the structure of this hash + # read the accompanying documentation for this method. + def ssh_info + nil + end + # This should return the state of the machine within this provider. # The state can be any symbol. # diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 2c6d6a09c..a85166437 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -11,7 +11,7 @@ describe Vagrant::Machine do obj end let(:box) { Object.new } - let(:config) { Object.new } + let(:config) { env.config.global } let(:env) do # We need to create a Vagrantfile so that this test environment # has a proper root path @@ -175,6 +175,73 @@ describe Vagrant::Machine do end end + describe "ssh info" do + describe "with the provider returning nil" do + it "should return nil if the provider returns nil" do + provider.should_receive(:ssh_info).and_return(nil) + instance.ssh_info.should be_nil + end + end + + describe "with the provider returning data" do + let(:provider_ssh_info) { {} } + + before(:each) do + provider.should_receive(:ssh_info).and_return(provider_ssh_info) + end + + [:host, :port, :username].each do |type| + it "should return the provider data if not configured in Vagrantfile" do + provider_ssh_info[type] = "foo" + instance.config.ssh.send("#{type}=", nil) + + instance.ssh_info[type].should == "foo" + end + + it "should return the Vagrantfile value over the provider data if given" do + provider_ssh_info[type] = "foo" + instance.config.ssh.send("#{type}=", "bar") + + instance.ssh_info[type].should == "bar" + end + end + + it "should set the configured forward agent settings" do + provider_ssh_info[:forward_agent] = true + instance.config.ssh.forward_agent = false + + instance.ssh_info[:forward_agent].should == false + end + + it "should set the configured forward X11 settings" do + provider_ssh_info[:forward_x11] = true + instance.config.ssh.forward_x11 = false + + instance.ssh_info[:forward_x11].should == false + end + + it "should return the provider private key if given" do + provider_ssh_info[:private_key_path] = "foo" + + instance.ssh_info[:private_key_path].should == "foo" + end + + it "should return the configured SSH key path if set" do + provider_ssh_info[:private_key_path] = "foo" + instance.config.ssh.private_key_path = "bar" + + instance.ssh_info[:private_key_path].should == "bar" + end + + it "should return the default private key path if provider and config doesn't have one" do + provider_ssh_info[:private_key_path] = nil + instance.config.ssh.private_key_path = nil + + instance.ssh_info[:private_key_path].should == instance.env.default_private_key_path + end + end + end + describe "state" do it "should query state from the provider" do state = :running diff --git a/test/unit/vagrant/plugin/v1/provider_test.rb b/test/unit/vagrant/plugin/v1/provider_test.rb index b92bd3385..2950d9bbf 100644 --- a/test/unit/vagrant/plugin/v1/provider_test.rb +++ b/test/unit/vagrant/plugin/v1/provider_test.rb @@ -7,4 +7,12 @@ describe Vagrant::Plugin::V1::Provider do it "should return nil by default for actions" do instance.action(:whatever).should be_nil end + + it "should return nil by default for ssh info" do + instance.ssh_info.should be_nil + end + + it "should return nil by default for state" do + instance.state.should be_nil + end end From 984c4f402522e54c67a1a884bb6a060434f02945 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Aug 2012 13:12:53 -0700 Subject: [PATCH 23/60] Add Util::SSH which has methods for checking key permissions and exec --- lib/vagrant.rb | 1 - lib/vagrant/ssh.rb | 128 ----------------------------- lib/vagrant/util/ssh.rb | 121 +++++++++++++++++++++++++++ test/unit/vagrant/ssh_test.rb | 30 ------- test/unit/vagrant/util/ssh_test.rb | 30 +++++++ 5 files changed, 151 insertions(+), 159 deletions(-) delete mode 100644 lib/vagrant/ssh.rb create mode 100644 lib/vagrant/util/ssh.rb delete mode 100644 test/unit/vagrant/ssh_test.rb create mode 100644 test/unit/vagrant/util/ssh_test.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index bd021743d..24af56514 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -78,7 +78,6 @@ module Vagrant autoload :Hosts, 'vagrant/hosts' autoload :Machine, 'vagrant/machine' autoload :Plugin, 'vagrant/plugin' - autoload :SSH, 'vagrant/ssh' autoload :TestHelpers, 'vagrant/test_helpers' autoload :UI, 'vagrant/ui' autoload :Util, 'vagrant/util' diff --git a/lib/vagrant/ssh.rb b/lib/vagrant/ssh.rb deleted file mode 100644 index f23b4d0f3..000000000 --- a/lib/vagrant/ssh.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'log4r' - -require 'vagrant/util/file_mode' -require 'vagrant/util/platform' -require 'vagrant/util/safe_exec' - -module Vagrant - # Manages SSH connection information as well as allows opening an - # SSH connection. - class SSH - include Util::SafeExec - - def initialize(vm) - @vm = vm - @logger = Log4r::Logger.new("vagrant::ssh") - end - - # Returns a hash of information necessary for accessing this - # virtual machine via SSH. - # - # @return [Hash] - def info - results = { - :host => @vm.config.ssh.host, - :port => @vm.config.ssh.port || @vm.driver.ssh_port(@vm.config.ssh.guest_port), - :username => @vm.config.ssh.username, - :forward_agent => @vm.config.ssh.forward_agent, - :forward_x11 => @vm.config.ssh.forward_x11 - } - - # This can happen if no port is set and for some reason Vagrant - # can't detect an SSH port. - raise Errors::SSHPortNotDetected if !results[:port] - - # Determine the private key path, which is either set by the - # configuration or uses just the built-in insecure key. - pk_path = @vm.config.ssh.private_key_path || @vm.env.default_private_key_path - results[:private_key_path] = File.expand_path(pk_path, @vm.env.root_path) - - # We need to check and fix the private key permissions - # to make sure that SSH gets a key with 0600 perms. - check_key_permissions(results[:private_key_path]) - - # Return the results - return results - end - - # Connects to the environment's virtual machine, replacing the ruby - # process with an SSH process. - # - # @param [Hash] opts Options hash - # @options opts [Boolean] :plain_mode If True, doesn't authenticate with - # the machine, only connects, allowing the user to connect. - def exec(opts={}) - # Get the SSH information and cache it here - ssh_info = info - - if Util::Platform.windows? - raise Errors::SSHUnavailableWindows, :host => ssh_info[:host], - :port => ssh_info[:port], - :username => ssh_info[:username], - :key_path => ssh_info[:private_key_path] - end - - raise Errors::SSHUnavailable if !Kernel.system("which ssh > /dev/null 2>&1") - - # If plain mode is enabled then we don't do any authentication (we don't - # set a user or an identity file) - plain_mode = opts[:plain_mode] - - options = {} - options[:host] = ssh_info[:host] - options[:port] = ssh_info[:port] - options[:username] = ssh_info[:username] - options[:private_key_path] = ssh_info[:private_key_path] - - # Command line options - command_options = ["-p", options[:port].to_s, "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=FATAL"] - - # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option - # (Also don't use it in plain mode, it'll skip user agents.) - command_options += ["-o", "IdentitiesOnly=yes"] if !(Util::Platform.solaris? || plain_mode) - - command_options += ["-i", options[:private_key_path]] if !plain_mode - command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent] - - # If there are extra options, then we append those - command_options.concat(opts[:extra_args]) if opts[:extra_args] - - if ssh_info[:forward_x11] - # Both are required so that no warnings are shown regarding X11 - command_options += ["-o", "ForwardX11=yes"] - command_options += ["-o", "ForwardX11Trusted=yes"] - end - - host_string = options[:host] - host_string = "#{options[:username]}@#{host_string}" if !plain_mode - command_options << host_string - @logger.info("Invoking SSH: #{command_options.inspect}") - safe_exec("ssh", *command_options) - end - - # Checks the file permissions for a private key, resetting them - # if needed. - def check_key_permissions(key_path) - # Windows systems don't have this issue - return if Util::Platform.windows? - - @logger.debug("Checking key permissions: #{key_path}") - stat = File.stat(key_path) - - if stat.owned? && Util::FileMode.from_octal(stat.mode) != "600" - @logger.info("Attempting to correct key permissions to 0600") - File.chmod(0600, key_path) - - stat = File.stat(key_path) - if Util::FileMode.from_octal(stat.mode) != "600" - raise Errors::SSHKeyBadPermissions, :key_path => key_path - end - end - rescue Errno::EPERM - # This shouldn't happen since we verified we own the file, but - # it is possible in theory, so we raise an error. - raise Errors::SSHKeyBadPermissions, :key_path => key_path - end - end -end diff --git a/lib/vagrant/util/ssh.rb b/lib/vagrant/util/ssh.rb new file mode 100644 index 000000000..0bac2a4a7 --- /dev/null +++ b/lib/vagrant/util/ssh.rb @@ -0,0 +1,121 @@ +require "log4r" + +require "vagrant/util/file_mode" +require "vagrant/util/platform" +require "vagrant/util/safe_exec" + +module Vagrant + module Util + # This is a class that has helpers on it for dealing with SSH. These + # helpers don't depend on any part of Vagrant except what is given + # via the parameters. + class SSH + include SafeExec + + LOGGER = Log4r::Logger.new("vagrant::util::ssh") + + # Checks that the permissions for a private key are valid, and fixes + # them if possible. SSH requires that permissions on the private key + # are 0600 on POSIX based systems. This will make a best effort to + # fix these permissions if they are not properly set. + # + # @param [Pathname] key_path The path to the private key. + def self.check_key_permissions(key_path) + # Don't do anything if we're on Windows, since Windows doesn't worry + # about key permissions. + return if Platform.windows? + + LOGGER.debug("Checking key permissions: #{key_path}") + stat = key_path.stat + + if stat.owned? && FileMode.from_octal(stat.mode) != "600" + LOGGER.info("Attempting to correct key permissions to 0600") + key_path.chmod(0600) + + # Re-stat the file to get the new mode, and verify it worked + stat = key_path.stat + if FileMode.from_octal(stat.mode) != "600" + raise Errors::SSHKeyBadPermissions, :key_path => key_path + end + end + rescue Errno::EPERM + # This shouldn't happen since we verify we own the file, but + # it is possible in theory, so we raise an error. + raise Errors::SSHKeyBadPermissions, :key_path => key_path + end + + # Halts the running of this process and replaces it with a full-fledged + # SSH shell into a remote machine. + # + # Note: This method NEVER returns. The process ends after this. + # + # @param [Hash] ssh_info This is the SSH information. For the keys + # required please see the documentation of {Machine#ssh_info}. + # @param [Hash] opts These are additional options that are supported + # by exec. + def exec(ssh_info, opts={}) + # If we're running Windows, raise an exception since we currently + # still don't support exec-ing into SSH. In the future this should + # certainly be possible if we can detect we're in an environment that + # supports it. + if Platform.windows? + raise Errors::SSHUnavailableWindows, + :host => ssh_info[:host], + :port => ssh_info[:port], + :username => ssh_info[:username], + :key_path => ssh_info[:private_key_path] + end + + # Verify that we have SSH available on the system. + raise Errors::SSHUnavailable if !Kernel.system("which ssh > /dev/null 2>&1") + + # If plain mode is enabled then we don't do any authentication (we don't + # set a user or an identity file) + plain_mode = opts[:plain_mode] + + options = {} + options[:host] = ssh_info[:host] + options[:port] = ssh_info[:port] + options[:username] = ssh_info[:username] + options[:private_key_path] = ssh_info[:private_key_path] + + # Command line options + command_options = [ + "-p", options[:port].to_s, + "-o", "LogLevel=FATAL", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null"] + + # Configurables + command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent] + command_options.concat(opts[:extra_args]) if opts[:extra_args] + + # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the + # IdentitiesOnly option. Also, we don't enable it in plain mode so + # that SSH properly searches our identities and tries to do it itself. + if !Platform.solaris? && !plain_mode + command_options += ["-o", "IdentitiesOnly=yes"] + end + + # If we're not in plain mode, attach the private key path. + command_options += ["-i", options[:private_key_path]] if !plain_mode + + if ssh_info[:forward_x11] + # Both are required so that no warnings are shown regarding X11 + command_options += [ + "-o", "ForwardX11=yes", + "-o", "ForwardX11Trusted=yes"] + end + + # Build up the host string for connecting + host_string = options[:host] + host_string = "#{options[:username]}@#{host_string}" if !plain_mode + command_options << host_string + + # Invoke SSH with all our options + @logger.info("Invoking SSH: #{command_options.inspect}") + safe_exec("ssh", *command_options) + end + end + end +end diff --git a/test/unit/vagrant/ssh_test.rb b/test/unit/vagrant/ssh_test.rb deleted file mode 100644 index 268222d95..000000000 --- a/test/unit/vagrant/ssh_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require File.expand_path("../../base", __FILE__) - -describe Vagrant::SSH do - context "check_key_permissions" do - let(:key_path) do - # We create a tempfile to guarantee some level of uniqueness - # then explicitly close/unlink but save the path so we can re-use - temp = Tempfile.new("vagrant") - result = Pathname.new(temp.path) - temp.close - temp.unlink - - result - end - - let(:ssh_instance) { Vagrant::SSH.new(double) } - - it "should not raise an exception if we set a keyfile permission correctly" do - # Write some stuff to our key file and chmod it to some - # incorrect permissions. - key_path.open("w") { |f| f.write("hello!") } - key_path.chmod(0644) - - # This should work! - expect { ssh_instance.check_key_permissions(key_path) }. - to_not raise_error - end - end -end - diff --git a/test/unit/vagrant/util/ssh_test.rb b/test/unit/vagrant/util/ssh_test.rb new file mode 100644 index 000000000..30bfe0d5e --- /dev/null +++ b/test/unit/vagrant/util/ssh_test.rb @@ -0,0 +1,30 @@ +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/platform" +require "vagrant/util/ssh" + +describe Vagrant::Util::SSH do + include_context "unit" + + describe "checking key permissions" do + let(:key_path) { temporary_file } + + it "should do nothing on Windows" do + Vagrant::Util::Platform.stub(:windows?).and_return(true) + + key_path.chmod(0700) + + # Get the mode now and verify that it is untouched afterwards + mode = key_path.stat.mode + described_class.check_key_permissions(key_path) + key_path.stat.mode.should == mode + end + + it "should fix the permissions" do + key_path.chmod(0644) + + described_class.check_key_permissions(key_path) + key_path.stat.mode.should == 0100600 + end + end +end From a1145615d04fa75e7b34ac560aa208d7dda9bb06 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Aug 2012 13:16:08 -0700 Subject: [PATCH 24/60] Machine#action supports passing in extra env vars for action runner --- lib/vagrant/machine.rb | 5 +++-- test/unit/vagrant/machine_test.rb | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 600f7ad05..bdab72f3d 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -70,7 +70,7 @@ module Vagrant # actually implement the action. # # @param [Symbol] name Name of the action to run. - def action(name) + def action(name, extra_env=nil) @logger.debug("Calling action: #{name} on provider #{@provider}") # Get the callable from the provider. @@ -85,7 +85,8 @@ module Vagrant end # Run the action with the action runner on the environment - @env.action_runner.run(callable, :machine => self) + env = { :machine => self }.merge(extra_env || {}) + @env.action_runner.run(callable, env) end # This sets the unique ID associated with this machine. This will diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index a85166437..34adb75b5 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -149,6 +149,17 @@ describe Vagrant::Machine do machine.should eql(instance) end + it "should pass any extra options to the environment" do + action_name = :up + foo = nil + callable = lambda { |env| foo = env[:foo] } + + provider.stub(:action).with(action_name).and_return(callable) + instance.action(:up, :foo => :bar) + + foo.should == :bar + end + it "should raise an exception if the action is not implemented" do action_name = :up From e0ec679838264996ce46afcb314673969eb14f42 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Aug 2012 13:45:24 -0700 Subject: [PATCH 25/60] `vagrant ssh` with full console works with new provider. This works by now calling the `:ssh` action on the provider. This action is allowed to do whatever it pleases, but should at some point probably call the `SSHExec` built-in middleware. The `SSHExec` built-in middleware was added. This uses the information returned by `Machine#ssh_info` and uses the `Vagrant::Util::SSH` helper to exec into the remote machine. The provider should do any work upfront in verifying that the machine is ready to be SSHed into. --- lib/vagrant/action.rb | 1 + lib/vagrant/action/builtin/ssh_exec.rb | 37 +++++++++++++++++++ lib/vagrant/machine.rb | 3 ++ lib/vagrant/util/safe_exec.rb | 4 +- lib/vagrant/util/ssh.rb | 10 ++--- plugins/commands/ssh/command.rb | 8 ++-- plugins/providers/virtualbox/action.rb | 13 +++++++ .../virtualbox/action/check_created.rb | 21 +++++++++++ .../virtualbox/action/check_running.rb | 21 +++++++++++ plugins/providers/virtualbox/provider.rb | 31 ++++++++++++---- .../vagrant/action/builtin/ssh_exec_test.rb | 22 +++++++++++ 11 files changed, 151 insertions(+), 20 deletions(-) create mode 100644 lib/vagrant/action/builtin/ssh_exec.rb create mode 100644 plugins/providers/virtualbox/action/check_created.rb create mode 100644 plugins/providers/virtualbox/action/check_running.rb create mode 100644 test/unit/vagrant/action/builtin/ssh_exec_test.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 9cc2641c4..91a2091cf 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -17,6 +17,7 @@ module Vagrant module Builtin autoload :Call, "vagrant/action/builtin/call" autoload :Confirm, "vagrant/action/builtin/confirm" + autoload :SSHExec, "vagrant/action/builtin/ssh_exec" end module Env diff --git a/lib/vagrant/action/builtin/ssh_exec.rb b/lib/vagrant/action/builtin/ssh_exec.rb new file mode 100644 index 000000000..3c54a6614 --- /dev/null +++ b/lib/vagrant/action/builtin/ssh_exec.rb @@ -0,0 +1,37 @@ +require "vagrant/util/ssh" + +module Vagrant + module Action + module Builtin + # This class will exec into a full fledged SSH console into the + # remote machine. This middleware assumes that the VM is running and + # ready for SSH, and uses the {Machine#ssh_info} method to retrieve + # SSH information necessary to connect. + # + # Note: If there are any middleware after `SSHExec`, they will **not** + # run, since exec replaces the currently running process. + class SSHExec + # For quick access to the `SSH` class. + include Vagrant::Util + + def initialize(app, env) + @app = app + end + + def call(env) + # Grab the SSH info from the machine + info = env[:machine].ssh_info + # XXX: Raise an exception if info is nil, since that means that + # SSH is not ready. + + # Check the SSH key permissions + SSH.check_key_permissions(info[:private_key_path]) + + # Exec! + # XXX: Options given in env + SSH.exec(info) + end + end + end + end +end diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index bdab72f3d..b8b0438ed 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -70,6 +70,9 @@ module Vagrant # actually implement the action. # # @param [Symbol] name Name of the action to run. + # @param [Hash] extra_env This data will be passed into the action runner + # as extra data set on the environment hash for the middleware + # runner. def action(name, extra_env=nil) @logger.debug("Calling action: #{name} on provider #{@provider}") diff --git a/lib/vagrant/util/safe_exec.rb b/lib/vagrant/util/safe_exec.rb index ccc214de6..d49a58a56 100644 --- a/lib/vagrant/util/safe_exec.rb +++ b/lib/vagrant/util/safe_exec.rb @@ -6,8 +6,8 @@ module Vagrant # This issue causes `exec` to fail if there is more than one system # thread. In that case, `safe_exec` automatically falls back to # forking. - module SafeExec - def safe_exec(command, *args) + class SafeExec + def self.exec(command, *args) # Create a list of things to rescue from. Since this is OS # specific, we need to do some defined? checks here to make # sure they exist. diff --git a/lib/vagrant/util/ssh.rb b/lib/vagrant/util/ssh.rb index 0bac2a4a7..f7c3a859e 100644 --- a/lib/vagrant/util/ssh.rb +++ b/lib/vagrant/util/ssh.rb @@ -10,8 +10,6 @@ module Vagrant # helpers don't depend on any part of Vagrant except what is given # via the parameters. class SSH - include SafeExec - LOGGER = Log4r::Logger.new("vagrant::util::ssh") # Checks that the permissions for a private key are valid, and fixes @@ -53,7 +51,7 @@ module Vagrant # required please see the documentation of {Machine#ssh_info}. # @param [Hash] opts These are additional options that are supported # by exec. - def exec(ssh_info, opts={}) + def self.exec(ssh_info, opts={}) # If we're running Windows, raise an exception since we currently # still don't support exec-ing into SSH. In the future this should # certainly be possible if we can detect we're in an environment that @@ -98,7 +96,7 @@ module Vagrant end # If we're not in plain mode, attach the private key path. - command_options += ["-i", options[:private_key_path]] if !plain_mode + command_options += ["-i", options[:private_key_path].to_s] if !plain_mode if ssh_info[:forward_x11] # Both are required so that no warnings are shown regarding X11 @@ -113,8 +111,8 @@ module Vagrant command_options << host_string # Invoke SSH with all our options - @logger.info("Invoking SSH: #{command_options.inspect}") - safe_exec("ssh", *command_options) + LOGGER.info("Invoking SSH: #{command_options.inspect}") + SafeExec.exec("ssh", *command_options) end end end diff --git a/plugins/commands/ssh/command.rb b/plugins/commands/ssh/command.rb index 6f5aa7b3f..9adf92bef 100644 --- a/plugins/commands/ssh/command.rb +++ b/plugins/commands/ssh/command.rb @@ -38,9 +38,9 @@ module VagrantPlugins # Execute the actual SSH with_target_vms(argv, :single_target => true) do |vm| # Basic checks that are required for proper SSH - raise Vagrant::Errors::VMNotCreatedError if !vm.created? - raise Vagrant::Errors::VMInaccessible if !vm.state == :inaccessible - raise Vagrant::Errors::VMNotRunningError if vm.state != :running + # raise Vagrant::Errors::VMNotCreatedError if !vm.created? + # raise Vagrant::Errors::VMInaccessible if !vm.state == :inaccessible + # raise Vagrant::Errors::VMNotRunningError if vm.state != :running if options[:command] ssh_execute(vm, options[:command]) @@ -50,7 +50,7 @@ module VagrantPlugins :extra_args => options[:ssh_args] } - ssh_connect(vm, opts) + vm.action(:ssh, :ssh_opts => opts) end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index dcba609a6..44961e2a3 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -4,6 +4,8 @@ module VagrantPlugins module ProviderVirtualBox module Action autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) + autoload :CheckCreated, File.expand_path("../action/check_created", __FILE__) + autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) @@ -55,6 +57,17 @@ module VagrantPlugins end end end + + # This is the action that will exec into an SSH shell. + def self.action_ssh + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use CheckCreated + b.use CheckAccessible + b.use CheckRunning + b.use SSHExec + end + end end end end diff --git a/plugins/providers/virtualbox/action/check_created.rb b/plugins/providers/virtualbox/action/check_created.rb new file mode 100644 index 000000000..307cc1ed4 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_created.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + # This middleware checks that the VM is created, and raises an exception + # if it is not, notifying the user that creation is required. + class CheckCreated + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].state == :not_created + raise Vagrant::Errors::VMNotCreatedError + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/check_running.rb b/plugins/providers/virtualbox/action/check_running.rb new file mode 100644 index 000000000..483493602 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_running.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + # This middleware checks that the VM is running, and raises an exception + # if it is not, notifying the user that the VM must be running. + class CheckRunning + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].state != :running + raise Vagrant::Errors::VMNotRunningError + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index a3180b7d4..ba41289e5 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -18,14 +18,19 @@ module VagrantPlugins nil end - # Returns a human-friendly string version of this provider which - # includes the machine's ID that this provider represents, if it - # has one. - # - # @return [String] - def to_s - id = @machine.id ? @machine.id : "new VM" - "VirtualBox (#{id})" + # Returns the SSH info for accessing the VirtualBox VM. + def ssh_info + # If the VM is not created then we cannot possibly SSH into it, so + # we return nil. + return nil if state == :not_created + + # Return what we know. The host is always "127.0.0.1" because + # VirtualBox VMs are always local. The port we try to discover + # by reading the forwarded ports. + return { + :host => "127.0.0.1", + :port => @driver.ssh_port(@machine.config.ssh.guest_port) + } end # Return the state of VirtualBox virtual machine by actually @@ -38,6 +43,16 @@ module VagrantPlugins return :unknown if !state state end + + # Returns a human-friendly string version of this provider which + # includes the machine's ID that this provider represents, if it + # has one. + # + # @return [String] + def to_s + id = @machine.id ? @machine.id : "new VM" + "VirtualBox (#{id})" + end end end end diff --git a/test/unit/vagrant/action/builtin/ssh_exec_test.rb b/test/unit/vagrant/action/builtin/ssh_exec_test.rb new file mode 100644 index 000000000..867848e3e --- /dev/null +++ b/test/unit/vagrant/action/builtin/ssh_exec_test.rb @@ -0,0 +1,22 @@ +require File.expand_path("../../../../base", __FILE__) + +require "vagrant/util/ssh" + +describe Vagrant::Action::Builtin::SSHExec do + let(:app) { lambda { |env| } } + let(:env) { { :machine => machine } } + let(:machine) do + result = double("machine") + result.stub(:ssh_info).and_return(machine_ssh_info) + result + end + let(:machine_ssh_info) { {} } + + it "should check key permissions then exec" do + pending + end + + it "should exec with the options given in `ssh_opts`" do + pending + end +end From 8d50c4774e44a7b4fa261d6581e3f2ac4de79b73 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Aug 2012 18:37:41 -0700 Subject: [PATCH 26/60] Tests for SSHExec, and forward in the ssh options --- lib/vagrant/action/builtin/ssh_exec.rb | 3 +- .../vagrant/action/builtin/ssh_exec_test.rb | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/vagrant/action/builtin/ssh_exec.rb b/lib/vagrant/action/builtin/ssh_exec.rb index 3c54a6614..300a40b25 100644 --- a/lib/vagrant/action/builtin/ssh_exec.rb +++ b/lib/vagrant/action/builtin/ssh_exec.rb @@ -28,8 +28,7 @@ module Vagrant SSH.check_key_permissions(info[:private_key_path]) # Exec! - # XXX: Options given in env - SSH.exec(info) + SSH.exec(info, env[:ssh_opts]) end end end diff --git a/test/unit/vagrant/action/builtin/ssh_exec_test.rb b/test/unit/vagrant/action/builtin/ssh_exec_test.rb index 867848e3e..f057cc393 100644 --- a/test/unit/vagrant/action/builtin/ssh_exec_test.rb +++ b/test/unit/vagrant/action/builtin/ssh_exec_test.rb @@ -11,12 +11,36 @@ describe Vagrant::Action::Builtin::SSHExec do result end let(:machine_ssh_info) { {} } + let(:ssh_klass) { Vagrant::Util::SSH } + + before(:each) do + # Stub the methods so that even if we test incorrectly, no side + # effects actually happen. + ssh_klass.stub(:check_key_permissions) + ssh_klass.stub(:exec) + end it "should check key permissions then exec" do - pending + ssh_klass.should_receive(:check_key_permissions). + with(machine_ssh_info[:private_key_path]). + once. + ordered + + ssh_klass.should_receive(:exec). + with(machine_ssh_info, nil). + once. + ordered + + described_class.new(app, env).call(env) end it "should exec with the options given in `ssh_opts`" do - pending + ssh_opts = { :foo => :bar } + + ssh_klass.should_receive(:exec). + with(machine_ssh_info, ssh_opts) + + env[:ssh_opts] = ssh_opts + described_class.new(app, env).call(env) end end From b1ced46d7cd357c8c48add1d1184b7a6e48506a4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Aug 2012 10:29:25 -0700 Subject: [PATCH 27/60] SSHExec raises proper exception if SSH is not yet ready --- lib/vagrant/action/builtin/ssh_exec.rb | 6 ++++-- lib/vagrant/errors.rb | 4 ++++ templates/locales/en.yml | 6 ++++++ test/unit/vagrant/action/builtin/ssh_exec_test.rb | 9 +++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/vagrant/action/builtin/ssh_exec.rb b/lib/vagrant/action/builtin/ssh_exec.rb index 300a40b25..372707b07 100644 --- a/lib/vagrant/action/builtin/ssh_exec.rb +++ b/lib/vagrant/action/builtin/ssh_exec.rb @@ -21,8 +21,10 @@ module Vagrant def call(env) # Grab the SSH info from the machine info = env[:machine].ssh_info - # XXX: Raise an exception if info is nil, since that means that - # SSH is not ready. + + # If the result is nil, then the machine is telling us that it is + # not yet ready for SSH, so we raise this exception. + raise Errors::SSHNotReady if info.nil? # Check the SSH key permissions SSH.check_key_permissions(info[:private_key_path]) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 6097de946..9d2597d36 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -356,6 +356,10 @@ module Vagrant error_key(:ssh_key_type_not_supported) end + class SSHNotReady < VagrantError + error_key(:ssh_not_ready) + end + class SSHPortNotDetected < VagrantError status_code(50) error_key(:ssh_port_not_detected) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index de3d55e31..eb9b04165 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -156,6 +156,12 @@ en: The private key you're attempting to use with this Vagrant box uses an unsupported encryption type. The SSH library Vagrant uses does not support this key type. Please use `ssh-rsa` or `ssh-dss` instead. + ssh_not_ready: |- + The provider for this Vagrant-managed machine is reporting that it + is not yet ready for SSH. Depending on your provider this can carry + different meanings. Make sure your machine is created and running and + try again. If you continue to get this error message, please view the + documentation for the provider you're using. ssh_port_not_detected: |- Vagrant couldn't determine the SSH port for your VM! Vagrant attempts to automatically find a forwarded port that matches your `config.ssh.guest_port` diff --git a/test/unit/vagrant/action/builtin/ssh_exec_test.rb b/test/unit/vagrant/action/builtin/ssh_exec_test.rb index f057cc393..a6ea69f47 100644 --- a/test/unit/vagrant/action/builtin/ssh_exec_test.rb +++ b/test/unit/vagrant/action/builtin/ssh_exec_test.rb @@ -20,6 +20,15 @@ describe Vagrant::Action::Builtin::SSHExec do ssh_klass.stub(:exec) end + it "should raise an exception if SSH is not ready" do + not_ready_machine = double("machine") + not_ready_machine.stub(:ssh_info).and_return(nil) + + env[:machine] = not_ready_machine + expect { described_class.new(app, env).call(env) }. + to raise_error(Vagrant::Errors::SSHNotReady) + end + it "should check key permissions then exec" do ssh_klass.should_receive(:check_key_permissions). with(machine_ssh_info[:private_key_path]). From f1c1dfad2f5b5eb16cfd3ce77d43e93256cdcb94 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Aug 2012 11:31:55 -0700 Subject: [PATCH 28/60] Some SSH command cleanup --- plugins/commands/ssh/command.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/commands/ssh/command.rb b/plugins/commands/ssh/command.rb index 9adf92bef..e84d77a9d 100644 --- a/plugins/commands/ssh/command.rb +++ b/plugins/commands/ssh/command.rb @@ -50,6 +50,7 @@ module VagrantPlugins :extra_args => options[:ssh_args] } + @logger.debug("Invoking `ssh` action on machine") vm.action(:ssh, :ssh_opts => opts) end end @@ -80,11 +81,6 @@ module VagrantPlugins # Exit with the exit status we got from executing the command exit exit_status end - - def ssh_connect(vm, opts) - @logger.debug("`exec` into ssh prompt") - vm.ssh.exec(opts) - end end end end From a1cef830e32455f12a299e6955707f0c0255c990 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Aug 2012 21:28:28 -0700 Subject: [PATCH 29/60] Add the Communicator plugin API. This allows communication protocols to be defined for the machine. This is how things like SSH will be implemented. --- lib/vagrant.rb | 18 ++-- lib/vagrant/machine.rb | 19 ++++ lib/vagrant/plugin/v1.rb | 1 + lib/vagrant/plugin/v1/communicator.rb | 89 +++++++++++++++++++ .../vagrant/plugin/v1/communicator_test.rb | 9 ++ test/unit/vagrant_test.rb | 1 + 6 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 lib/vagrant/plugin/v1/communicator.rb create mode 100644 test/unit/vagrant/plugin/v1/communicator_test.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 24af56514..550df06b1 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -85,14 +85,16 @@ module Vagrant # These are the various plugin versions and their components in # a lazy loaded Hash-like structure. - c = PLUGIN_COMPONENTS = Registry.new - c.register(:"1") { Plugin::V1::Plugin } - c.register([:"1", :command]) { Plugin::V1::Command } - c.register([:"1", :config]) { Plugin::V1::Config } - c.register([:"1", :guest]) { Plugin::V1::Guest } - c.register([:"1", :host]) { Plugin::V1::Host } - c.register([:"1", :provider]) { Plugin::V1::Provider } - c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } + PLUGIN_COMPONENTS = Registry.new.tap do |c| + c.register(:"1") { Plugin::V1::Plugin } + c.register([:"1", :command]) { Plugin::V1::Command } + c.register([:"1", :communicator]) { Plugin::V1::Communicator } + c.register([:"1", :config]) { Plugin::V1::Config } + c.register([:"1", :guest]) { Plugin::V1::Guest } + c.register([:"1", :host]) { Plugin::V1::Host } + c.register([:"1", :provider]) { Plugin::V1::Provider } + c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } + end # The source root is the path to the root directory of # the Vagrant gem. diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index b8b0438ed..5aa505f69 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -92,6 +92,25 @@ module Vagrant @env.action_runner.run(callable, env) end + # Returns a communication object for executing commands on the remote + # machine. Note that the _exact_ semantics of this are up to the + # communication provider itself. Despite this, the semantics are expected + # to be consistent across operating systems. For example, all linux-based + # systems should have similar communication (usually a shell). All + # Windows systems should have similar communication as well. Therefore, + # prior to communicating with the machine, users of this method are + # expected to check the guest OS to determine their behavior. + # + # This method will _always_ return some valid communication object. + # The `ready?` API can be used on the object to check if communication + # is actually ready. + # + # @return [Object] + def communicate + # For now, we always return SSH. In the future, we'll abstract + # this and allow plugins to define new methods of communication. + end + # This sets the unique ID associated with this machine. This will # persist this ID so that in the future Vagrant will be able to find # this machine again. The unique ID must be absolutely unique to the diff --git a/lib/vagrant/plugin/v1.rb b/lib/vagrant/plugin/v1.rb index c43e8d687..55b23e5eb 100644 --- a/lib/vagrant/plugin/v1.rb +++ b/lib/vagrant/plugin/v1.rb @@ -6,6 +6,7 @@ module Vagrant module Plugin module V1 autoload :Command, "vagrant/plugin/v1/command" + autoload :Communicator, "vagrant/plugin/v1/communicator" autoload :Config, "vagrant/plugin/v1/config" autoload :Guest, "vagrant/plugin/v1/guest" autoload :Host, "vagrant/plugin/v1/host" diff --git a/lib/vagrant/plugin/v1/communicator.rb b/lib/vagrant/plugin/v1/communicator.rb new file mode 100644 index 000000000..517f3e417 --- /dev/null +++ b/lib/vagrant/plugin/v1/communicator.rb @@ -0,0 +1,89 @@ +module Vagrant + module Plugin + module V1 + # Base class for a communicator in Vagrant. A communicator is + # responsible for communicating with a machine in some way. There + # are various stages of Vagrant that require things such as uploading + # files to the machine, executing shell commands, etc. Implementors + # of this class are expected to provide this functionality in some + # way. + # + # Note that a communicator must provide **all** of the methods + # in this base class. There is currently no way for one communicator + # to provide say a more efficient way of uploading a file, but not + # provide shell execution. This sort of thing will come in a future + # version. + class Communicator + # This returns true/false depending on if the given machine + # can be communicated with using this communicator. If this returns + # `true`, then this class will be used as the primary communication + # method for the machine. + # + # @return [Boolean] + def self.match?(machine) + false + end + + # Initializes the communicator with the machine that we will be + # communicating with. This base method does nothing (it doesn't + # even store the machine in an instance variable for you), so you're + # expected to override this and do something with the machine if + # you care about it. + # + # @param [Machine] machine The machine this instance is expected to + # communicate with. + def initialize(machine) + end + + # Checks if the target machine is ready for communication. If this + # returns true, then all the other methods for communicating with + # the machine are expected to be functional. + # + # @return [Boolean] + def ready? + false + end + + # Download a file from the remote machine to the local machine. + # + # @param [String] from Path of the file on the remote machine. + # @param [String] to Path of where to save the file locally. + def download(from, to) + end + + # Upload a file to the remote machine. + # + # @param [String] from Path of the file locally to upload. + # @param [String] to Path of where to save the file on the remote + # machine. + def upload(from, to) + end + + # Execute a command on the remote machine. The exact semantics + # of this method are up to the implementor, but in general the + # users of this class will expect this to be a shell. + # + # This method gives you no way to write data back to the remote + # machine, so only execute commands that don't expect input. + # + # @param [String] command Command to execute. + # @yield [type, data] Realtime output of the command being executed. + # @yieldparam [String] type Type of the output. This can be + # `:stdout`, `:stderr`, etc. The exact types are up to the + # implementor. + # @yieldparam [String] data Data for the given output. + # @return [Integer] Exit code of the command. + def execute(command, opts=nil) + end + + # Executes a command on the remote machine with administrative + # privileges. See {#execute} for documentation, as the API is the + # same. + # + # @see #execute + def sudo(command, opts=nil) + end + end + end + end +end diff --git a/test/unit/vagrant/plugin/v1/communicator_test.rb b/test/unit/vagrant/plugin/v1/communicator_test.rb new file mode 100644 index 000000000..a10e5a88d --- /dev/null +++ b/test/unit/vagrant/plugin/v1/communicator_test.rb @@ -0,0 +1,9 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Plugin::V1::Communicator do + let(:machine) { Object.new } + + it "should not match by default" do + described_class.match?(machine).should_not be + end +end diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index 6e555af1f..5c61a3af2 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -13,6 +13,7 @@ describe Vagrant do it "returns the proper components for version 1" do described_class.plugin("1", :command).should == Vagrant::Plugin::V1::Command + described_class.plugin("1", :communicator).should == Vagrant::Plugin::V1::Communicator described_class.plugin("1", :config).should == Vagrant::Plugin::V1::Config described_class.plugin("1", :guest).should == Vagrant::Plugin::V1::Guest described_class.plugin("1", :host).should == Vagrant::Plugin::V1::Host From 595e7cee0e61a8ea1ad0c97873f4f54b9af6c169 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Aug 2012 21:48:51 -0700 Subject: [PATCH 30/60] Move SSH communication to a plugin --- lib/vagrant.rb | 1 - lib/vagrant/communication.rb | 7 --- lib/vagrant/communication/base.rb | 63 ------------------- .../communicators/ssh/communicator.rb | 51 ++++++++------- plugins/communicators/ssh/plugin.rb | 19 ++++++ 5 files changed, 48 insertions(+), 93 deletions(-) delete mode 100644 lib/vagrant/communication.rb delete mode 100644 lib/vagrant/communication/base.rb rename lib/vagrant/communication/ssh.rb => plugins/communicators/ssh/communicator.rb (85%) create mode 100644 plugins/communicators/ssh/plugin.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 550df06b1..1e160ab47 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -66,7 +66,6 @@ module Vagrant autoload :BoxCollection, 'vagrant/box_collection' autoload :CLI, 'vagrant/cli' autoload :Command, 'vagrant/command' - autoload :Communication, 'vagrant/communication' autoload :Config, 'vagrant/config' autoload :DataStore, 'vagrant/data_store' autoload :Downloaders, 'vagrant/downloaders' diff --git a/lib/vagrant/communication.rb b/lib/vagrant/communication.rb deleted file mode 100644 index cb9795b71..000000000 --- a/lib/vagrant/communication.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Vagrant - module Communication - autoload :Base, 'vagrant/communication/base' - - autoload :SSH, 'vagrant/communication/ssh' - end -end diff --git a/lib/vagrant/communication/base.rb b/lib/vagrant/communication/base.rb deleted file mode 100644 index b16d2626f..000000000 --- a/lib/vagrant/communication/base.rb +++ /dev/null @@ -1,63 +0,0 @@ -module Vagrant - module Communication - # The base class for any classes that provide an API for communicating - # with the virtual machine. - # - # There are various stages that require Vagrant to copy files or - # run commands on the target system, and communication classes provide - # the abstraction necessary to perform these tasks, via SSH or some - # other mechanism. - # - # Any subclasses of this class **must** implement all of the methods - # below. - class Base - # Checks if the target machine is ready for communication. - # - # @return [Boolean] - def ready? - end - - # Download a file from the virtual machine to the local machine. - # - # @param [String] from Path of the file on the virtual machine. - # @param [String] to Path to where to save the remote file. - def download(from, to) - end - - # Upload a file to the virtual machine. - # - # @param [String] from Path to a file to upload. - # @param [String] to Path to where to save this file. - def upload(from, to) - end - - # Execute a command on the remote machine. - # - # @param [String] command Command to execute. - # @yield [type, data] Realtime output of the command being executed. - # @yieldparam [String] type Type of the output, `:stdout`, `:stderr`, etc. - # @yieldparam [String] data Data for the given output. - # @return [Integer] Exit code of the command. - def execute(command, opts=nil) - end - - # Execute a comand with super user privileges. - # - # See #execute for parameter information. - def sudo(command, opts=nil) - end - - # Executes a command and returns a boolean statement if it was successful - # or not. - # - # This is implemented by default as expecting `execute` to return 0. - def test(command, opts=nil) - # Disable error checking no matter what - opts = (opts || {}).merge(:error_check => false) - - # Successful if the exit status is 0 - execute(command, opts) == 0 - end - end - end -end diff --git a/lib/vagrant/communication/ssh.rb b/plugins/communicators/ssh/communicator.rb similarity index 85% rename from lib/vagrant/communication/ssh.rb rename to plugins/communicators/ssh/communicator.rb index 353260547..1065168a5 100644 --- a/lib/vagrant/communication/ssh.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -8,17 +8,23 @@ require 'vagrant/util/ansi_escape_code_remover' require 'vagrant/util/file_mode' require 'vagrant/util/platform' require 'vagrant/util/retryable' +require 'vagrant/util/ssh' -module Vagrant - module Communication - # Provides communication with the VM via SSH. - class SSH < Base +module VagrantPlugins + module CommunicatorSSH + # This class provides communication with the VM via SSH. + class Communicator < Vagrant.plugin("1", :communicator) include Util::ANSIEscapeCodeRemover include Util::Retryable - def initialize(vm) - @vm = vm - @logger = Log4r::Logger.new("vagrant::communication::ssh") + def self.match?(machine) + # All machines are currently expected to have SSH. + true + end + + def initialize(machine) + @machine = machine + @logger = Log4r::Logger.new("vagrant::communication::ssh") @connection = nil end @@ -31,7 +37,7 @@ module Vagrant # If we reached this point then we successfully connected @logger.info("SSH is ready!") true - rescue Errors::VagrantError => e + rescue Vagrant::Errors::VagrantError => e # We catch a `VagrantError` which would signal that something went # wrong expectedly in the `connect`, which means we didn't connect. @logger.info("SSH not up: #{e.inspect}") @@ -41,7 +47,7 @@ module Vagrant def execute(command, opts=nil, &block) opts = { :error_check => true, - :error_class => Errors::VagrantError, + :error_class => Vagrant::Errors::VagrantError, :error_key => :ssh_bad_exit_status, :command => command, :sudo => false @@ -93,7 +99,7 @@ module Vagrant # Otherwise, it is a permission denied, so let's raise a proper # exception - raise Errors::SCPPermissionDenied, :path => from.to_s + raise Vagrant::Errors::SCPPermissionDenied, :path => from.to_s end protected @@ -120,7 +126,8 @@ module Vagrant end end - ssh_info = @vm.ssh.info + # XXX: We need to raise some exception if SSH is not ready + ssh_info = @machine.ssh_info # Build the options we'll use to initiate the connection via Net::SSH opts = { @@ -134,38 +141,38 @@ module Vagrant } # Check that the private key permissions are valid - @vm.ssh.check_key_permissions(ssh_info[:private_key_path]) + Vagrant::Util::SSH.check_key_permissions(ssh_info[:private_key_path]) # Connect to SSH, giving it a few tries connection = nil begin exceptions = [Errno::ECONNREFUSED, Net::SSH::Disconnect, Timeout::Error] - connection = retryable(:tries => @vm.config.ssh.max_tries, :on => exceptions) do - Timeout.timeout(@vm.config.ssh.timeout) do + connection = retryable(:tries => @machine.config.ssh.max_tries, :on => exceptions) do + Timeout.timeout(@machine.config.ssh.timeout) do @logger.info("Attempting to connect to SSH: #{ssh_info[:host]}:#{ssh_info[:port]}") Net::SSH.start(ssh_info[:host], ssh_info[:username], opts) end end rescue Timeout::Error # This happens if we continued to timeout when attempting to connect. - raise Errors::SSHConnectionTimeout + raise Vagrant::Errors::SSHConnectionTimeout rescue Net::SSH::AuthenticationFailed # This happens if authentication failed. We wrap the error in our # own exception. - raise Errors::SSHAuthenticationFailed + raise Vagrant::Errors::SSHAuthenticationFailed rescue Net::SSH::Disconnect # This happens if the remote server unexpectedly closes the # connection. This is usually raised when SSH is running on the # other side but can't properly setup a connection. This is # usually a server-side issue. - raise Errors::SSHDisconnected + raise Vagrant::Errors::SSHDisconnected rescue Errno::ECONNREFUSED # This is raised if we failed to connect the max amount of times - raise Errors::SSHConnectionRefused + raise Vagrant::Errors::SSHConnectionRefused rescue NotImplementedError # This is raised if a private key type that Net-SSH doesn't support # is used. Show a nicer error. - raise Errors::SSHKeyTypeNotSupported + raise Vagrant::Errors::SSHKeyTypeNotSupported end @connection = connection @@ -178,7 +185,7 @@ module Vagrant # Yield the connection that is ready to be used and # return the value of the block return yield connection if block_given? - end + end # Executes the command on an SSH connection within a login shell. def shell_execute(connection, command, sudo=false) @@ -187,7 +194,7 @@ module Vagrant # Determine the shell to execute. If we are using `sudo` then we # need to wrap the shell in a `sudo` call. - shell = @vm.config.ssh.shell + shell = @machine.config.ssh.shell shell = "sudo -H #{shell}" if sudo # Open the channel so we can execute or command @@ -248,7 +255,7 @@ module Vagrant end rescue Net::SCP::Error => e # If we get the exit code of 127, then this means SCP is unavailable. - raise Errors::SCPUnavailable if e.message =~ /\(127\)/ + raise Vagrant::Errors::SCPUnavailable if e.message =~ /\(127\)/ # Otherwise, just raise the error up raise diff --git a/plugins/communicators/ssh/plugin.rb b/plugins/communicators/ssh/plugin.rb new file mode 100644 index 000000000..10de987a4 --- /dev/null +++ b/plugins/communicators/ssh/plugin.rb @@ -0,0 +1,19 @@ +require "vagrant" + +module VagrantPlugins + module CommunicatorSSH + class Plugin < Vagrant.plugin("1") + name "ssh communiator" + description <<-DESC + This plugin allows Vagrant to communicate with remote machines using + SSH as the underlying protocol, powered internally by Ruby's + net-ssh library. + DESC + + communicator("ssh") do + require File.expand_path("../communicator", __FILE__) + Communicator + end + end + end +end From 5ae3e0e80c66e2538f622de9c0feffcff56dadaa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Aug 2012 21:52:25 -0700 Subject: [PATCH 31/60] Allow the definition of communicators in plugins --- lib/vagrant/machine.rb | 1 + lib/vagrant/plugin/v1/plugin.rb | 18 ++++++++++++++ test/unit/vagrant/plugin/v1/plugin_test.rb | 28 ++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 5aa505f69..19de0cd2c 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -109,6 +109,7 @@ module Vagrant def communicate # For now, we always return SSH. In the future, we'll abstract # this and allow plugins to define new methods of communication. + end # This sets the unique ID associated with this machine. This will diff --git a/lib/vagrant/plugin/v1/plugin.rb b/lib/vagrant/plugin/v1/plugin.rb index abb503549..7d0e57b53 100644 --- a/lib/vagrant/plugin/v1/plugin.rb +++ b/lib/vagrant/plugin/v1/plugin.rb @@ -103,6 +103,24 @@ module Vagrant data[:command] end + # Defines additional communicators to be available. Communicators + # should be returned by a block passed to this method. This is done + # to ensure that the class is lazy loaded, so if your class inherits + # from or uses any Vagrant internals specific to Vagrant 1.0, then + # the plugin can still be defined without breaking anything in future + # versions of Vagrant. + # + # @param [String] name Communicator name. + def self.communicator(name=UNSET_VALUE, &block) + data[:communicator] ||= Registry.new + + # Register a new communicator class only if a name was given. + data[:communicator].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:communicator] + end + # Defines additional configuration keys to be available in the # Vagrantfile. The configuration class should be returned by a # block passed to this method. This is done to ensure that the class diff --git a/test/unit/vagrant/plugin/v1/plugin_test.rb b/test/unit/vagrant/plugin/v1/plugin_test.rb index 6a20f14b8..df1d5ccc2 100644 --- a/test/unit/vagrant/plugin/v1/plugin_test.rb +++ b/test/unit/vagrant/plugin/v1/plugin_test.rb @@ -100,6 +100,34 @@ describe Vagrant::Plugin::V1::Plugin do end end + describe "communicators" do + it "should register communicator classes" do + plugin = Class.new(described_class) do + communicator("foo") { "bar" } + end + + plugin.communicator[:foo].should == "bar" + end + + it "should lazily register communicator classes" do + # Below would raise an error if the value of the class was + # evaluated immediately. By asserting that this does not raise an + # error, we verify that the value is actually lazily loaded + plugin = nil + expect { + plugin = Class.new(described_class) do + communicator("foo") { raise StandardError, "FAIL!" } + end + }.to_not raise_error + + # Now verify when we actually get the configuration key that + # a proper error is raised. + expect { + plugin.communicator[:foo] + }.to raise_error(StandardError) + end + end + describe "configuration" do it "should register configuration classes" do plugin = Class.new(described_class) do From 64afd578b3e0baace6243e587c0f2117bcb973d0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Aug 2012 21:56:22 -0700 Subject: [PATCH 32/60] Just always return the SSH communicator for machines for now. In the future we'll actually find a matching communicator but for now since we're just focusing on machine abstraction, we just return SSH. --- lib/vagrant/machine.rb | 15 +++++++++++++-- plugins/communicators/ssh/communicator.rb | 4 ++-- test/unit/vagrant/machine_test.rb | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 19de0cd2c..70498bf56 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -107,9 +107,20 @@ module Vagrant # # @return [Object] def communicate - # For now, we always return SSH. In the future, we'll abstract - # this and allow plugins to define new methods of communication. + if !@communicator + # For now, we always return SSH. In the future, we'll abstract + # this and allow plugins to define new methods of communication. + Vagrant.plugin("1").registered.each do |plugin| + klass = plugin.communicator[:ssh] + if klass + # This plugin has the SSH communicator, use it. + @communicator = klass.new(self) + break + end + end + end + @communicator end # This sets the unique ID associated with this machine. This will diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index 1065168a5..a66ee3a33 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -14,8 +14,8 @@ module VagrantPlugins module CommunicatorSSH # This class provides communication with the VM via SSH. class Communicator < Vagrant.plugin("1", :communicator) - include Util::ANSIEscapeCodeRemover - include Util::Retryable + include Vagrant::Util::ANSIEscapeCodeRemover + include Vagrant::Util::Retryable def self.match?(machine) # All machines are currently expected to have SSH. diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 34adb75b5..eaf33ba2d 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -170,6 +170,17 @@ describe Vagrant::Machine do end end + describe "communicator" do + it "should always return the SSH communicator" do + instance.communicate.should be_kind_of(VagrantPlugins::CommunicatorSSH::Communicator) + end + + it "should memoize the result" do + obj = instance.communicate + instance.communicate.should eql(obj) + end + end + describe "setting the ID" do it "should not have an ID by default" do instance.id.should be_nil From 2e2528529772559a1ee2888a9c529374ffa46a4e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Aug 2012 00:38:11 -0700 Subject: [PATCH 33/60] Add nice inspect results for Environment and Machine --- lib/vagrant/environment.rb | 8 ++++++++ lib/vagrant/machine.rb | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 532d1d030..7be9e476e 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -104,6 +104,14 @@ module Vagrant activate_plugins end + # Return a human-friendly string for pretty printed or inspected + # instances. + # + # @return [String] + def inspect + "#<#{self.class}: #{@cwd}>" + end + #--------------------------------------------------------------- # Helpers #--------------------------------------------------------------- diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 70498bf56..2f3173901 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -151,6 +151,14 @@ module Vagrant @id = value end + # This returns a clean inspect value so that printing the value via + # a pretty print (`p`) results in a readable value. + # + # @return [String] + def inspect + "#<#{self.class}: #{@name} (#{@provider.class})>" + end + # This returns the SSH info for accessing this machine. This SSH info # is queried from the underlying provider. This method returns `nil` if # the machine is not ready for SSH communication. From 5e70ad0ec20d83fdb874350add97056447c5ff5c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Aug 2012 00:57:23 -0700 Subject: [PATCH 34/60] `vagrant ssh -c` now uses a middleware sequence --- lib/vagrant/action.rb | 1 + lib/vagrant/action/builtin/ssh_run.rb | 43 ++++++++++++++++++++++++++ plugins/commands/ssh/command.rb | 32 ++----------------- plugins/providers/virtualbox/action.rb | 11 +++++++ 4 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 lib/vagrant/action/builtin/ssh_run.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 91a2091cf..ee28a6b93 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -18,6 +18,7 @@ module Vagrant autoload :Call, "vagrant/action/builtin/call" autoload :Confirm, "vagrant/action/builtin/confirm" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" + autoload :SSHRun, "vagrant/action/builtin/ssh_run" end module Env diff --git a/lib/vagrant/action/builtin/ssh_run.rb b/lib/vagrant/action/builtin/ssh_run.rb new file mode 100644 index 000000000..1969600eb --- /dev/null +++ b/lib/vagrant/action/builtin/ssh_run.rb @@ -0,0 +1,43 @@ +require "log4r" + +module Vagrant + module Action + module Builtin + # This class will run a single command on the remote machine and will + # mirror the output to the UI. The resulting exit status of the command + # will exist in the `:ssh_run_exit_status` key in the environment. + class SSHRun + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::ssh_run") + end + + def call(env) + command = env[:ssh_run_command] + + @logger.debug("Executing command: #{command}") + exit_status = 0 + exit_status = env[:machine].communicate.execute(command, :error_check => false) do |type, data| + # Determine the proper channel to send the output onto depending + # on the type of data we are receiving. + channel = type == :stdout ? :out : :error + + # Print the output as it comes in, but don't prefix it and don't + # force a new line so that the output is properly preserved however + # it may be formatted. + env[:ui].info(data.to_s, + :prefix => false, + :new_line => false, + :channel => channel) + end + + # Set the exit status on a known environmental variable + env[:ssh_run_exit_status] = exit_status + + # Call the next middleware + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/ssh/command.rb b/plugins/commands/ssh/command.rb index e84d77a9d..d8e7ae4f5 100644 --- a/plugins/commands/ssh/command.rb +++ b/plugins/commands/ssh/command.rb @@ -37,13 +37,10 @@ module VagrantPlugins # Execute the actual SSH with_target_vms(argv, :single_target => true) do |vm| - # Basic checks that are required for proper SSH - # raise Vagrant::Errors::VMNotCreatedError if !vm.created? - # raise Vagrant::Errors::VMInaccessible if !vm.state == :inaccessible - # raise Vagrant::Errors::VMNotRunningError if vm.state != :running - if options[:command] - ssh_execute(vm, options[:command]) + # XXX: Exit with proper exit status + @logger.debug("Executing single command on remote machine: #{options[:command]}") + vm.action(:ssh_run, :ssh_run_command => options[:command]) else opts = { :plain_mode => options[:plain_mode], @@ -57,29 +54,6 @@ module VagrantPlugins # Success, exit status 0 0 - end - - protected - - def ssh_execute(vm, command=nil) - exit_status = 0 - - @logger.debug("Executing command: #{command}") - exit_status = vm.channel.execute(command, :error_check => false) do |type, data| - # Determine the proper channel to send the output onto depending - # on the type of data we are receiving. - channel = type == :stdout ? :out : :error - - # Print the SSH output as it comes in, but don't prefix it and don't - # force a new line so that the output is properly preserved - vm.ui.info(data.to_s, - :prefix => false, - :new_line => false, - :channel => channel) - end - - # Exit with the exit status we got from executing the command - exit exit_status end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 44961e2a3..804be8911 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -68,6 +68,17 @@ module VagrantPlugins b.use SSHExec end end + + # This is the action that will run a single SSH command. + def self.action_ssh_run + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use CheckCreated + b.use CheckAccessible + b.use CheckRunning + b.use SSHRun + end + end end end end From de0865b9e248c26440fc64c4dd94d036ef0e3a82 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Aug 2012 20:33:09 -0700 Subject: [PATCH 35/60] Test to ensure that the environment hash is returned from Machine#action This will allow methods that call into the action to use any results that might be set onto the environment hash by any of the middleware. --- test/unit/vagrant/machine_test.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index eaf33ba2d..06275f31c 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -160,6 +160,16 @@ describe Vagrant::Machine do foo.should == :bar end + it "should return the environment as a result" do + action_name = :up + callable = lambda { |env| env[:result] = "FOO" } + + provider.stub(:action).with(action_name).and_return(callable) + result = instance.action(action_name) + + result[:result].should == "FOO" + end + it "should raise an exception if the action is not implemented" do action_name = :up From 7efee573b822f3a28130aa28beb1289bf1006b05 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Aug 2012 20:35:34 -0700 Subject: [PATCH 36/60] `vagrant ssh -c` now exits with the proper exit code --- plugins/commands/ssh/command.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/commands/ssh/command.rb b/plugins/commands/ssh/command.rb index d8e7ae4f5..a9b73dad8 100644 --- a/plugins/commands/ssh/command.rb +++ b/plugins/commands/ssh/command.rb @@ -38,9 +38,13 @@ module VagrantPlugins # Execute the actual SSH with_target_vms(argv, :single_target => true) do |vm| if options[:command] - # XXX: Exit with proper exit status @logger.debug("Executing single command on remote machine: #{options[:command]}") - vm.action(:ssh_run, :ssh_run_command => options[:command]) + env = vm.action(:ssh_run, :ssh_run_command => options[:command]) + + # Exit with the exit status of the command or a 0 if we didn't + # get one. + exit_status = env[:ssh_run_exit_status] || 0 + return exit_status else opts = { :plain_mode => options[:plain_mode], @@ -49,11 +53,13 @@ module VagrantPlugins @logger.debug("Invoking `ssh` action on machine") vm.action(:ssh, :ssh_opts => opts) + + # We should never reach this point, since the point of `ssh` + # is to exec into the proper SSH shell, but we'll just return + # an exit status of 0 anyways. + return 0 end end - - # Success, exit status 0 - 0 end end end From 28f341ec75318394c9d779b2897ba728cf25b04e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Aug 2012 16:46:00 -0700 Subject: [PATCH 37/60] Begin working on the #guest method for Machine instances --- lib/vagrant/errors.rb | 4 ++++ lib/vagrant/machine.rb | 10 ++++++++++ templates/locales/en.yml | 4 ++++ test/unit/vagrant/machine_test.rb | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 9d2597d36..69d6c5163 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -226,6 +226,10 @@ module Vagrant error_key(:port_collision_resume) end + class MachineGuestNotReady < VagrantError + error_key(:machine_guest_not_ready) + end + class MultiVMEnvironmentRequired < VagrantError status_code(5) error_key(:multi_vm_required) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 2f3173901..43abe1b5e 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -123,6 +123,16 @@ module Vagrant @communicator end + # Returns a guest implementation for this machine. The guest implementation + # knows how to do guest-OS specific tasks, such as configuring networks, + # mounting folders, etc. + # + # @return [Object] + def guest + raise Errors::MachineGuestNotReady if !communicator.ready? + # XXX: Todo + end + # This sets the unique ID associated with this machine. This will # persist this ID so that in the future Vagrant will be able to find # this machine again. The unique ID must be absolutely unique to the diff --git a/templates/locales/en.yml b/templates/locales/en.yml index eb9b04165..36c17aa3c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -85,6 +85,10 @@ en: You specified: %{home_path} interrupted: |- Vagrant exited after cleanup due to external interrupt. + machine_guest_not_ready: |- + Guest-specific operations were attempted on a machine that is not + ready for guest communication. This should not happen and a bug + should be reported. multi_vm_required: |- A multi-vm environment is required for name specification to this command. multi_vm_target_required: |- diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 06275f31c..a8ccc5ab8 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -191,6 +191,25 @@ describe Vagrant::Machine do end end + describe "guest implementation" do + let(:communicator) do + result = double("communicator") + result.stub(:ready?).and_return(true) + result + end + + before(:each) do + instance.stub(:communicator).and_return(communicator) + end + + it "should raise an exception if communication is not ready" do + communicator.should_receive(:ready?).and_return(false) + + expect { instance.guest }. + to raise_error(Vagrant::Errors::MachineGuestNotReady) + end + end + describe "setting the ID" do it "should not have an ID by default" do instance.id.should be_nil From f9752d78d8113bfdb410f9ee12745126149e2eb0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Aug 2012 18:35:19 -0700 Subject: [PATCH 38/60] Properly resolve and load the guest class impl for Machines --- lib/vagrant/machine.rb | 44 ++++++++++++++++++++- lib/vagrant/plugin/v1/guest.rb | 5 +-- plugins/providers/virtualbox/action/halt.rb | 6 +-- test/unit/support/shared/base_context.rb | 26 ++++++++++++ test/unit/vagrant/machine_test.rb | 43 +++++++++++++++++++- 5 files changed, 115 insertions(+), 9 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 43abe1b5e..f2c74fd57 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -129,8 +129,21 @@ module Vagrant # # @return [Object] def guest - raise Errors::MachineGuestNotReady if !communicator.ready? - # XXX: Todo + raise Errors::MachineGuestNotReady if !communicate.ready? + + # Load the initial guest. + guest = load_guest(config.vm.guest) + + # Loop and distro dispatch while there are distros. + while true + distro = guest.distro_dispatch + break if !distro + + guest = load_guest(distro) + end + + # Return the result + guest end # This sets the unique ID associated with this machine. This will @@ -230,5 +243,32 @@ module Vagrant def state @provider.state end + + protected + + # Given a guest name (such as `:windows`), this will load the associated + # guest implementation and return an instance. + # + # @param [Symbol] guest The name of the guest implementation. + # @return [Object] + def load_guest(guest) + @logger.info("Loading guest: #{guest}") + + klass = nil + Vagrant.plugin("1").registered.each do |plugin| + if plugin.guest.has_key?(guest) + klass = plugin.guest[guest] + break + end + end + + if klass.nil? + raise Errors::VMGuestError, + :_key => :unknown_type, + :guest => guest.to_s + end + + return klass.new(self) + end end end diff --git a/lib/vagrant/plugin/v1/guest.rb b/lib/vagrant/plugin/v1/guest.rb index 86e96c249..a62cbbce6 100644 --- a/lib/vagrant/plugin/v1/guest.rb +++ b/lib/vagrant/plugin/v1/guest.rb @@ -27,9 +27,8 @@ module Vagrant # Vagrant can successfully SSH into the machine) to give the system a chance # to determine the distro and return a distro-specific system. # - # **Warning:** If a return value which subclasses from {Base} is - # returned, Vagrant will use it as the new system instance for the - # class. + # If this method returns nil, then this instance is assumed to be + # the most specific guest implementation. def distro_dispatch end diff --git a/plugins/providers/virtualbox/action/halt.rb b/plugins/providers/virtualbox/action/halt.rb index b4f2d8bba..5f339de8e 100644 --- a/plugins/providers/virtualbox/action/halt.rb +++ b/plugins/providers/virtualbox/action/halt.rb @@ -13,13 +13,13 @@ module VagrantPlugins # attempt a graceful shutdown if current_state == :running && !env["force"] env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") - env[:vm].guest.halt + env[:machine].guest.halt end # If we're not powered off now, then force it - if env[:vm].state != :poweroff + if env[:machine].state != :poweroff env[:ui].info I18n.t("vagrant.actions.vm.halt.force") - env[:vm].driver.halt + env[:machine].driver.halt end # Sleep for a second to verify that the VM properly diff --git a/test/unit/support/shared/base_context.rb b/test/unit/support/shared/base_context.rb index 947fd01ed..734a9c5d8 100644 --- a/test/unit/support/shared/base_context.rb +++ b/test/unit/support/shared/base_context.rb @@ -5,11 +5,23 @@ require "unit/support/isolated_environment" shared_context "unit" do before(:each) do + # State to store the list of registered plugins that we have to + # unregister later. + @_plugins = [] + # Create a thing to store our temporary files so that they aren't # unlinked right away. @_temp_files = [] end + after(:each) do + # Unregister each of the plugins we have may have temporarily + # registered for the duration of this test. + @_plugins.each do |plugin| + plugin.unregister! + end + end + # This creates an isolated environment so that Vagrant doesn't # muck around with your real system during unit tests. # @@ -22,6 +34,20 @@ shared_context "unit" do env end + # This registers a Vagrant plugin for the duration of a single test. + # This will yield a new plugin class that you can then call the + # public plugin methods on. + # + # @yield [plugin] Yields the plugin class for you to call the public + # API that you need to. + def register_plugin + plugin = Class.new(Vagrant.plugin("1")) + plugin.name("Test Plugin #{plugin.inspect}") + yield plugin if block_given? + @_plugins << plugin + plugin + end + # This helper creates a temporary file and returns a Pathname # object pointed to it. # diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index a8ccc5ab8..7cc7eb03f 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -199,7 +199,7 @@ describe Vagrant::Machine do end before(:each) do - instance.stub(:communicator).and_return(communicator) + instance.stub(:communicate).and_return(communicator) end it "should raise an exception if communication is not ready" do @@ -208,6 +208,47 @@ describe Vagrant::Machine do expect { instance.guest }. to raise_error(Vagrant::Errors::MachineGuestNotReady) end + + it "should return the configured guest" do + test_guest = Class.new(Vagrant.plugin("1", :guest)) + + register_plugin do |p| + p.guest(:test) { test_guest } + end + + config.vm.guest = :test + + result = instance.guest + result.should be_kind_of(test_guest) + end + + it "should raise an exception if it can't find the configured guest" do + config.vm.guest = :bad + + expect { instance.guest }. + to raise_error(Vagrant::Errors::VMGuestError) + end + + it "should distro dispatch to the most specific guest" do + # Create the classes and dispatch the parent into the child + guest_parent = Class.new(Vagrant.plugin("1", :guest)) do + def distro_dispatch + :child + end + end + + guest_child = Class.new(Vagrant.plugin("1", :guest)) + + # Register the classes + register_plugin do |p| + p.guest(:parent) { guest_parent } + p.guest(:child) { guest_child } + end + + # Test that the result is the child + config.vm.guest = :parent + instance.guest.should be_kind_of(guest_child) + end end describe "setting the ID" do From 0eddda3552ba8362c1621f272c9dcb959d803f72 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Aug 2012 18:54:52 -0700 Subject: [PATCH 39/60] Halt works with new machine. This required some modifications to the linux guest implementation. And the other guests will have to be modified as well. This is because `channel` is now `communicate`. --- lib/vagrant/plugin/v1/communicator.rb | 9 +++++++++ plugins/communicators/ssh/communicator.rb | 5 +++++ plugins/guests/linux/guest.rb | 22 +++++++++++---------- plugins/providers/virtualbox/action/halt.rb | 4 ++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/vagrant/plugin/v1/communicator.rb b/lib/vagrant/plugin/v1/communicator.rb index 517f3e417..7e4494dc3 100644 --- a/lib/vagrant/plugin/v1/communicator.rb +++ b/lib/vagrant/plugin/v1/communicator.rb @@ -83,6 +83,15 @@ module Vagrant # @see #execute def sudo(command, opts=nil) end + + # Executes a command and returns true if the command succeeded, + # and false otherwise. By default, this executes as a normal user, + # and it is up to the communicator implementation if they expose an + # option for running tests as an administrator. + # + # @see #execute + def test(command, opts=nil) + end end end end diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index a66ee3a33..c268e3b34 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -85,6 +85,11 @@ module VagrantPlugins end end + def test(command, opts=nil) + opts = { :error_check => false }.merge(opts || {}) + execute(command, opts) == 0 + end + def upload(from, to) @logger.debug("Uploading: #{from} to #{to}") diff --git a/plugins/guests/linux/guest.rb b/plugins/guests/linux/guest.rb index 49d40be0c..5033bba9a 100644 --- a/plugins/guests/linux/guest.rb +++ b/plugins/guests/linux/guest.rb @@ -16,23 +16,25 @@ module VagrantPlugins end def distro_dispatch - if @vm.channel.test("cat /etc/debian_version") - return :debian if @vm.channel.test("cat /proc/version | grep 'Debian'") - return :ubuntu if @vm.channel.test("cat /proc/version | grep 'Ubuntu'") - end + @vm.communicate.tap do |comm| + if comm.test("cat /etc/debian_version") == 0 + return :debian if comm.test("cat /proc/version | grep 'Debian'") == 0 + return :ubuntu if comm.test("cat /proc/version | grep 'Ubuntu'") == 0 + end - return :gentoo if @vm.channel.test("cat /etc/gentoo-release") - return :fedora if @vm.channel.test("grep 'Fedora release 16' /etc/redhat-release") - return :redhat if @vm.channel.test("cat /etc/redhat-release") - return :suse if @vm.channel.test("cat /etc/SuSE-release") - return :arch if @vm.channel.test("cat /etc/arch-release") + return :gentoo if comm.test("cat /etc/gentoo-release") == 0 + return :fedora if comm.test("grep 'Fedora release 16' /etc/redhat-release") == 0 + return :redhat if comm.test("cat /etc/redhat-release") == 0 + return :suse if comm.test("cat /etc/SuSE-release") == 0 + return :arch if comm.test("cat /etc/arch-release") == 0 + end # Can't detect the distro, assume vanilla linux nil end def halt - @vm.channel.sudo("shutdown -h now") + @vm.communicate.sudo("shutdown -h now") # Wait until the VM's state is actually powered off. If this doesn't # occur within a reasonable amount of time (15 seconds by default), diff --git a/plugins/providers/virtualbox/action/halt.rb b/plugins/providers/virtualbox/action/halt.rb index 5f339de8e..ed14d0ce0 100644 --- a/plugins/providers/virtualbox/action/halt.rb +++ b/plugins/providers/virtualbox/action/halt.rb @@ -17,9 +17,9 @@ module VagrantPlugins end # If we're not powered off now, then force it - if env[:machine].state != :poweroff + if env[:machine].provider.state != :poweroff env[:ui].info I18n.t("vagrant.actions.vm.halt.force") - env[:machine].driver.halt + env[:machine].provider.halt end # Sleep for a second to verify that the VM properly From 296878cff5639888c6cda76b5f5debdcd117046b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Aug 2012 19:03:22 -0700 Subject: [PATCH 40/60] Add basic loop detection for distro_dispatch --- lib/vagrant/machine.rb | 16 ++++++++++++++-- test/unit/vagrant/machine_test.rb | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index f2c74fd57..ea448a6ad 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -132,14 +132,26 @@ module Vagrant raise Errors::MachineGuestNotReady if !communicate.ready? # Load the initial guest. - guest = load_guest(config.vm.guest) + last_guest = config.vm.guest + guest = load_guest(last_guest) # Loop and distro dispatch while there are distros. while true distro = guest.distro_dispatch break if !distro - guest = load_guest(distro) + # This is just some really basic loop detection and avoiding for + # guest classes. This is just here to help implementers a bit + # avoid a situation that is fairly easy, since if you subclass + # a parent which does `distro_dispatch`, you'll end up dispatching + # forever. + if distro == last_guest + @logger.warn("Distro dispatch loop in '#{distro}'. Exiting loop.") + break + end + + last_guest = distro + guest = load_guest(distro) end # Return the result diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 7cc7eb03f..d32fed62d 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -249,6 +249,24 @@ describe Vagrant::Machine do config.vm.guest = :parent instance.guest.should be_kind_of(guest_child) end + + it "should protect against loops in the distro dispatch" do + # Create the classes and dispatch the parent into the child + guest_parent = Class.new(Vagrant.plugin("1", :guest)) do + def distro_dispatch + :parent + end + end + + # Register the classes + register_plugin do |p| + p.guest(:parent) { guest_parent } + end + + # Test that the result is the child + config.vm.guest = :parent + instance.guest.should be_kind_of(guest_parent) + end end describe "setting the ID" do From aad022a6266960b04cae4a0d2c02db8657ecfc20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Aug 2012 19:03:31 -0700 Subject: [PATCH 41/60] Switch all `channel` to `communicate` in the linux guest --- plugins/guests/linux/guest.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/guests/linux/guest.rb b/plugins/guests/linux/guest.rb index 5033bba9a..82f512e8a 100644 --- a/plugins/guests/linux/guest.rb +++ b/plugins/guests/linux/guest.rb @@ -52,9 +52,9 @@ module VagrantPlugins real_guestpath = expanded_guest_path(guestpath) @logger.debug("Shell expanded guest path: #{real_guestpath}") - @vm.channel.sudo("mkdir -p #{real_guestpath}") + @vm.communicate.sudo("mkdir -p #{real_guestpath}") mount_folder(name, real_guestpath, options) - @vm.channel.sudo("chown `id -u #{options[:owner]}`:`id -g #{options[:group]}` #{real_guestpath}") + @vm.communicate.sudo("chown `id -u #{options[:owner]}`:`id -g #{options[:group]}` #{real_guestpath}") end def mount_nfs(ip, folders) @@ -65,8 +65,8 @@ module VagrantPlugins real_guestpath = expanded_guest_path(opts[:guestpath]) # Do the actual creating and mounting - @vm.channel.sudo("mkdir -p #{real_guestpath}") - @vm.channel.sudo("mount -o vers=#{opts[:nfs_version]} #{ip}:'#{opts[:hostpath]}' #{real_guestpath}", + @vm.communicate.sudo("mkdir -p #{real_guestpath}") + @vm.communicate.sudo("mount -o vers=#{opts[:nfs_version]} #{ip}:'#{opts[:hostpath]}' #{real_guestpath}", :error_class => LinuxError, :error_key => :mount_nfs_fail) end @@ -82,7 +82,7 @@ module VagrantPlugins # @return [String] The expanded guestpath def expanded_guest_path(guestpath) real_guestpath = nil - @vm.channel.execute("printf #{guestpath}") do |type, data| + @vm.communicate.execute("printf #{guestpath}") do |type, data| if type == :stdout real_guestpath ||= "" real_guestpath += data @@ -107,7 +107,7 @@ module VagrantPlugins attempts = 0 while true success = true - @vm.channel.sudo("mount -t vboxsf #{mount_options} #{name} #{guestpath}") do |type, data| + @vm.communicate.sudo("mount -t vboxsf #{mount_options} #{name} #{guestpath}") do |type, data| success = false if type == :stderr && data =~ /No such device/i end From 83b908f3d8f1fe1d46a7b2c087f702291fe726c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Aug 2012 19:30:41 -0700 Subject: [PATCH 42/60] `vagrant suspend` works with new machine abstraction --- plugins/commands/suspend/command.rb | 16 ++++------------ plugins/providers/virtualbox/action.rb | 17 +++++++++++++++++ .../providers/virtualbox/action}/suspend.rb | 10 +++++----- 3 files changed, 26 insertions(+), 17 deletions(-) rename {lib/vagrant/action/vm => plugins/providers/virtualbox/action}/suspend.rb (60%) diff --git a/plugins/commands/suspend/command.rb b/plugins/commands/suspend/command.rb index 539be8ac5..6183b71d3 100644 --- a/plugins/commands/suspend/command.rb +++ b/plugins/commands/suspend/command.rb @@ -4,10 +4,8 @@ module VagrantPlugins module CommandSuspend class Command < Vagrant.plugin("1", :command) def execute - options = {} - - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant suspend [vm-name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant suspend [vm-name]" end # Parse the options @@ -16,18 +14,12 @@ module VagrantPlugins @logger.debug("'suspend' each target VM...") with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Suspending: #{vm.name}") - vm.suspend - else - @logger.info("Not created: #{vm.name}. Not suspending.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + vm.action(:suspend) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 804be8911..33e6b3f11 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -13,6 +13,7 @@ module VagrantPlugins autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) + autoload :Suspend, File.expand_path("../action/suspend", __FILE__) # Include the built-in modules so that we can use them as top-level # things. @@ -79,6 +80,22 @@ module VagrantPlugins b.use SSHRun end end + + # This is the action that is primarily responsible for suspending + # the virtual machine. + def self.action_suspend + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use CheckAccessible + b2.use Suspend + else + b2.use MessageNotCreated + end + end + end + end end end end diff --git a/lib/vagrant/action/vm/suspend.rb b/plugins/providers/virtualbox/action/suspend.rb similarity index 60% rename from lib/vagrant/action/vm/suspend.rb rename to plugins/providers/virtualbox/action/suspend.rb index 8f39293eb..9a9b7a94b 100644 --- a/lib/vagrant/action/vm/suspend.rb +++ b/plugins/providers/virtualbox/action/suspend.rb @@ -1,15 +1,15 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Suspend def initialize(app, env) @app = app end def call(env) - if env[:vm].state == :running + if env[:machine].provider.state == :running env[:ui].info I18n.t("vagrant.actions.vm.suspend.suspending") - env[:vm].driver.suspend + env[:machine].provider.driver.suspend end @app.call(env) From db11c16b79707a393d3cb26a0ff57c404b808b72 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Aug 2012 19:48:26 -0700 Subject: [PATCH 43/60] ssh_config works with new machine abstraction --- plugins/commands/ssh_config/command.rb | 20 +++++++++----------- templates/locales/en.yml | 6 ++++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/commands/ssh_config/command.rb b/plugins/commands/ssh_config/command.rb index 7ca855ea5..e9ce9e78a 100644 --- a/plugins/commands/ssh_config/command.rb +++ b/plugins/commands/ssh_config/command.rb @@ -10,12 +10,11 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant ssh-config [vm-name] [-h name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant ssh-config [vm-name] [-h name]" + o.separator "" - opts.separator "" - - opts.on("--host COMMAND", "Name the host for the config..") do |h| + o.on("--host COMMAND", "Name the host for the config..") do |h| options[:host] = h end end @@ -23,13 +22,12 @@ module VagrantPlugins argv = parse_options(opts) return if !argv - with_target_vms(argv, :single_target => true) do |vm| - raise Vagrant::Errors::VMNotCreatedError if !vm.created? - raise Vagrant::Errors::VMInaccessible if !vm.state == :inaccessible + with_target_vms(argv, :single_target => true) do |machine| + ssh_info = machine.ssh_info + raise Vagrant::Errors::SSHNotReady if ssh_info.nil? - ssh_info = vm.ssh.info variables = { - :host_key => options[:host] || vm.name || "vagrant", + :host_key => options[:host] || machine.name || "vagrant", :ssh_host => ssh_info[:host], :ssh_port => ssh_info[:port], :ssh_user => ssh_info[:username], @@ -45,7 +43,7 @@ module VagrantPlugins # Success, exit status 0 0 - end + end end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 36c17aa3c..30f3d4513 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -164,8 +164,10 @@ en: The provider for this Vagrant-managed machine is reporting that it is not yet ready for SSH. Depending on your provider this can carry different meanings. Make sure your machine is created and running and - try again. If you continue to get this error message, please view the - documentation for the provider you're using. + try again. Additionally, check the output of `vagrant status` to verify + that the machine is in the state that you expect. If you continue to + get this error message, please view the documentation for the provider + you're using. ssh_port_not_detected: |- Vagrant couldn't determine the SSH port for your VM! Vagrant attempts to automatically find a forwarded port that matches your `config.ssh.guest_port` From bca86637420ba82c568acc888c52ecc12f9d8a0b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Aug 2012 20:03:35 -0700 Subject: [PATCH 44/60] `vagrant resume` works with the new machine abstraction --- plugins/commands/resume/command.rb | 18 ++-- plugins/providers/virtualbox/action.rb | 20 +++++ plugins/providers/virtualbox/action/boot.rb | 48 ++++++++++ .../action/check_port_collisions.rb | 89 +++++++++++++++++++ .../providers/virtualbox/action}/resume.rb | 8 +- 5 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 plugins/providers/virtualbox/action/boot.rb create mode 100644 plugins/providers/virtualbox/action/check_port_collisions.rb rename {lib/vagrant/action/vm => plugins/providers/virtualbox/action}/resume.rb (71%) diff --git a/plugins/commands/resume/command.rb b/plugins/commands/resume/command.rb index 256593d90..db942b48d 100644 --- a/plugins/commands/resume/command.rb +++ b/plugins/commands/resume/command.rb @@ -4,10 +4,8 @@ module VagrantPlugins module CommandResume class Command < Vagrant.plugin("1", :command) def execute - options = {} - - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant resume [vm-name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant resume [vm-name]" end # Parse the options @@ -15,19 +13,13 @@ module VagrantPlugins return if !argv @logger.debug("'resume' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Resume: #{vm.name}") - vm.resume - else - @logger.info("Not created: #{vm.name}. Not resuming.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:resume) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 33e6b3f11..e7c6b7021 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -3,8 +3,10 @@ require "vagrant/action/builder" module VagrantPlugins module ProviderVirtualBox module Action + autoload :Boot, File.expand_path("../action/boot", __FILE__) autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) autoload :CheckCreated, File.expand_path("../action/check_created", __FILE__) + autoload :CheckPortCollisions, File.expand_path("../action/check_port_collisions", __FILE__) autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) @@ -13,6 +15,7 @@ module VagrantPlugins autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) + autoload :Resume, File.expand_path("../action/resume", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) # Include the built-in modules so that we can use them as top-level @@ -59,6 +62,23 @@ module VagrantPlugins end end + # This is the action that is primarily responsible for resuming + # suspended machines. + def self.action_resume + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use CheckAccessible + b2.use CheckPortCollisions + b2.use Resume + else + b2.use MessageNotCreated + end + end + end + end + # This is the action that will exec into an SSH shell. def self.action_ssh Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/boot.rb b/plugins/providers/virtualbox/action/boot.rb new file mode 100644 index 000000000..7c4c18f0e --- /dev/null +++ b/plugins/providers/virtualbox/action/boot.rb @@ -0,0 +1,48 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Boot + def initialize(app, env) + @app = app + end + + def call(env) + @env = env + + # Start up the VM and wait for it to boot. + env[:ui].info I18n.t("vagrant.actions.vm.boot.booting") + env[:machine].provider.driver.start(@env[:machine].config.vm.boot_mode) + raise Errors::VMFailedToBoot if !wait_for_boot + + @app.call(env) + end + + def wait_for_boot + @env[:ui].info I18n.t("vagrant.actions.vm.boot.waiting") + + @env[:machine].config.ssh.max_tries.to_i.times do |i| + if @env[:machine].communicate.ready? + @env[:ui].info I18n.t("vagrant.actions.vm.boot.ready") + return true + end + + # Return true so that the vm_failed_to_boot error doesn't + # get shown + return true if @env[:interrupted] + + # If the VM is not starting or running, something went wrong + # and we need to show a useful error. + state = @env[:machine].provider.state + raise Errors::VMFailedToRun if state != :starting && state != :running + + sleep 2 if !@env["vagrant.test"] + end + + @env[:ui].error I18n.t("vagrant.actions.vm.boot.failed") + false + end + + end + end + end +end diff --git a/plugins/providers/virtualbox/action/check_port_collisions.rb b/plugins/providers/virtualbox/action/check_port_collisions.rb new file mode 100644 index 000000000..c4a537d47 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_port_collisions.rb @@ -0,0 +1,89 @@ +require "vagrant/util/is_port_open" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class CheckPortCollisions + include Vagrant::Util::IsPortOpen + + def initialize(app, env) + @app = app + end + + def call(env) + # For the handlers... + @env = env + + # Figure out how we handle port collisions. By default we error. + handler = env[:port_collision_handler] || :error + + # Read our forwarded ports, if we have any, to override what + # we have configured. + current = {} + env[:machine].provider.driver.read_forwarded_ports.each do |nic, name, hostport, guestport| + current[name] = hostport.to_i + end + + existing = env[:machine].provider.driver.read_used_ports + env[:machine].config.vm.forwarded_ports.each do |options| + # Use the proper port, whether that be the configured port or the + # port that is currently on use of the VM. + hostport = options[:hostport].to_i + hostport = current[options[:name]] if current.has_key?(options[:name]) + + if existing.include?(hostport) || is_port_open?("localhost", hostport) + # We have a collision! Handle it + send("handle_#{handler}".to_sym, options, existing) + end + end + + @app.call(env) + end + + # Handles a port collision by raising an exception. + def handle_error(options, existing_ports) + raise Vagrant::Errors::ForwardPortCollisionResume + end + + # Handles a port collision by attempting to fix it. + def handle_correct(options, existing_ports) + # We need to keep this for messaging purposes + original_hostport = options[:hostport] + + if !options[:auto] + # Auto fixing is disabled for this port forward, so we + # must throw an error so the user can fix it. + raise Vagrant::Errors::ForwardPortCollision, + :host_port => options[:hostport].to_s, + :guest_port => options[:guestport].to_s + end + + # Get the auto port range and get rid of the used ports and + # ports which are being used in other forwards so we're just + # left with available ports. + range = @env[:machine].config.vm.auto_port_range.to_a + range -= @env[:machine].config.vm.forwarded_ports.collect { |opts| opts[:hostport].to_i } + range -= existing_ports + + if range.empty? + raise Vagrant::Errors::ForwardPortAutolistEmpty, + :vm_name => @env[:machine].name, + :host_port => options[:hostport].to_s, + :guest_port => options[:guestport].to_s + end + + # Set the port up to be the first one and add that port to + # the used list. + options[:hostport] = range.shift + existing_ports << options[:hostport] + + # Notify the user + @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.fixed_collision", + :host_port => original_hostport.to_s, + :guest_port => options[:guestport].to_s, + :new_port => options[:hostport])) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/resume.rb b/plugins/providers/virtualbox/action/resume.rb similarity index 71% rename from lib/vagrant/action/vm/resume.rb rename to plugins/providers/virtualbox/action/resume.rb index bf62c8b3e..9967b91fe 100644 --- a/lib/vagrant/action/vm/resume.rb +++ b/plugins/providers/virtualbox/action/resume.rb @@ -1,13 +1,13 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Resume def initialize(app, env) @app = app end def call(env) - if env[:vm].state == :saved + if env[:machine].provider.state == :saved env[:ui].info I18n.t("vagrant.actions.vm.resume.resuming") env[:action_runner].run(Boot, env) end From 2fc18f7207aee3ab0fe47f2431374deef993a22e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Aug 2012 23:18:50 -0700 Subject: [PATCH 45/60] `destroy` gets a little farther, and properly halts the VM --- lib/vagrant/action.rb | 1 + lib/vagrant/action/builtin/env_set.rb | 24 +++++++++++++++++++ plugins/commands/destroy/command.rb | 8 +++---- plugins/providers/virtualbox/action.rb | 2 ++ plugins/providers/virtualbox/action/halt.rb | 4 ++-- .../vagrant/action/builtin/env_set_test.rb | 20 ++++++++++++++++ 6 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 lib/vagrant/action/builtin/env_set.rb create mode 100644 test/unit/vagrant/action/builtin/env_set_test.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index ee28a6b93..482cf82e3 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -17,6 +17,7 @@ module Vagrant module Builtin autoload :Call, "vagrant/action/builtin/call" autoload :Confirm, "vagrant/action/builtin/confirm" + autoload :EnvSet, "vagrant/action/builtin/env_set" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHRun, "vagrant/action/builtin/ssh_run" end diff --git a/lib/vagrant/action/builtin/env_set.rb b/lib/vagrant/action/builtin/env_set.rb new file mode 100644 index 000000000..bad9cb11d --- /dev/null +++ b/lib/vagrant/action/builtin/env_set.rb @@ -0,0 +1,24 @@ +module Vagrant + module Action + module Builtin + # This middleware class allows you to modify the environment hash + # in the middle of a middleware sequence. The new environmental data + # will take affect at this stage in the middleware and will persist + # through. + class EnvSet + def initialize(app, env, new_env=nil) + @app = app + @new_env = new_env || {} + end + + def call(env) + # Merge in the new data + env.merge!(@new_env) + + # Carry on + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/destroy/command.rb b/plugins/commands/destroy/command.rb index 278e3bda3..d0ef4b169 100644 --- a/plugins/commands/destroy/command.rb +++ b/plugins/commands/destroy/command.rb @@ -4,11 +4,11 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant destroy [vm-name]" - opts.separator "" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant destroy [vm-name]" + o.separator "" - opts.on("-f", "--force", "Destroy without confirmation.") do |f| + o.on("-f", "--force", "Destroy without confirmation.") do |f| options[:force] = f end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index e7c6b7021..1de27463b 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -37,6 +37,8 @@ module VagrantPlugins if env2[:result] b3.use Vagrant::Action::General::Validate b3.use CheckAccessible + b3.use EnvSet, :force => true + b3.use action_halt else b3.use MessageWillNotDestroy end diff --git a/plugins/providers/virtualbox/action/halt.rb b/plugins/providers/virtualbox/action/halt.rb index ed14d0ce0..42b749acd 100644 --- a/plugins/providers/virtualbox/action/halt.rb +++ b/plugins/providers/virtualbox/action/halt.rb @@ -11,7 +11,7 @@ module VagrantPlugins if current_state == :running || current_state == :gurumeditation # If the VM is running and we're not forcing, we can # attempt a graceful shutdown - if current_state == :running && !env["force"] + if current_state == :running && !env[:force] env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") env[:machine].guest.halt end @@ -19,7 +19,7 @@ module VagrantPlugins # If we're not powered off now, then force it if env[:machine].provider.state != :poweroff env[:ui].info I18n.t("vagrant.actions.vm.halt.force") - env[:machine].provider.halt + env[:machine].provider.driver.halt end # Sleep for a second to verify that the VM properly diff --git a/test/unit/vagrant/action/builtin/env_set_test.rb b/test/unit/vagrant/action/builtin/env_set_test.rb new file mode 100644 index 000000000..0a551ec2b --- /dev/null +++ b/test/unit/vagrant/action/builtin/env_set_test.rb @@ -0,0 +1,20 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::EnvSet do + let(:app) { lambda { |env| } } + let(:env) { {} } + + it "should set the new environment" do + described_class.new(app, env, :foo => :bar).call(env) + + env[:foo].should == :bar + end + + it "should call the next middleware" do + callable = lambda { |env| env[:called] = env[:foo] } + + env[:called].should be_nil + described_class.new(callable, env, :foo => :yep).call(env) + env[:called].should == :yep + end +end From 0cc63c05e24e8814c4fb0632228bc54fd1f973e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Aug 2012 23:31:12 -0700 Subject: [PATCH 46/60] `vagrant destroy` fully works --- plugins/providers/virtualbox/action.rb | 10 +++++ .../virtualbox/action/clean_machine_folder.rb | 43 +++++++++++++++++++ .../providers/virtualbox/action/destroy.rb | 19 ++++++++ .../destroy_unused_network_interfaces.rb | 16 +++++++ .../virtualbox/action/provisioner_cleanup.rb | 25 +++++++++++ .../virtualbox/action/prune_nfs_exports.rb | 20 +++++++++ plugins/providers/virtualbox/provider.rb | 2 + 7 files changed, 135 insertions(+) create mode 100644 plugins/providers/virtualbox/action/clean_machine_folder.rb create mode 100644 plugins/providers/virtualbox/action/destroy.rb create mode 100644 plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb create mode 100644 plugins/providers/virtualbox/action/provisioner_cleanup.rb create mode 100644 plugins/providers/virtualbox/action/prune_nfs_exports.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 1de27463b..5d75e9e11 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -9,12 +9,17 @@ module VagrantPlugins autoload :CheckPortCollisions, File.expand_path("../action/check_port_collisions", __FILE__) autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) + autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) + autoload :Destroy, File.expand_path("../action/destroy", __FILE__) autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) + autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) + autoload :ProvisionerCleanup, File.expand_path("../action/provisioner_cleanup", __FILE__) + autoload :PruneNFSExports, File.expand_path("../action/prune_nfs_exports", __FILE__) autoload :Resume, File.expand_path("../action/resume", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) @@ -39,6 +44,11 @@ module VagrantPlugins b3.use CheckAccessible b3.use EnvSet, :force => true b3.use action_halt + b3.use ProvisionerCleanup + b3.use PruneNFSExports + b3.use Destroy + b3.use CleanMachineFolder + b3.use DestroyUnusedNetworkInterfaces else b3.use MessageWillNotDestroy end diff --git a/plugins/providers/virtualbox/action/clean_machine_folder.rb b/plugins/providers/virtualbox/action/clean_machine_folder.rb new file mode 100644 index 000000000..3007cbd0f --- /dev/null +++ b/plugins/providers/virtualbox/action/clean_machine_folder.rb @@ -0,0 +1,43 @@ +require "fileutils" + +module VagrantPlugins + module ProviderVirtualBox + module Action + # Cleans up the VirtualBox machine folder for any ".xml-prev" + # files which VirtualBox may have left over. This is a bug in + # VirtualBox. As soon as this is fixed, this middleware can and + # will be removed. + class CleanMachineFolder + def initialize(app, env) + @app = app + end + + def call(env) + clean_machine_folder(env[:machine].provider.driver.read_machine_folder) + @app.call(env) + end + + def clean_machine_folder(machine_folder) + folder = File.join(machine_folder, "*") + + # Small safeguard against potentially unwanted rm-rf, since the default + # machine folder will typically always be greater than 10 characters long. + # For users with it < 10, out of luck? + return if folder.length < 10 + + Dir[folder].each do |f| + next unless File.directory?(f) + + keep = Dir["#{f}/**/*"].find do |d| + # Find a file that doesn't have ".xml-prev" as the suffix, + # which signals that we want to keep this folder + File.file?(d) && !(File.basename(d) =~ /\.vbox-prev$/) + end + + FileUtils.rm_rf(f) if !keep + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/destroy.rb b/plugins/providers/virtualbox/action/destroy.rb new file mode 100644 index 000000000..dcf8a8201 --- /dev/null +++ b/plugins/providers/virtualbox/action/destroy.rb @@ -0,0 +1,19 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Destroy + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.actions.vm.destroy.destroying") + env[:machine].provider.driver.delete + env[:machine].id = nil + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb b/plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb new file mode 100644 index 000000000..0897ef6dd --- /dev/null +++ b/plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class DestroyUnusedNetworkInterfaces + def initialize(app, env) + @app = app + end + + def call(env) + env[:machine].provider.driver.delete_unused_host_only_networks + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/provisioner_cleanup.rb b/plugins/providers/virtualbox/action/provisioner_cleanup.rb new file mode 100644 index 000000000..c97e3334e --- /dev/null +++ b/plugins/providers/virtualbox/action/provisioner_cleanup.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ProvisionerCleanup + def initialize(app, env) + @app = app + end + + def call(env) + # Instantiate all the enabled provisioners + provisioners = env[:machine].config.vm.provisioners.map do |provisioner| + provisioner.provisioner.new(env, provisioner.config) + end + + # Call cleanup on each + provisioners.each do |instance| + instance.cleanup + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/prune_nfs_exports.rb b/plugins/providers/virtualbox/action/prune_nfs_exports.rb new file mode 100644 index 000000000..e74f176fc --- /dev/null +++ b/plugins/providers/virtualbox/action/prune_nfs_exports.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class PruneNFSExports + def initialize(app, env) + @app = app + end + + def call(env) + if env[:host] + valid_ids = env[:machine].provider.driver.read_vms + env[:host].nfs_prune(valid_ids) + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index ba41289e5..9d9d5c9c8 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -38,6 +38,8 @@ module VagrantPlugins # # @return [Symbol] def state + # XXX: What happens if we destroy the VM but the UUID is still + # set here? return :not_created if !@driver.uuid state = @driver.read_state return :unknown if !state From 85a499ffb814594bf075bdfd7ec19b3682326bbe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Aug 2012 20:27:28 -0700 Subject: [PATCH 47/60] Properly handle the case that VM doesn't exist for the VB driver. --- plugins/providers/virtualbox/provider.rb | 15 ++++++++++++++- test/unit/vagrant/machine_test.rb | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 9d9d5c9c8..2854a7d9c 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -1,11 +1,24 @@ +require "log4r" + module VagrantPlugins module ProviderVirtualBox class Provider < Vagrant.plugin("1", :provider) attr_reader :driver def initialize(machine) + @logger = Log4r::Logger.new("vagrant::provider::virtualbox") @machine = machine - @driver = Driver::Meta.new(@machine.id) + + begin + @logger.debug("Instantiating the driver for machine ID: #{@machine.id.inspect}") + @driver = Driver::Meta.new(@machine.id) + rescue Driver::Meta::VMNotFound + # The virtual machine doesn't exist, so we probably have a stale + # ID. Just clear the id out of the machine and reload it. + @logger.debug("VM not found! Clearing saved machine ID and reloading.") + @machine.id = nil + retry + end end # @see Vagrant::Plugin::V1::Provider#action diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index d32fed62d..20661d55e 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -283,6 +283,17 @@ describe Vagrant::Machine do instance.id = "foo" new_instance.id.should == "foo" end + + it "should delete the ID" do + instance.id = "foo" + + second = new_instance + second.id.should == "foo" + second.id = nil + + third = new_instance + third.id.should be_nil + end end describe "ssh info" do From 7aa083d25924b1e32649f7656aad5476a027aec7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Aug 2012 21:12:41 -0700 Subject: [PATCH 48/60] `vagrant reload` now works with the new machine abstraction --- plugins/commands/reload/command.rb | 20 +- plugins/providers/virtualbox/action.rb | 55 +++ .../action/clear_forwarded_ports.rb | 18 + .../action/clear_network_interfaces.rb | 31 ++ .../virtualbox/action/clear_shared_folders.rb | 17 + .../providers/virtualbox/action/customize.rb | 36 ++ .../virtualbox/action/forward_ports.rb | 92 ++++ .../providers/virtualbox/action/host_name.rb | 21 + .../providers/virtualbox/action/network.rb | 404 ++++++++++++++++++ plugins/providers/virtualbox/action/nfs.rb | 185 ++++++++ .../providers/virtualbox/action/provision.rb | 62 +++ .../virtualbox/action/sane_defaults.rb | 75 ++++ .../virtualbox/action/share_folders.rb | 115 +++++ 13 files changed, 1118 insertions(+), 13 deletions(-) create mode 100644 plugins/providers/virtualbox/action/clear_forwarded_ports.rb create mode 100644 plugins/providers/virtualbox/action/clear_network_interfaces.rb create mode 100644 plugins/providers/virtualbox/action/clear_shared_folders.rb create mode 100644 plugins/providers/virtualbox/action/customize.rb create mode 100644 plugins/providers/virtualbox/action/forward_ports.rb create mode 100644 plugins/providers/virtualbox/action/host_name.rb create mode 100644 plugins/providers/virtualbox/action/network.rb create mode 100644 plugins/providers/virtualbox/action/nfs.rb create mode 100644 plugins/providers/virtualbox/action/provision.rb create mode 100644 plugins/providers/virtualbox/action/sane_defaults.rb create mode 100644 plugins/providers/virtualbox/action/share_folders.rb diff --git a/plugins/commands/reload/command.rb b/plugins/commands/reload/command.rb index 79f88eab0..718a45917 100644 --- a/plugins/commands/reload/command.rb +++ b/plugins/commands/reload/command.rb @@ -14,10 +14,10 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant reload [vm-name]" - opts.separator "" - build_start_options(opts, options) + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant reload [vm-name]" + o.separator "" + build_start_options(o, options) end # Parse the options @@ -25,19 +25,13 @@ module VagrantPlugins return if !argv @logger.debug("'reload' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Reloading: #{vm.name}") - vm.reload(options) - else - @logger.info("Not created: #{vm.name}. Not reloading.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:reload) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 5d75e9e11..aa58b285f 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -10,17 +10,28 @@ module VagrantPlugins autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) + autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) + autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) + autoload :ClearSharedFolders, File.expand_path("../action/clear_shared_folders", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) + autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) + autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Halt, File.expand_path("../action/halt", __FILE__) + autoload :HostName, File.expand_path("../action/host_name", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) + autoload :Network, File.expand_path("../action/network", __FILE__) + autoload :NFS, File.expand_path("../action/nfs", __FILE__) + autoload :Provision, File.expand_path("../action/provision", __FILE__) autoload :ProvisionerCleanup, File.expand_path("../action/provisioner_cleanup", __FILE__) autoload :PruneNFSExports, File.expand_path("../action/prune_nfs_exports", __FILE__) autoload :Resume, File.expand_path("../action/resume", __FILE__) + autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) + autoload :ShareFolders, File.expand_path("../action/share_folders", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) # Include the built-in modules so that we can use them as top-level @@ -74,6 +85,25 @@ module VagrantPlugins end end + # This action is responsible for reloading the machine, which + # brings it down, sucks in new configuration, and brings the + # machine back up with the new configuration. + def self.action_reload + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use Vagrant::Action::General::Validate + b2.use action_halt + b2.use action_start + end + end + end + # This is the action that is primarily responsible for resuming # suspended machines. def self.action_resume @@ -113,6 +143,31 @@ module VagrantPlugins end end + # This action starts a VM, assuming it is already imported and exists. + # A precondition of this action is that the VM exists. + def self.action_start + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use CheckAccessible + b.use CleanMachineFolder + b.use ClearForwardedPorts + b.use EnvSet, :port_collision_handler => :correct + b.use ForwardPorts + b.use Provision + b.use PruneNFSExports + b.use NFS + b.use ClearSharedFolders + b.use ShareFolders + b.use ClearNetworkInterfaces + b.use Network + b.use HostName + b.use SaneDefaults + b.use Customize + b.use Boot + end + end + # This is the action that is primarily responsible for suspending # the virtual machine. def self.action_suspend diff --git a/plugins/providers/virtualbox/action/clear_forwarded_ports.rb b/plugins/providers/virtualbox/action/clear_forwarded_ports.rb new file mode 100644 index 000000000..c4e87610b --- /dev/null +++ b/plugins/providers/virtualbox/action/clear_forwarded_ports.rb @@ -0,0 +1,18 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ClearForwardedPorts + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.actions.vm.clear_forward_ports.deleting") + env[:machine].provider.driver.clear_forwarded_ports + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/clear_network_interfaces.rb b/plugins/providers/virtualbox/action/clear_network_interfaces.rb new file mode 100644 index 000000000..0098692f3 --- /dev/null +++ b/plugins/providers/virtualbox/action/clear_network_interfaces.rb @@ -0,0 +1,31 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ClearNetworkInterfaces + def initialize(app, env) + @app = app + end + + def call(env) + # Create the adapters array to make all adapters nothing. + # We do adapters 2 to 8 because that is every built-in adapter + # excluding the NAT adapter on port 1 which Vagrant always + # expects to exist. + adapters = [] + 2.upto(8).each do |i| + adapters << { + :adapter => i, + :type => :none + } + end + + # "Enable" all the adapters we setup. + env[:ui].info I18n.t("vagrant.actions.vm.clear_network_interfaces.deleting") + env[:machine].provider.driver.enable_adapters(adapters) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/clear_shared_folders.rb b/plugins/providers/virtualbox/action/clear_shared_folders.rb new file mode 100644 index 000000000..1aa617ff1 --- /dev/null +++ b/plugins/providers/virtualbox/action/clear_shared_folders.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ClearSharedFolders + def initialize(app, env) + @app = app + end + + def call(env) + env[:machine].provider.driver.clear_shared_folders + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/customize.rb b/plugins/providers/virtualbox/action/customize.rb new file mode 100644 index 000000000..ea699ac33 --- /dev/null +++ b/plugins/providers/virtualbox/action/customize.rb @@ -0,0 +1,36 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Customize + def initialize(app, env) + @app = app + end + + def call(env) + customizations = env[:machine].config.vm.customizations + if !customizations.empty? + env[:ui].info I18n.t("vagrant.actions.vm.customize.running") + + # Execute each customization command. + customizations.each do |command| + processed_command = command.collect do |arg| + arg = env[:machine].id if arg == :id + arg.to_s + end + + result = env[:machine].provider.driver.execute_command(processed_command) + if result.exit_code != 0 + raise Errors::VMCustomizationFailed, { + :command => processed_command.inspect, + :error => result.stderr + } + end + end + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/forward_ports.rb b/plugins/providers/virtualbox/action/forward_ports.rb new file mode 100644 index 000000000..0afc71115 --- /dev/null +++ b/plugins/providers/virtualbox/action/forward_ports.rb @@ -0,0 +1,92 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ForwardPorts + def initialize(app, env) + @app = app + end + + #-------------------------------------------------------------- + # Execution + #-------------------------------------------------------------- + def call(env) + @env = env + + # Get the ports we're forwarding + ports = forward_port_definitions + + # Warn if we're port forwarding to any privileged ports... + threshold_check(ports) + + env[:ui].info I18n.t("vagrant.actions.vm.forward_ports.forwarding") + forward_ports(ports) + + @app.call(env) + end + + # This returns an array of forwarded ports with overrides properly + # squashed. + def forward_port_definitions + # Get all the port mappings in the order they're defined and + # organize them by their guestport, taking the "last one wins" + # approach. + guest_port_mapping = {} + @env[:machine].config.vm.forwarded_ports.each do |options| + guest_port_mapping[options[:guestport]] = options + end + + # Return the values, since the order doesn't really matter + guest_port_mapping.values + end + + # This method checks for any forwarded ports on the host below + # 1024, which causes the forwarded ports to fail. + def threshold_check(ports) + ports.each do |options| + if options[:hostport] <= 1024 + @env[:ui].warn I18n.t("vagrant.actions.vm.forward_ports.privileged_ports") + return + end + end + end + + def forward_ports(mappings) + ports = [] + + interfaces = @env[:machine].provider.driver.read_network_interfaces + + mappings.each do |options| + message_attributes = { + :guest_port => options[:guestport], + :host_port => options[:hostport], + :adapter => options[:adapter] + } + + # Assuming the only reason to establish port forwarding is + # because the VM is using Virtualbox NAT networking. Host-only + # bridged networking don't require port-forwarding and establishing + # forwarded ports on these attachment types has uncertain behaviour. + @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.forwarding_entry", + message_attributes)) + + # Port forwarding requires the network interface to be a NAT interface, + # so verify that that is the case. + if interfaces[options[:adapter]][:type] != :nat + @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.non_nat", + message_attributes)) + next + end + + # Add the options to the ports array to send to the driver later + ports << options.merge(:name => options[:name], :adapter => options[:adapter]) + end + + if !ports.empty? + # We only need to forward ports if there are any to forward + @env[:machine].provider.driver.forward_ports(ports) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/host_name.rb b/plugins/providers/virtualbox/action/host_name.rb new file mode 100644 index 000000000..40cc934fe --- /dev/null +++ b/plugins/providers/virtualbox/action/host_name.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class HostName + def initialize(app, env) + @app = app + end + + def call(env) + @app.call(env) + + host_name = env[:machine].config.vm.host_name + if !host_name.nil? + env[:ui].info I18n.t("vagrant.actions.vm.host_name.setting") + env[:machine].guest.change_host_name(host_name) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb new file mode 100644 index 000000000..31a6fa38d --- /dev/null +++ b/plugins/providers/virtualbox/action/network.rb @@ -0,0 +1,404 @@ +require "set" + +require "log4r" + +require "vagrant/util/network_ip" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class Network + # Utilities to deal with network addresses + include Vagrant::Util::NetworkIP + + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::network") + + @app = app + end + + def call(env) + @env = env + + # First we have to get the array of adapters that we need + # to create on the virtual machine itself, as well as the + # driver-agnostic network configurations for each. + @logger.debug("Determining adapters and networks...") + adapters = [] + networks = [] + env[:machine].config.vm.networks.each do |type, args| + # Get the normalized configuration we'll use around + config = send("#{type}_config", args) + + # Get the virtualbox adapter configuration + adapter = send("#{type}_adapter", config) + adapters << adapter + + # Get the network configuration + network = send("#{type}_network_config", config) + network[:_auto_config] = true if config[:auto_config] + networks << network + end + + if !adapters.empty? + # Automatically assign an adapter number to any adapters + # that aren't explicitly set. + @logger.debug("Assigning adapter locations...") + assign_adapter_locations(adapters) + + # Verify that our adapters are good just prior to enabling them. + verify_adapters(adapters) + + # Create all the network interfaces + @logger.info("Enabling adapters...") + env[:ui].info I18n.t("vagrant.actions.vm.network.preparing") + env[:machine].provider.driver.enable_adapters(adapters) + end + + # Continue the middleware chain. We're done with our VM + # setup until after it is booted. + @app.call(env) + + if !adapters.empty? && !networks.empty? + # Determine the interface numbers for the guest. + assign_interface_numbers(networks, adapters) + + # Configure all the network interfaces on the guest. We only + # want to configure the networks that have `auto_config` setup. + networks_to_configure = networks.select { |n| n[:_auto_config] } + env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") + env[:machine].guest.configure_networks(networks_to_configure) + end + end + + # This method assigns the adapter to use for the adapter. + # e.g. it says that the first adapter is actually on the + # virtual machine's 2nd adapter location. + # + # It determines the adapter numbers by simply finding the + # "next available" in each case. + # + # The adapters are modified in place by adding an ":adapter" + # field to each. + def assign_adapter_locations(adapters) + available = Set.new(1..8) + + # Determine which NICs are actually available. + interfaces = @env[:machine].provider.driver.read_network_interfaces + interfaces.each do |number, nic| + # Remove the number from the available NICs if the + # NIC is in use. + available.delete(number) if nic[:type] != :none + end + + # Based on the available set, assign in order to + # the adapters. + available = available.to_a.sort + @logger.debug("Available NICs: #{available.inspect}") + adapters.each do |adapter| + # Ignore the adapters that already have been assigned + if !adapter[:adapter] + # If we have no available adapters, then that is an exceptional + # event. + raise Vagrant::Errors::NetworkNoAdapters if available.empty? + + # Otherwise, assign as the adapter the next available item + adapter[:adapter] = available.shift + end + end + end + + # Verifies that the adapter configurations look good. This will + # raise an exception in the case that any errors occur. + def verify_adapters(adapters) + # Verify that there are no collisions in the adapters being used. + used = Set.new + adapters.each do |adapter| + raise Vagrant::Errors::NetworkAdapterCollision if used.include?(adapter[:adapter]) + used.add(adapter[:adapter]) + end + end + + # Assigns the actual interface number of a network based on the + # enabled NICs on the virtual machine. + # + # This interface number is used by the guest to configure the + # NIC on the guest VM. + # + # The networks are modified in place by adding an ":interface" + # field to each. + def assign_interface_numbers(networks, adapters) + current = 0 + adapter_to_interface = {} + + # Make a first pass to assign interface numbers by adapter location + vm_adapters = @env[:machine].provider.driver.read_network_interfaces + vm_adapters.sort.each do |number, adapter| + if adapter[:type] != :none + # Not used, so assign the interface number and increment + adapter_to_interface[number] = current + current += 1 + end + end + + # Make a pass through the adapters to assign the :interface + # key to each network configuration. + adapters.each_index do |i| + adapter = adapters[i] + network = networks[i] + + # Figure out the interface number by simple lookup + network[:interface] = adapter_to_interface[adapter[:adapter]] + end + end + + def hostonly_config(args) + ip = args[0] + options = args[1] || {} + + # Determine if we're dealing with a static IP or a DHCP-served IP. + type = ip == :dhcp ? :dhcp : :static + + # Default IP is in the 20-bit private network block for DHCP based networks + ip = "172.28.128.1" if type == :dhcp + + options = { + :type => type, + :ip => ip, + :netmask => "255.255.255.0", + :adapter => nil, + :mac => nil, + :name => nil, + :auto_config => true + }.merge(options) + + # Verify that this hostonly network wouldn't conflict with any + # bridged interfaces + verify_no_bridge_collision(options) + + # Get the network address and IP parts which are used for many + # default calculations + netaddr = network_address(options[:ip], options[:netmask]) + ip_parts = netaddr.split(".").map { |i| i.to_i } + + # Calculate the adapter IP, which we assume is the IP ".1" at the + # end usually. + adapter_ip = ip_parts.dup + adapter_ip[3] += 1 + options[:adapter_ip] ||= adapter_ip.join(".") + + if type == :dhcp + # Calculate the DHCP server IP, which is the network address + # with the final octet + 2. So "172.28.0.0" turns into "172.28.0.2" + dhcp_ip = ip_parts.dup + dhcp_ip[3] += 2 + options[:dhcp_ip] ||= dhcp_ip.join(".") + + # Calculate the lower and upper bound for the DHCP server + dhcp_lower = ip_parts.dup + dhcp_lower[3] += 3 + options[:dhcp_lower] ||= dhcp_lower.join(".") + + dhcp_upper = ip_parts.dup + dhcp_upper[3] = 254 + options[:dhcp_upper] ||= dhcp_upper.join(".") + end + + # Return the hostonly network configuration + return options + end + + def hostonly_adapter(config) + @logger.debug("Searching for matching network: #{config[:ip]}") + interface = find_matching_hostonly_network(config) + + if !interface + @logger.debug("Network not found. Creating if we can.") + + # It is an error case if a specific name was given but the network + # doesn't exist. + if config[:name] + raise Vagrant::Errors::NetworkNotFound, :name => config[:name] + end + + # Otherwise, we create a new network and put the net network + # in the list of available networks so other network definitions + # can use it! + interface = create_hostonly_network(config) + @logger.debug("Created network: #{interface[:name]}") + end + + if config[:type] == :dhcp + # Check that if there is a DHCP server attached on our interface, + # then it is identical. Otherwise, we can't set it. + if interface[:dhcp] + valid = interface[:dhcp][:ip] == config[:dhcp_ip] && + interface[:dhcp][:lower] == config[:dhcp_lower] && + interface[:dhcp][:upper] == config[:dhcp_upper] + + raise Vagrant::Errors::NetworkDHCPAlreadyAttached if !valid + + @logger.debug("DHCP server already properly configured") + else + # Configure the DHCP server for the network. + @logger.debug("Creating a DHCP server...") + @env[:machine].provider.driver.create_dhcp_server(interface[:name], config) + end + end + + return { + :adapter => config[:adapter], + :type => :hostonly, + :hostonly => interface[:name], + :mac_address => config[:mac], + :nic_type => config[:nic_type] + } + end + + def hostonly_network_config(config) + return { + :type => config[:type], + :adapter_ip => config[:adapter_ip], + :ip => config[:ip], + :netmask => config[:netmask] + } + end + + # Creates a new hostonly network that matches the network requested + # by the given host-only network configuration. + def create_hostonly_network(config) + # Create the options that are going to be used to create our + # new network. + options = config.dup + options[:ip] = options[:adapter_ip] + + @env[:machine].provider.driver.create_host_only_network(options) + end + + # Finds a host only network that matches our configuration on VirtualBox. + # This will return nil if a matching network does not exist. + def find_matching_hostonly_network(config) + this_netaddr = network_address(config[:ip], config[:netmask]) + + @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| + if config[:name] && config[:name] == interface[:name] + return interface + elsif this_netaddr == network_address(interface[:ip], interface[:netmask]) + return interface + end + end + + nil + end + + # Verifies that a host-only network subnet would not collide with + # a bridged networking interface. + # + # If the subnets overlap in any way then the host only network + # will not work because the routing tables will force the traffic + # onto the real interface rather than the virtualbox interface. + def verify_no_bridge_collision(options) + this_netaddr = network_address(options[:ip], options[:netmask]) + + @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| + that_netaddr = network_address(interface[:ip], interface[:netmask]) + raise Vagrant::Errors::NetworkCollision if this_netaddr == that_netaddr && interface[:status] != "Down" + end + end + + def bridged_config(args) + options = args[0] || {} + options = {} if !options.is_a?(Hash) + + return { + :adapter => nil, + :mac => nil, + :bridge => nil, + :auto_config => true, + :use_dhcp_assigned_default_route => false + }.merge(options) + end + + def bridged_adapter(config) + # Find the bridged interfaces that are available + bridgedifs = @env[:machine].provider.driver.read_bridged_interfaces + bridgedifs.delete_if { |interface| interface[:status] == "Down" } + + # The name of the chosen bridge interface will be assigned to this + # variable. + chosen_bridge = nil + + if config[:bridge] + @logger.debug("Bridge was directly specified in config, searching for: #{config[:bridge]}") + + # Search for a matching bridged interface + bridgedifs.each do |interface| + if interface[:name].downcase == config[:bridge].downcase + @logger.debug("Specific bridge found as configured in the Vagrantfile. Using it.") + chosen_bridge = interface[:name] + break + end + end + + # If one wasn't found, then we notify the user here. + if !chosen_bridge + @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.specific_not_found", + :bridge => config[:bridge]) + end + end + + # If we still don't have a bridge chosen (this means that one wasn't + # specified in the Vagrantfile, or the bridge specified in the Vagrantfile + # wasn't found), then we fall back to the normal means of searchign for a + # bridged network. + if !chosen_bridge + if bridgedifs.length == 1 + # One bridgable interface? Just use it. + chosen_bridge = bridgedifs[0][:name] + @logger.debug("Only one bridged interface available. Using it by default.") + else + # More than one bridgable interface requires a user decision, so + # show options to choose from. + @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.available", + :prefix => false) + bridgedifs.each_index do |index| + interface = bridgedifs[index] + @env[:ui].info("#{index + 1}) #{interface[:name]}", :prefix => false) + end + + # The range of valid choices + valid = Range.new(1, bridgedifs.length) + + # The choice that the user has chosen as the bridging interface + choice = nil + while !valid.include?(choice) + choice = @env[:ui].ask("What interface should the network bridge to? ") + choice = choice.to_i + end + + chosen_bridge = bridgedifs[choice - 1][:name] + end + end + + @logger.info("Bridging adapter #{config[:adapter]} to #{chosen_bridge}") + + # Given the choice we can now define the adapter we're using + return { + :adapter => config[:adapter], + :type => :bridged, + :bridge => chosen_bridge, + :mac_address => config[:mac], + :nic_type => config[:nic_type] + } + end + + def bridged_network_config(config) + return { + :type => :dhcp, + :use_dhcp_assigned_default_route => config[:use_dhcp_assigned_default_route] + } + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/nfs.rb b/plugins/providers/virtualbox/action/nfs.rb new file mode 100644 index 000000000..90eaae320 --- /dev/null +++ b/plugins/providers/virtualbox/action/nfs.rb @@ -0,0 +1,185 @@ +require 'digest/md5' +require 'fileutils' +require 'pathname' + +require 'log4r' + +module VagrantPlugins + module ProviderVirtualBox + module Action + class NFS + def initialize(app,env) + @logger = Log4r::Logger.new("vagrant::action::vm::nfs") + @app = app + @env = env + + verify_settings if nfs_enabled? + end + + def call(env) + @env = env + + extract_folders + + if !folders.empty? + prepare_folders + export_folders + end + + @app.call(env) + + mount_folders if !folders.empty? + end + + # Returns the folders which are to be synced via NFS. + def folders + @folders ||= {} + end + + # Removes the NFS enabled shared folders from the configuration, + # so they will no longer be mounted by the actual shared folder + # task. + def extract_folders + # Load the NFS enabled shared folders + @folders = {} + @env[:machine].config.vm.shared_folders.each do |key, opts| + if opts[:nfs] + # Duplicate the options, set the hostpath, and set disabled on the original + # options so the ShareFolders middleware doesn't try to mount it. + folder = opts.dup + hostpath = Pathname.new(opts[:hostpath]).expand_path(@env[:root_path]) + + if !hostpath.directory? && opts[:create] + # Host path doesn't exist, so let's create it. + @logger.debug("Host path doesn't exist, creating: #{hostpath}") + + begin + FileUtils.mkpath(hostpath) + rescue Errno::EACCES + raise Vagrant::Errors::SharedFolderCreateFailed, + :path => hostpath.to_s + end + end + + # Set the hostpath now that it exists. + folder[:hostpath] = hostpath.to_s + + # Assign the folder to our instance variable for later use + @folders[key] = folder + + # Disable the folder so that regular shared folders don't try to + # mount it. + opts[:disabled] = true + end + end + end + + # Prepares the settings for the NFS folders, such as setting the + # options on the NFS folders. + def prepare_folders + @folders = @folders.inject({}) do |acc, data| + key, opts = data + opts[:map_uid] = prepare_permission(:uid, opts) + opts[:map_gid] = prepare_permission(:gid, opts) + opts[:nfs_version] ||= 3 + + # The poor man's UUID. An MD5 hash here is sufficient since + # we need a 32 character "uuid" to represent the filesystem + # of an export. Hashing the host path is safe because two of + # the same host path will hash to the same fsid. + opts[:uuid] = Digest::MD5.hexdigest(opts[:hostpath]) + + acc[key] = opts + acc + end + end + + # Prepares the UID/GID settings for a single folder. + def prepare_permission(perm, opts) + key = "map_#{perm}".to_sym + return nil if opts.has_key?(key) && opts[key].nil? + + # The options on the hash get priority, then the default + # values + value = opts.has_key?(key) ? opts[key] : @env[:machine].config.nfs.send(key) + return value if value != :auto + + # Get UID/GID from folder if we've made it this far + # (value == :auto) + stat = File.stat(opts[:hostpath]) + return stat.send(perm) + end + + # Uses the host class to export the folders via NFS. This typically + # involves adding a line to `/etc/exports` for this VM, but it is + # up to the host class to define the specific behavior. + def export_folders + @env[:ui].info I18n.t("vagrant.actions.vm.nfs.exporting") + @env[:host].nfs_export(@env[:machine].id, guest_ip, folders) + end + + # Uses the system class to mount the NFS folders. + def mount_folders + @env[:ui].info I18n.t("vagrant.actions.vm.nfs.mounting") + + # Only mount the folders which have a guest path specified + mount_folders = {} + folders.each do |name, opts| + if opts[:guestpath] + mount_folders[name] = opts.dup + end + end + + @env[:machine].guest.mount_nfs(host_ip, mount_folders) + end + + # Returns the IP address of the first host only network adapter + # + # @return [String] + def host_ip + @env[:machine].provider.driver.read_network_interfaces.each do |adapter, opts| + if opts[:type] == :hostonly + @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| + if interface[:name] == opts[:hostonly] + return interface[:ip] + end + end + end + end + + nil + end + + # Returns the IP address of the guest by looking at the first + # enabled host only network. + # + # @return [String] + def guest_ip + @env[:machine].config.vm.networks.each do |type, args| + if type == :hostonly && args[0].is_a?(String) + return args[0] + end + end + + nil + end + + # Checks if there are any NFS enabled shared folders. + def nfs_enabled? + @env[:machine].config.vm.shared_folders.each do |key, opts| + return true if opts[:nfs] + end + + false + end + + # Verifies that the host is set and supports NFS. + def verify_settings + raise Vagrant::Errors::NFSHostRequired if @env[:host].nil? + raise Vagrant::Errors::NFSNotSupported if !@env[:host].nfs? + raise Vagrant::Errors::NFSNoHostNetwork if !guest_ip + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/provision.rb b/plugins/providers/virtualbox/action/provision.rb new file mode 100644 index 000000000..d2304c609 --- /dev/null +++ b/plugins/providers/virtualbox/action/provision.rb @@ -0,0 +1,62 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class Provision + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::provision") + @app = app + + env["provision.enabled"] = true if !env.has_key?("provision.enabled") + end + + def call(env) + @env = env + + provisioners = nil + + # We set this here so that even if this value is changed in the future, + # it stays constant to what we expect here in this moment. + enabled = env["provision.enabled"] + + # Instantiate and prepare the provisioners. Preparation must happen here + # so that shared folders and such can properly take effect. + provisioners = enabled_provisioners + provisioners.map { |p| p.prepare } + + @app.call(env) + + if enabled + # Take prepared provisioners and run the provisioning + provisioners.each do |instance| + @env[:ui].info I18n.t("vagrant.actions.vm.provision.beginning", + :provisioner => instance.class) + instance.provision! + end + end + end + + def enabled_provisioners + enabled = [] + @env[:machine].config.vm.provisioners.each do |provisioner| + if @env["provision.types"] + # If we've specified types of provisioners to enable, then we + # only use those provisioners, and skip any that we haven't + # specified. + if !@env["provision.types"].include?(provisioner.shortcut.to_s) + @logger.debug("Skipping provisioner: #{provisioner.shortcut}") + next + end + end + + enabled << provisioner.provisioner.new(@env, provisioner.config) + end + + # Return the enable provisioners + enabled + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/sane_defaults.rb b/plugins/providers/virtualbox/action/sane_defaults.rb new file mode 100644 index 000000000..21d8e9055 --- /dev/null +++ b/plugins/providers/virtualbox/action/sane_defaults.rb @@ -0,0 +1,75 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class SaneDefaults + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::sanedefaults") + @app = app + end + + def call(env) + # Set the env on an instance variable so we can access it in + # helpers. + @env = env + + # Enable the host IO cache on the sata controller. Note that + # if this fails then its not a big deal, so we don't raise any + # errors. The Host IO cache vastly improves disk IO performance + # for VMs. + command = [ + "storagectl", env[:machine].id, + "--name", "SATA Controller", + "--hostiocache", "on" + ] + attempt_and_log(command, "Enabling the Host I/O cache on the SATA controller...") + + enable_dns_proxy = true + begin + contents = File.read("/etc/resolv.conf") + + if contents =~ /^nameserver 127\.0\.0\.1$/ + # The use of both natdnsproxy and natdnshostresolver break on + # Ubuntu 12.04 that uses resolvconf with localhost. When used + # VirtualBox will give the client dns server 10.0.2.3, while + # not binding to that address itself. Therefore disable this + # feature if host uses the resolvconf server 127.0.0.1 + @logger.info("Disabling DNS proxy since resolv.conf contains 127.0.0.1") + enable_dns_proxy = false + end + rescue Errno::ENOENT; end + + # Enable/disable the NAT DNS proxy as necessary + if enable_dns_proxy + command = [ + "modifyvm", env[:machine].id, + "--natdnsproxy1", "on" + ] + attempt_and_log(command, "Enable the NAT DNS proxy on adapter 1...") + else + command = [ "modifyvm", env[:machine].id, "--natdnsproxy1", "off" ] + attempt_and_log(command, "Disable the NAT DNS proxy on adapter 1...") + command = [ "modifyvm", env[:machine].id, "--natdnshostresolver1", "off" ] + attempt_and_log(command, "Disable the NAT DNS resolver on adapter 1...") + end + + @app.call(env) + end + + protected + + # This is just a helper method that executes a single command, logs + # the given string to the log, and also includes the exit status in + # the log message. + # + # @param [Array] command Command to run + # @param [String] log Log message to write. + def attempt_and_log(command, log) + result = @env[:machine].provider.driver.execute_command(command) + @logger.info("#{log} (exit status = #{result.exit_code})") + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/share_folders.rb b/plugins/providers/virtualbox/action/share_folders.rb new file mode 100644 index 000000000..a68beb926 --- /dev/null +++ b/plugins/providers/virtualbox/action/share_folders.rb @@ -0,0 +1,115 @@ +require "pathname" + +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class ShareFolders + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::share_folders") + @app = app + end + + def call(env) + @env = env + + prepare_folders + create_metadata + + @app.call(env) + + mount_shared_folders + end + + # This method returns an actual list of VirtualBox shared + # folders to create and their proper path. + def shared_folders + @env[:machine].config.vm.shared_folders.inject({}) do |acc, data| + key, value = data + + next acc if value[:disabled] + + # This to prevent overwriting the actual shared folders data + value = value.dup + acc[key] = value + acc + end + end + + # Prepares the shared folders by verifying they exist and creating them + # if they don't. + def prepare_folders + shared_folders.each do |name, options| + hostpath = Pathname.new(options[:hostpath]).expand_path(@env[:root_path]) + + if !hostpath.directory? && options[:create] + # Host path doesn't exist, so let's create it. + @logger.debug("Host path doesn't exist, creating: #{hostpath}") + + begin + hostpath.mkpath + rescue Errno::EACCES + raise Vagrant::Errors::SharedFolderCreateFailed, + :path => hostpath.to_s + end + end + end + end + + def create_metadata + @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.creating") + + folders = [] + shared_folders.each do |name, data| + folders << { + :name => name, + :hostpath => File.expand_path(data[:hostpath], @env[:root_path]), + :transient => data[:transient] + } + end + + @env[:machine].provider.driver.share_folders(folders) + end + + def mount_shared_folders + @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.mounting") + + # short guestpaths first, so we don't step on ourselves + folders = shared_folders.sort_by do |name, data| + if data[:guestpath] + data[:guestpath].length + else + # A long enough path to just do this at the end. + 10000 + end + end + + # Go through each folder and mount + folders.each do |name, data| + if data[:guestpath] + # Guest path specified, so mount the folder to specified point + @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", + :name => name, + :guest_path => data[:guestpath])) + + # Dup the data so we can pass it to the guest API + data = data.dup + + # Calculate the owner and group + data[:owner] ||= @env[:machine].config.ssh.username + data[:group] ||= @env[:machine].config.ssh.username + + # Mount the actual folder + @env[:machine].guest.mount_shared_folder(name, data[:guestpath], data) + else + # If no guest path is specified, then automounting is disabled + @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", + :name => name)) + end + end + end + end + end + end +end From aaeb060f33a7e880f1dff2680b1bba5511865f43 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Aug 2012 21:21:31 -0700 Subject: [PATCH 49/60] `vagrant provision` --- plugins/commands/provision/command.rb | 24 ++++------------- plugins/providers/virtualbox/action.rb | 26 +++++++++++++++++++ .../providers/virtualbox/action/is_running.rb | 20 ++++++++++++++ .../virtualbox/action/message_not_running.rb | 16 ++++++++++++ 4 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 plugins/providers/virtualbox/action/is_running.rb create mode 100644 plugins/providers/virtualbox/action/message_not_running.rb diff --git a/plugins/commands/provision/command.rb b/plugins/commands/provision/command.rb index d6bd66cbe..bbaa4044e 100644 --- a/plugins/commands/provision/command.rb +++ b/plugins/commands/provision/command.rb @@ -4,10 +4,8 @@ module VagrantPlugins module CommandProvision class Command < Vagrant.plugin("1", :command) def execute - options = {} - - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant provision [vm-name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant provision [vm-name]" end # Parse the options @@ -16,25 +14,13 @@ module VagrantPlugins # Go over each VM and provision! @logger.debug("'provision' each target VM...") - with_target_vms(argv) do |vm| - - if vm.created? - if vm.state == :running - @logger.info("Provisioning: #{vm.name}") - vm.provision - else - @logger.info("#{vm.name} not running. Not provisioning.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_running") - end - else - @logger.info("#{vm.name} not created. Not provisioning.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:provision) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index aa58b285f..e27d089f9 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -22,7 +22,9 @@ module VagrantPlugins autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :HostName, File.expand_path("../action/host_name", __FILE__) + autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) + autoload :MessageNotRunning, File.expand_path("../action/message_not_running", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) autoload :Network, File.expand_path("../action/network", __FILE__) autoload :NFS, File.expand_path("../action/nfs", __FILE__) @@ -85,6 +87,30 @@ module VagrantPlugins end end + # This action just runs the provisioners on the machine. + def self.action_provision + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use Call, IsRunning do |env2, b3| + if !env2[:result] + b3.use MessageNotRunning + next + end + + b3.use CheckAccessible + b3.use Provision + end + end + end + end + # This action is responsible for reloading the machine, which # brings it down, sucks in new configuration, and brings the # machine back up with the new configuration. diff --git a/plugins/providers/virtualbox/action/is_running.rb b/plugins/providers/virtualbox/action/is_running.rb new file mode 100644 index 000000000..565b3a735 --- /dev/null +++ b/plugins/providers/virtualbox/action/is_running.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class IsRunning + def initialize(app, env) + @app = app + end + + def call(env) + # Set the result to be true if the machine is running. + env[:result] = env[:machine].state == :running + + # Call the next if we have one (but we shouldn't, since this + # middleware is built to run with the Call-type middlewares) + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/message_not_running.rb b/plugins/providers/virtualbox/action/message_not_running.rb new file mode 100644 index 000000000..fdfff199e --- /dev/null +++ b/plugins/providers/virtualbox/action/message_not_running.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MessageNotRunning + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.commands.common.vm_not_running") + @app.call(env) + end + end + end + end +end From b659191a023e1e29442cf696193db4239f9ca36f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Aug 2012 22:38:41 -0700 Subject: [PATCH 50/60] `vagrant up`! --- lib/vagrant/machine.rb | 4 + lib/vagrant/plugin/v1/provider.rb | 9 +++ plugins/commands/up/command.rb | 21 ++--- plugins/providers/virtualbox/action.rb | 81 +++++++++++++++---- .../providers/virtualbox/action/check_box.rb | 33 ++++++++ .../action/check_guest_additions.rb | 36 +++++++++ .../virtualbox/action/default_name.rb | 22 +++++ plugins/providers/virtualbox/action/import.rb | 49 +++++++++++ .../providers/virtualbox/action/is_saved.rb | 20 +++++ .../virtualbox/action/match_mac_address.rb | 21 +++++ plugins/providers/virtualbox/provider.rb | 30 ++++--- test/unit/vagrant/machine_test.rb | 14 ++++ 12 files changed, 300 insertions(+), 40 deletions(-) create mode 100644 plugins/providers/virtualbox/action/check_box.rb create mode 100644 plugins/providers/virtualbox/action/check_guest_additions.rb create mode 100644 plugins/providers/virtualbox/action/default_name.rb create mode 100644 plugins/providers/virtualbox/action/import.rb create mode 100644 plugins/providers/virtualbox/action/is_saved.rb create mode 100644 plugins/providers/virtualbox/action/match_mac_address.rb diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index ea448a6ad..0f8e8671c 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -184,6 +184,10 @@ module Vagrant # Store the ID locally @id = value + + # Notify the provider that the ID changed in case it needs to do + # any accounting from it. + @provider.machine_id_changed end # This returns a clean inspect value so that printing the value via diff --git a/lib/vagrant/plugin/v1/provider.rb b/lib/vagrant/plugin/v1/provider.rb index 513b4c2d8..0a71e134c 100644 --- a/lib/vagrant/plugin/v1/provider.rb +++ b/lib/vagrant/plugin/v1/provider.rb @@ -21,6 +21,15 @@ module Vagrant nil end + # This method is called if the underying machine ID changes. Providers + # can use this method to load in new data for the actual backing + # machine or to realize that the machine is now gone (the ID can + # become `nil`). No parameters are given, since the underlying machine + # is simply the machine instance given to this object. And no + # return value is necessary. + def machine_id_changed + end + # This should return a hash of information that explains how to # SSH into the machine. If the machine is not at a point where # SSH is even possible, then `nil` should be returned. diff --git a/plugins/commands/up/command.rb b/plugins/commands/up/command.rb index 1336760ed..186f0c98e 100644 --- a/plugins/commands/up/command.rb +++ b/plugins/commands/up/command.rb @@ -11,10 +11,10 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant up [vm-name] [--[no-]provision] [-h]" - opts.separator "" - build_start_options(opts, options) + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant up [vm-name] [--[no-]provision] [-h]" + o.separator "" + build_start_options(o, options) end # Parse the options @@ -23,20 +23,13 @@ module VagrantPlugins # Go over each VM and bring it up @logger.debug("'Up' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Booting: #{vm.name}") - vm.ui.info I18n.t("vagrant.commands.up.vm_created") - vm.start(options) - else - @logger.info("Creating: #{vm.name}") - vm.up(options) - end + with_target_vms(argv) do |machine| + machine.action(:up) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index e27d089f9..47f5021c0 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -5,7 +5,9 @@ module VagrantPlugins module Action autoload :Boot, File.expand_path("../action/boot", __FILE__) autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) + autoload :CheckBox, File.expand_path("../action/check_box", __FILE__) autoload :CheckCreated, File.expand_path("../action/check_created", __FILE__) + autoload :CheckGuestAdditions, File.expand_path("../action/check_guest_additions", __FILE__) autoload :CheckPortCollisions, File.expand_path("../action/check_port_collisions", __FILE__) autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) @@ -15,6 +17,7 @@ module VagrantPlugins autoload :ClearSharedFolders, File.expand_path("../action/clear_shared_folders", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__) + autoload :DefaultName, File.expand_path("../action/default_name", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) @@ -22,7 +25,10 @@ module VagrantPlugins autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :HostName, File.expand_path("../action/host_name", __FILE__) + autoload :Import, File.expand_path("../action/import", __FILE__) autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) + autoload :IsSaved, File.expand_path("../action/is_saved", __FILE__) + autoload :MatchMACAddress, File.expand_path("../action/match_mac_address", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageNotRunning, File.expand_path("../action/message_not_running", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) @@ -40,6 +46,29 @@ module VagrantPlugins # things. include Vagrant::Action::Builtin + # This action boots the VM, assuming the VM is in a state that requires + # a bootup (i.e. not saved). + def self.action_boot + Vagrant::Action::Builder.new.tap do |b| + b.use CheckAccessible + b.use CleanMachineFolder + b.use ClearForwardedPorts + b.use EnvSet, :port_collision_handler => :correct + b.use ForwardPorts + b.use Provision + b.use PruneNFSExports + b.use NFS + b.use ClearSharedFolders + b.use ShareFolders + b.use ClearNetworkInterfaces + b.use Network + b.use HostName + b.use SaneDefaults + b.use Customize + b.use Boot + end + end + # This is the action that is primarily responsible for completely # freeing the resources of the underlying virtual machine. def self.action_destroy @@ -175,22 +204,21 @@ module VagrantPlugins Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Vagrant::Action::General::Validate - b.use CheckAccessible - b.use CleanMachineFolder - b.use ClearForwardedPorts - b.use EnvSet, :port_collision_handler => :correct - b.use ForwardPorts - b.use Provision - b.use PruneNFSExports - b.use NFS - b.use ClearSharedFolders - b.use ShareFolders - b.use ClearNetworkInterfaces - b.use Network - b.use HostName - b.use SaneDefaults - b.use Customize - b.use Boot + b.use Call, IsRunning do |env, b2| + # If the VM is running, then our work here is done, exit + next if env[:result] + + b2.use Call, IsSaved do |env2, b3| + if env2[:result] + # The VM is saved, so just resume it + b3.use action_resume + else + # The VM is not saved, so we must have to boot it up + # like normal. Boot! + b3.use action_boot + end + end + end end end @@ -209,6 +237,27 @@ module VagrantPlugins end end end + + # This action brings the machine up from nothing, including importing + # the box, configuring metadata, and booting. + def self.action_up + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use Call, Created do |env, b2| + # If the VM is NOT created yet, then do the setup steps + if !env[:result] + b2.use CheckAccessible + b2.use CheckBox + b2.use Import + b2.use CheckGuestAdditions + b2.use DefaultName + b2.use MatchMACAddress + end + end + b.use action_start + end + end end end end diff --git a/plugins/providers/virtualbox/action/check_box.rb b/plugins/providers/virtualbox/action/check_box.rb new file mode 100644 index 000000000..02c6fa866 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_box.rb @@ -0,0 +1,33 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class CheckBox + def initialize(app, env) + @app = app + end + + def call(env) + box_name = env[:machine].config.vm.box + raise Vagrant::Errors::BoxNotSpecified if !box_name + + if !env[:box_collection].find(box_name, :virtualbox) + box_url = env[:machine].config.vm.box_url + raise Vagrant::Errors::BoxSpecifiedDoesntExist, :name => box_name if !box_url + + # 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) + env[:box_collection].add(box_name, box_url) + env[:box_collection].reload! + + # Reload the environment and set the VM to be the new loaded VM. + env[:machine].env.reload! + env[:machine] = env[:machine].env.vms[env[:machine].name] + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/check_guest_additions.rb b/plugins/providers/virtualbox/action/check_guest_additions.rb new file mode 100644 index 000000000..2fd798814 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_guest_additions.rb @@ -0,0 +1,36 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class CheckGuestAdditions + def initialize(app, env) + @app = app + end + + def call(env) + # Use the raw interface for now, while the virtualbox gem + # doesn't support guest properties (due to cross platform issues) + version = env[:machine].provider.driver.read_guest_additions_version + if !version + env[:ui].warn I18n.t("vagrant.actions.vm.check_guest_additions.not_detected") + else + # Strip the -OSE/_OSE off from the guest additions and the virtual box + # version since all the matters are that the version _numbers_ match up. + guest_version, vb_version = [version, env[:machine].provider.driver.version].map do |v| + v.gsub(/[-_]ose/i, '') + end + + if guest_version != vb_version + env[:ui].warn(I18n.t("vagrant.actions.vm.check_guest_additions.version_mismatch", + :guest_version => version, + :virtualbox_version => vb_version)) + end + end + + # Continue + @app.call(env) + end + + end + end + end +end diff --git a/plugins/providers/virtualbox/action/default_name.rb b/plugins/providers/virtualbox/action/default_name.rb new file mode 100644 index 000000000..9ef89cf70 --- /dev/null +++ b/plugins/providers/virtualbox/action/default_name.rb @@ -0,0 +1,22 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class DefaultName + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::defaultname") + @app = app + end + + def call(env) + @logger.info("Setting the default name of the VM") + name = env[:root_path].basename.to_s + "_#{Time.now.to_i}" + env[:machine].provider.driver.set_name(name) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/import.rb b/plugins/providers/virtualbox/action/import.rb new file mode 100644 index 000000000..64e2507ab --- /dev/null +++ b/plugins/providers/virtualbox/action/import.rb @@ -0,0 +1,49 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Import + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.actions.vm.import.importing", + :name => env[:machine].box.name) + + # Import the virtual machine + ovf_file = env[:machine].box.directory.join("box.ovf").to_s + env[:machine].id = env[:machine].provider.driver.import(ovf_file) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear + # immediately. + env[:ui].clear_line + + # If we got interrupted, then the import could have been + # interrupted and its not a big deal. Just return out. + return if env[:interrupted] + + # Flag as erroneous and return if import failed + raise Vagrant::Errors::VMImportFailure if !env[:machine].id + + # Import completed successfully. Continue the chain + @app.call(env) + end + + def recover(env) + if env[:machine].provider.state != :not_created + return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) + + # Interrupted, destroy the VM. We note that we don't want to + # validate the configuration here. + destroy_env = env.clone + destroy_env[:validate] = false + env[:action_runner].run(Action.action_destroy, destroy_env) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/is_saved.rb b/plugins/providers/virtualbox/action/is_saved.rb new file mode 100644 index 000000000..749f64964 --- /dev/null +++ b/plugins/providers/virtualbox/action/is_saved.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class IsSaved + def initialize(app, env) + @app = app + end + + def call(env) + # Set the result to be true if the machine is saved. + env[:result] = env[:machine].state == :saved + + # Call the next if we have one (but we shouldn't, since this + # middleware is built to run with the Call-type middlewares) + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/match_mac_address.rb b/plugins/providers/virtualbox/action/match_mac_address.rb new file mode 100644 index 000000000..a66f40bad --- /dev/null +++ b/plugins/providers/virtualbox/action/match_mac_address.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MatchMACAddress + def initialize(app, env) + @app = app + end + + def call(env) + raise Vagrant::Errors::VMBaseMacNotSpecified if !env[:machine].config.vm.base_mac + + # Create the proc which we want to use to modify the virtual machine + env[:ui].info I18n.t("vagrant.actions.vm.match_mac.matching") + env[:machine].provider.driver.set_mac_address(env[:machine].config.vm.base_mac) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 2854a7d9c..44407c031 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -9,16 +9,9 @@ module VagrantPlugins @logger = Log4r::Logger.new("vagrant::provider::virtualbox") @machine = machine - begin - @logger.debug("Instantiating the driver for machine ID: #{@machine.id.inspect}") - @driver = Driver::Meta.new(@machine.id) - rescue Driver::Meta::VMNotFound - # The virtual machine doesn't exist, so we probably have a stale - # ID. Just clear the id out of the machine and reload it. - @logger.debug("VM not found! Clearing saved machine ID and reloading.") - @machine.id = nil - retry - end + # This method will load in our driver, so we call it now to + # initialize it. + machine_id_changed end # @see Vagrant::Plugin::V1::Provider#action @@ -31,6 +24,23 @@ module VagrantPlugins nil end + # If the machine ID changed, then we need to rebuild our underlying + # driver. + def machine_id_changed + id = @machine.id + + begin + @logger.debug("Instantiating the driver for machine ID: #{@machine.id.inspect}") + @driver = Driver::Meta.new(id) + rescue Driver::Meta::VMNotFound + # The virtual machine doesn't exist, so we probably have a stale + # ID. Just clear the id out of the machine and reload it. + @logger.debug("VM not found! Clearing saved machine ID and reloading.") + id = nil + retry + end + end + # Returns the SSH info for accessing the VirtualBox VM. def ssh_info # If the VM is not created then we cannot possibly SSH into it, so diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 20661d55e..ecf056c75 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -90,6 +90,10 @@ describe Vagrant::Machine do end it "should have access to the ID" do + # Stub this because #id= calls it. + provider.stub(:machine_id_changed) + + # Set the ID on the previous instance so that it is persisted instance.id = "foo" provider_init_test do |machine| @@ -270,6 +274,10 @@ describe Vagrant::Machine do end describe "setting the ID" do + before(:each) do + provider.stub(:machine_id_changed) + end + it "should not have an ID by default" do instance.id.should be_nil end @@ -279,6 +287,12 @@ describe Vagrant::Machine do instance.id.should == "bar" end + it "should notify the machine that the ID changed" do + provider.should_receive(:machine_id_changed).once + + instance.id = "bar" + end + it "should persist the ID" do instance.id = "foo" new_instance.id.should == "foo" From ef5eabb63d99cb33444166ebee6c0f32b55f1780 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Aug 2012 22:39:25 -0700 Subject: [PATCH 51/60] Remove the builtin VM middleware that are now part of the VB provider --- lib/vagrant/action/vm/boot.rb | 53 --- lib/vagrant/action/vm/check_box.rb | 33 -- .../action/vm/check_guest_additions.rb | 38 -- .../action/vm/check_port_collisions.rb | 89 ---- lib/vagrant/action/vm/clean_machine_folder.rb | 43 -- .../action/vm/clear_forwarded_ports.rb | 18 - .../action/vm/clear_network_interfaces.rb | 31 -- lib/vagrant/action/vm/clear_shared_folders.rb | 18 - lib/vagrant/action/vm/customize.rb | 36 -- lib/vagrant/action/vm/default_name.rb | 22 - lib/vagrant/action/vm/destroy.rb | 19 - .../vm/destroy_unused_network_interfaces.rb | 20 - lib/vagrant/action/vm/discard_state.rb | 22 - lib/vagrant/action/vm/forward_ports.rb | 92 ---- lib/vagrant/action/vm/halt.rb | 36 -- lib/vagrant/action/vm/host_name.rb | 21 - lib/vagrant/action/vm/import.rb | 53 --- lib/vagrant/action/vm/match_mac_address.rb | 21 - lib/vagrant/action/vm/network.rb | 406 ------------------ lib/vagrant/action/vm/nfs.rb | 196 --------- lib/vagrant/action/vm/provision.rb | 61 --- lib/vagrant/action/vm/provisioner_cleanup.rb | 26 -- lib/vagrant/action/vm/prune_nfs_exports.rb | 20 - lib/vagrant/action/vm/sane_defaults.rb | 78 ---- lib/vagrant/action/vm/setup_package_files.rb | 54 --- lib/vagrant/action/vm/share_folders.rb | 114 ----- 26 files changed, 1620 deletions(-) delete mode 100644 lib/vagrant/action/vm/boot.rb delete mode 100644 lib/vagrant/action/vm/check_box.rb delete mode 100644 lib/vagrant/action/vm/check_guest_additions.rb delete mode 100644 lib/vagrant/action/vm/check_port_collisions.rb delete mode 100644 lib/vagrant/action/vm/clean_machine_folder.rb delete mode 100644 lib/vagrant/action/vm/clear_forwarded_ports.rb delete mode 100644 lib/vagrant/action/vm/clear_network_interfaces.rb delete mode 100644 lib/vagrant/action/vm/clear_shared_folders.rb delete mode 100644 lib/vagrant/action/vm/customize.rb delete mode 100644 lib/vagrant/action/vm/default_name.rb delete mode 100644 lib/vagrant/action/vm/destroy.rb delete mode 100644 lib/vagrant/action/vm/destroy_unused_network_interfaces.rb delete mode 100644 lib/vagrant/action/vm/discard_state.rb delete mode 100644 lib/vagrant/action/vm/forward_ports.rb delete mode 100644 lib/vagrant/action/vm/halt.rb delete mode 100644 lib/vagrant/action/vm/host_name.rb delete mode 100644 lib/vagrant/action/vm/import.rb delete mode 100644 lib/vagrant/action/vm/match_mac_address.rb delete mode 100644 lib/vagrant/action/vm/network.rb delete mode 100644 lib/vagrant/action/vm/nfs.rb delete mode 100644 lib/vagrant/action/vm/provision.rb delete mode 100644 lib/vagrant/action/vm/provisioner_cleanup.rb delete mode 100644 lib/vagrant/action/vm/prune_nfs_exports.rb delete mode 100644 lib/vagrant/action/vm/sane_defaults.rb delete mode 100644 lib/vagrant/action/vm/setup_package_files.rb delete mode 100644 lib/vagrant/action/vm/share_folders.rb diff --git a/lib/vagrant/action/vm/boot.rb b/lib/vagrant/action/vm/boot.rb deleted file mode 100644 index 6a2ce7d35..000000000 --- a/lib/vagrant/action/vm/boot.rb +++ /dev/null @@ -1,53 +0,0 @@ -module Vagrant - module Action - module VM - class Boot - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env = env - - # Start up the VM and wait for it to boot. - boot - raise Errors::VMFailedToBoot if !wait_for_boot - - @app.call(env) - end - - def boot - @env[:ui].info I18n.t("vagrant.actions.vm.boot.booting") - @env[:vm].driver.start(@env[:vm].config.vm.boot_mode) - end - - def wait_for_boot - @env[:ui].info I18n.t("vagrant.actions.vm.boot.waiting") - - @env[:vm].config.ssh.max_tries.to_i.times do |i| - if @env[:vm].channel.ready? - @env[:ui].info I18n.t("vagrant.actions.vm.boot.ready") - return true - end - - # Return true so that the vm_failed_to_boot error doesn't - # get shown - return true if @env[:interrupted] - - # If the VM is not starting or running, something went wrong - # and we need to show a useful error. - state = @env[:vm].state - raise Errors::VMFailedToRun if state != :starting && state != :running - - sleep 2 if !@env["vagrant.test"] - end - - @env[:ui].error I18n.t("vagrant.actions.vm.boot.failed") - false - end - end - end - end -end - diff --git a/lib/vagrant/action/vm/check_box.rb b/lib/vagrant/action/vm/check_box.rb deleted file mode 100644 index 454f8b492..000000000 --- a/lib/vagrant/action/vm/check_box.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Vagrant - module Action - module VM - class CheckBox - def initialize(app, env) - @app = app - end - - def call(env) - box_name = env[:vm].config.vm.box - raise Errors::BoxNotSpecified if !box_name - - if !env[:box_collection].find(box_name, :virtualbox) - box_url = env[:vm].config.vm.box_url - raise Errors::BoxSpecifiedDoesntExist, :name => box_name if !box_url - - # 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) - env[:box_collection].add(box_name, box_url) - env[:box_collection].reload! - - # Reload the environment and set the VM to be the new loaded VM. - env[:vm].env.reload! - env[:vm] = env[:vm].env.vms[env[:vm].name] - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/check_guest_additions.rb b/lib/vagrant/action/vm/check_guest_additions.rb deleted file mode 100644 index fb96a5e65..000000000 --- a/lib/vagrant/action/vm/check_guest_additions.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Vagrant - module Action - module VM - # Middleware which verifies that the VM has the proper guest additions - # installed and prints a warning if they're not detected or if the - # version does not match the installed VirtualBox version. - class CheckGuestAdditions - def initialize(app, env) - @app = app - end - - def call(env) - # Use the raw interface for now, while the virtualbox gem - # doesn't support guest properties (due to cross platform issues) - version = env[:vm].driver.read_guest_additions_version - if !version - env[:ui].warn I18n.t("vagrant.actions.vm.check_guest_additions.not_detected") - else - # Strip the -OSE/_OSE off from the guest additions and the virtual box - # version since all the matters are that the version _numbers_ match up. - guest_version, vb_version = [version, env[:vm].driver.version].map do |v| - v.gsub(/[-_]ose/i, '') - end - - if guest_version != vb_version - env[:ui].warn(I18n.t("vagrant.actions.vm.check_guest_additions.version_mismatch", - :guest_version => version, - :virtualbox_version => vb_version)) - end - end - - # Continue - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/check_port_collisions.rb b/lib/vagrant/action/vm/check_port_collisions.rb deleted file mode 100644 index 254b351ad..000000000 --- a/lib/vagrant/action/vm/check_port_collisions.rb +++ /dev/null @@ -1,89 +0,0 @@ -require "vagrant/util/is_port_open" - -module Vagrant - module Action - module VM - # Action that checks to make sure there are no forwarded port collisions, - # and raises an exception if there is. - class CheckPortCollisions - include Util::IsPortOpen - - def initialize(app, env) - @app = app - end - - def call(env) - # For the handlers... - @env = env - - # Figure out how we handle port collisions. By default we error. - handler = env[:port_collision_handler] || :error - - # Read our forwarded ports, if we have any, to override what - # we have configured. - current = {} - env[:vm].driver.read_forwarded_ports.each do |nic, name, hostport, guestport| - current[name] = hostport.to_i - end - - existing = env[:vm].driver.read_used_ports - env[:vm].config.vm.forwarded_ports.each do |options| - # Use the proper port, whether that be the configured port or the - # port that is currently on use of the VM. - hostport = options[:hostport].to_i - hostport = current[options[:name]] if current.has_key?(options[:name]) - - if existing.include?(hostport) || is_port_open?("localhost", hostport) - # We have a collision! Handle it - send("handle_#{handler}".to_sym, options, existing) - end - end - - @app.call(env) - end - - # Handles a port collision by raising an exception. - def handle_error(options, existing_ports) - raise Errors::ForwardPortCollisionResume - end - - # Handles a port collision by attempting to fix it. - def handle_correct(options, existing_ports) - # We need to keep this for messaging purposes - original_hostport = options[:hostport] - - if !options[:auto] - # Auto fixing is disabled for this port forward, so we - # must throw an error so the user can fix it. - raise Errors::ForwardPortCollision, :host_port => options[:hostport].to_s, - :guest_port => options[:guestport].to_s - end - - # Get the auto port range and get rid of the used ports and - # ports which are being used in other forwards so we're just - # left with available ports. - range = @env[:vm].config.vm.auto_port_range.to_a - range -= @env[:vm].config.vm.forwarded_ports.collect { |opts| opts[:hostport].to_i } - range -= existing_ports - - if range.empty? - raise Errors::ForwardPortAutolistEmpty, :vm_name => @env[:vm].name, - :host_port => options[:hostport].to_s, - :guest_port => options[:guestport].to_s - end - - # Set the port up to be the first one and add that port to - # the used list. - options[:hostport] = range.shift - existing_ports << options[:hostport] - - # Notify the user - @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.fixed_collision", - :host_port => original_hostport.to_s, - :guest_port => options[:guestport].to_s, - :new_port => options[:hostport])) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/clean_machine_folder.rb b/lib/vagrant/action/vm/clean_machine_folder.rb deleted file mode 100644 index 66c522342..000000000 --- a/lib/vagrant/action/vm/clean_machine_folder.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'fileutils' - -module Vagrant - module Action - module VM - # Cleans up the VirtualBox machine folder for any ".xml-prev" - # files which VirtualBox may have left over. This is a bug in - # VirtualBox. As soon as this is fixed, this middleware can and - # will be removed. - class CleanMachineFolder - def initialize(app, env) - @app = app - end - - def call(env) - clean_machine_folder(env[:vm].driver.read_machine_folder) - @app.call(env) - end - - def clean_machine_folder(machine_folder) - folder = File.join(machine_folder, "*") - - # Small safeguard against potentially unwanted rm-rf, since the default - # machine folder will typically always be greater than 10 characters long. - # For users with it < 10, out of luck? - return if folder.length < 10 - - Dir[folder].each do |f| - next unless File.directory?(f) - - keep = Dir["#{f}/**/*"].find do |d| - # Find a file that doesn't have ".xml-prev" as the suffix, - # which signals that we want to keep this folder - File.file?(d) && !(File.basename(d) =~ /\.vbox-prev$/) - end - - FileUtils.rm_rf(f) if !keep - end - end - end - end - end -end diff --git a/lib/vagrant/action/vm/clear_forwarded_ports.rb b/lib/vagrant/action/vm/clear_forwarded_ports.rb deleted file mode 100644 index cf25df861..000000000 --- a/lib/vagrant/action/vm/clear_forwarded_ports.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Vagrant - module Action - module VM - class ClearForwardedPorts - def initialize(app, env) - @app = app - end - - def call(env) - env[:ui].info I18n.t("vagrant.actions.vm.clear_forward_ports.deleting") - env[:vm].driver.clear_forwarded_ports - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/clear_network_interfaces.rb b/lib/vagrant/action/vm/clear_network_interfaces.rb deleted file mode 100644 index 8d27a5d22..000000000 --- a/lib/vagrant/action/vm/clear_network_interfaces.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Vagrant - module Action - module VM - class ClearNetworkInterfaces - def initialize(app, env) - @app = app - end - - def call(env) - # Create the adapters array to make all adapters nothing. - # We do adapters 2 to 8 because that is every built-in adapter - # excluding the NAT adapter on port 1 which Vagrant always - # expects to exist. - adapters = [] - 2.upto(8).each do |i| - adapters << { - :adapter => i, - :type => :none - } - end - - # "Enable" all the adapters we setup. - env[:ui].info I18n.t("vagrant.actions.vm.clear_network_interfaces.deleting") - env[:vm].driver.enable_adapters(adapters) - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/clear_shared_folders.rb b/lib/vagrant/action/vm/clear_shared_folders.rb deleted file mode 100644 index 7ebeee543..000000000 --- a/lib/vagrant/action/vm/clear_shared_folders.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Vagrant - module Action - module VM - class ClearSharedFolders - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - env[:vm].driver.clear_shared_folders - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/customize.rb b/lib/vagrant/action/vm/customize.rb deleted file mode 100644 index a8696ec91..000000000 --- a/lib/vagrant/action/vm/customize.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Vagrant - module Action - module VM - class Customize - def initialize(app, env) - @app = app - end - - def call(env) - customizations = env[:vm].config.vm.customizations - if !customizations.empty? - env[:ui].info I18n.t("vagrant.actions.vm.customize.running") - - # Execute each customization command. - customizations.each do |command| - processed_command = command.collect do |arg| - arg = env[:vm].uuid if arg == :id - arg.to_s - end - - result = env[:vm].driver.execute_command(processed_command) - if result.exit_code != 0 - raise Errors::VMCustomizationFailed, { - :command => processed_command.inspect, - :error => result.stderr - } - end - end - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/default_name.rb b/lib/vagrant/action/vm/default_name.rb deleted file mode 100644 index 3ae69851d..000000000 --- a/lib/vagrant/action/vm/default_name.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'log4r' - -module Vagrant - module Action - module VM - class DefaultName - def initialize(app, env) - @logger = Log4r::Logger.new("vagrant::action::vm::defaultname") - @app = app - end - - def call(env) - @logger.info("Setting the default name of the VM") - name = env[:root_path].basename.to_s + "_#{Time.now.to_i}" - env[:vm].driver.set_name(name) - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/destroy.rb b/lib/vagrant/action/vm/destroy.rb deleted file mode 100644 index 2af9b0a18..000000000 --- a/lib/vagrant/action/vm/destroy.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Vagrant - module Action - module VM - class Destroy - def initialize(app, env) - @app = app - end - - def call(env) - env[:ui].info I18n.t("vagrant.actions.vm.destroy.destroying") - env[:vm].driver.delete - env[:vm].uuid = nil - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/destroy_unused_network_interfaces.rb b/lib/vagrant/action/vm/destroy_unused_network_interfaces.rb deleted file mode 100644 index a0fb94c8b..000000000 --- a/lib/vagrant/action/vm/destroy_unused_network_interfaces.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Vagrant - module Action - module VM - # Destroys the unused host only interfaces. This middleware cleans - # up any created host only networks. - class DestroyUnusedNetworkInterfaces - def initialize(app, env) - @app = app - end - - def call(env) - env[:vm].driver.delete_unused_host_only_networks - - # Continue along - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/discard_state.rb b/lib/vagrant/action/vm/discard_state.rb deleted file mode 100644 index 408b4580f..000000000 --- a/lib/vagrant/action/vm/discard_state.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Vagrant - module Action - module VM - # Discards the saved state of the VM if its saved. If its - # not saved, does nothing. - class DiscardState - def initialize(app, env) - @app = app - end - - def call(env) - if env[:vm].state == :saved - env[:ui].info I18n.t("vagrant.actions.vm.discard_state.discarding") - env[:vm].driver.discard_saved_state - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/forward_ports.rb b/lib/vagrant/action/vm/forward_ports.rb deleted file mode 100644 index 59c5d2a3f..000000000 --- a/lib/vagrant/action/vm/forward_ports.rb +++ /dev/null @@ -1,92 +0,0 @@ -module Vagrant - module Action - module VM - class ForwardPorts - def initialize(app,env) - @app = app - end - - #-------------------------------------------------------------- - # Execution - #-------------------------------------------------------------- - def call(env) - @env = env - - # Get the ports we're forwarding - ports = forward_port_definitions - - # Warn if we're port forwarding to any privileged ports... - threshold_check(ports) - - env[:ui].info I18n.t("vagrant.actions.vm.forward_ports.forwarding") - forward_ports(ports) - - @app.call(env) - end - - # This returns an array of forwarded ports with overrides properly - # squashed. - def forward_port_definitions - # Get all the port mappings in the order they're defined and - # organize them by their guestport, taking the "last one wins" - # approach. - guest_port_mapping = {} - @env[:vm].config.vm.forwarded_ports.each do |options| - guest_port_mapping[options[:guestport]] = options - end - - # Return the values, since the order doesn't really matter - guest_port_mapping.values - end - - # This method checks for any forwarded ports on the host below - # 1024, which causes the forwarded ports to fail. - def threshold_check(ports) - ports.each do |options| - if options[:hostport] <= 1024 - @env[:ui].warn I18n.t("vagrant.actions.vm.forward_ports.privileged_ports") - return - end - end - end - - def forward_ports(mappings) - ports = [] - - interfaces = @env[:vm].driver.read_network_interfaces - - mappings.each do |options| - message_attributes = { - :guest_port => options[:guestport], - :host_port => options[:hostport], - :adapter => options[:adapter] - } - - # Assuming the only reason to establish port forwarding is - # because the VM is using Virtualbox NAT networking. Host-only - # bridged networking don't require port-forwarding and establishing - # forwarded ports on these attachment types has uncertain behaviour. - @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.forwarding_entry", - message_attributes)) - - # Port forwarding requires the network interface to be a NAT interface, - # so verify that that is the case. - if interfaces[options[:adapter]][:type] != :nat - @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.non_nat", - message_attributes)) - next - end - - # Add the options to the ports array to send to the driver later - ports << options.merge(:name => options[:name], :adapter => options[:adapter]) - end - - if !ports.empty? - # We only need to forward ports if there are any to forward - @env[:vm].driver.forward_ports(ports) - end - end - end - end - end -end diff --git a/lib/vagrant/action/vm/halt.rb b/lib/vagrant/action/vm/halt.rb deleted file mode 100644 index 65bc06d0d..000000000 --- a/lib/vagrant/action/vm/halt.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Vagrant - module Action - module VM - class Halt - def initialize(app, env, options=nil) - @app = app - env.merge!(options || {}) - end - - def call(env) - current_state = env[:vm].state - if current_state == :running || current_state == :gurumeditation - # If the VM is running and we're not forcing, we can - # attempt a graceful shutdown - if current_state == :running && !env["force"] - env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") - env[:vm].guest.halt - end - - # If we're not powered off now, then force it - if env[:vm].state != :poweroff - env[:ui].info I18n.t("vagrant.actions.vm.halt.force") - env[:vm].driver.halt - end - - # Sleep for a second to verify that the VM properly - # cleans itself up - sleep 1 if !env["vagrant.test"] - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/host_name.rb b/lib/vagrant/action/vm/host_name.rb deleted file mode 100644 index 8df7a9fee..000000000 --- a/lib/vagrant/action/vm/host_name.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Vagrant - module Action - module VM - class HostName - def initialize(app, env) - @app = app - end - - def call(env) - @app.call(env) - - host_name = env[:vm].config.vm.host_name - if !host_name.nil? - env[:ui].info I18n.t("vagrant.actions.vm.host_name.setting") - env[:vm].guest.change_host_name(host_name) - end - end - end - end - end -end diff --git a/lib/vagrant/action/vm/import.rb b/lib/vagrant/action/vm/import.rb deleted file mode 100644 index 6e690d24d..000000000 --- a/lib/vagrant/action/vm/import.rb +++ /dev/null @@ -1,53 +0,0 @@ -module Vagrant - module Action - module VM - class Import - # The name for easy reference - def self.name - :import - end - - def initialize(app, env) - @app = app - end - - def call(env) - env[:ui].info I18n.t("vagrant.actions.vm.import.importing", :name => env[:vm].box.name) - - # Import the virtual machine - ovf_file = env[:vm].box.directory.join("box.ovf").to_s - env[:vm].uuid = env[:vm].driver.import(ovf_file) do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't disappear - # immediately. - env[:ui].clear_line - - # If we got interrupted, then the import could have been - # interrupted and its not a big deal. Just return out. - return if env[:interrupted] - - # Flag as erroneous and return if import failed - raise Errors::VMImportFailure if !env[:vm].uuid - - # Import completed successfully. Continue the chain - @app.call(env) - end - - def recover(env) - if env[:vm].created? - return if env["vagrant.error"].is_a?(Errors::VagrantError) - - # Interrupted, destroy the VM. We note that we don't want to - # validate the configuration here. - destroy_env = env.clone - destroy_env[:validate] = false - env[:action_runner].run(:destroy, destroy_env) - end - end - end - end - end -end diff --git a/lib/vagrant/action/vm/match_mac_address.rb b/lib/vagrant/action/vm/match_mac_address.rb deleted file mode 100644 index 70da6b2a4..000000000 --- a/lib/vagrant/action/vm/match_mac_address.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Vagrant - module Action - module VM - class MatchMACAddress - def initialize(app, env) - @app = app - end - - def call(env) - raise Errors::VMBaseMacNotSpecified if !env[:vm].config.vm.base_mac - - # Create the proc which we want to use to modify the virtual machine - env[:ui].info I18n.t("vagrant.actions.vm.match_mac.matching") - env[:vm].driver.set_mac_address(env[:vm].config.vm.base_mac) - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/network.rb b/lib/vagrant/action/vm/network.rb deleted file mode 100644 index f10d5d957..000000000 --- a/lib/vagrant/action/vm/network.rb +++ /dev/null @@ -1,406 +0,0 @@ -require 'set' - -require 'log4r' - -require 'vagrant/util/network_ip' - -module Vagrant - module Action - module VM - # This action handles all `config.vm.network` configurations by - # setting up the VM properly and enabling the networks afterword. - class Network - # Utilities to deal with network addresses - include Util::NetworkIP - - def initialize(app, env) - @logger = Log4r::Logger.new("vagrant::action::vm::network") - - @app = app - end - - def call(env) - @env = env - - # First we have to get the array of adapters that we need - # to create on the virtual machine itself, as well as the - # driver-agnostic network configurations for each. - @logger.debug("Determining adapters and networks...") - adapters = [] - networks = [] - env[:vm].config.vm.networks.each do |type, args| - # Get the normalized configuration we'll use around - config = send("#{type}_config", args) - - # Get the virtualbox adapter configuration - adapter = send("#{type}_adapter", config) - adapters << adapter - - # Get the network configuration - network = send("#{type}_network_config", config) - network[:_auto_config] = true if config[:auto_config] - networks << network - end - - if !adapters.empty? - # Automatically assign an adapter number to any adapters - # that aren't explicitly set. - @logger.debug("Assigning adapter locations...") - assign_adapter_locations(adapters) - - # Verify that our adapters are good just prior to enabling them. - verify_adapters(adapters) - - # Create all the network interfaces - @logger.info("Enabling adapters...") - env[:ui].info I18n.t("vagrant.actions.vm.network.preparing") - env[:vm].driver.enable_adapters(adapters) - end - - # Continue the middleware chain. We're done with our VM - # setup until after it is booted. - @app.call(env) - - if !adapters.empty? && !networks.empty? - # Determine the interface numbers for the guest. - assign_interface_numbers(networks, adapters) - - # Configure all the network interfaces on the guest. We only - # want to configure the networks that have `auto_config` setup. - networks_to_configure = networks.select { |n| n[:_auto_config] } - env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") - env[:vm].guest.configure_networks(networks_to_configure) - end - end - - # This method assigns the adapter to use for the adapter. - # e.g. it says that the first adapter is actually on the - # virtual machine's 2nd adapter location. - # - # It determines the adapter numbers by simply finding the - # "next available" in each case. - # - # The adapters are modified in place by adding an ":adapter" - # field to each. - def assign_adapter_locations(adapters) - available = Set.new(1..8) - - # Determine which NICs are actually available. - interfaces = @env[:vm].driver.read_network_interfaces - interfaces.each do |number, nic| - # Remove the number from the available NICs if the - # NIC is in use. - available.delete(number) if nic[:type] != :none - end - - # Based on the available set, assign in order to - # the adapters. - available = available.to_a.sort - @logger.debug("Available NICs: #{available.inspect}") - adapters.each do |adapter| - # Ignore the adapters that already have been assigned - if !adapter[:adapter] - # If we have no available adapters, then that is an exceptional - # event. - raise Errors::NetworkNoAdapters if available.empty? - - # Otherwise, assign as the adapter the next available item - adapter[:adapter] = available.shift - end - end - end - - # Verifies that the adapter configurations look good. This will - # raise an exception in the case that any errors occur. - def verify_adapters(adapters) - # Verify that there are no collisions in the adapters being used. - used = Set.new - adapters.each do |adapter| - raise Errors::NetworkAdapterCollision if used.include?(adapter[:adapter]) - used.add(adapter[:adapter]) - end - end - - # Assigns the actual interface number of a network based on the - # enabled NICs on the virtual machine. - # - # This interface number is used by the guest to configure the - # NIC on the guest VM. - # - # The networks are modified in place by adding an ":interface" - # field to each. - def assign_interface_numbers(networks, adapters) - current = 0 - adapter_to_interface = {} - - # Make a first pass to assign interface numbers by adapter location - vm_adapters = @env[:vm].driver.read_network_interfaces - vm_adapters.sort.each do |number, adapter| - if adapter[:type] != :none - # Not used, so assign the interface number and increment - adapter_to_interface[number] = current - current += 1 - end - end - - # Make a pass through the adapters to assign the :interface - # key to each network configuration. - adapters.each_index do |i| - adapter = adapters[i] - network = networks[i] - - # Figure out the interface number by simple lookup - network[:interface] = adapter_to_interface[adapter[:adapter]] - end - end - - def hostonly_config(args) - ip = args[0] - options = args[1] || {} - - # Determine if we're dealing with a static IP or a DHCP-served IP. - type = ip == :dhcp ? :dhcp : :static - - # Default IP is in the 20-bit private network block for DHCP based networks - ip = "172.28.128.1" if type == :dhcp - - options = { - :type => type, - :ip => ip, - :netmask => "255.255.255.0", - :adapter => nil, - :mac => nil, - :name => nil, - :auto_config => true - }.merge(options) - - # Verify that this hostonly network wouldn't conflict with any - # bridged interfaces - verify_no_bridge_collision(options) - - # Get the network address and IP parts which are used for many - # default calculations - netaddr = network_address(options[:ip], options[:netmask]) - ip_parts = netaddr.split(".").map { |i| i.to_i } - - # Calculate the adapter IP, which we assume is the IP ".1" at the - # end usually. - adapter_ip = ip_parts.dup - adapter_ip[3] += 1 - options[:adapter_ip] ||= adapter_ip.join(".") - - if type == :dhcp - # Calculate the DHCP server IP, which is the network address - # with the final octet + 2. So "172.28.0.0" turns into "172.28.0.2" - dhcp_ip = ip_parts.dup - dhcp_ip[3] += 2 - options[:dhcp_ip] ||= dhcp_ip.join(".") - - # Calculate the lower and upper bound for the DHCP server - dhcp_lower = ip_parts.dup - dhcp_lower[3] += 3 - options[:dhcp_lower] ||= dhcp_lower.join(".") - - dhcp_upper = ip_parts.dup - dhcp_upper[3] = 254 - options[:dhcp_upper] ||= dhcp_upper.join(".") - end - - # Return the hostonly network configuration - return options - end - - def hostonly_adapter(config) - @logger.debug("Searching for matching network: #{config[:ip]}") - interface = find_matching_hostonly_network(config) - - if !interface - @logger.debug("Network not found. Creating if we can.") - - # It is an error case if a specific name was given but the network - # doesn't exist. - if config[:name] - raise Errors::NetworkNotFound, :name => config[:name] - end - - # Otherwise, we create a new network and put the net network - # in the list of available networks so other network definitions - # can use it! - interface = create_hostonly_network(config) - @logger.debug("Created network: #{interface[:name]}") - end - - if config[:type] == :dhcp - # Check that if there is a DHCP server attached on our interface, - # then it is identical. Otherwise, we can't set it. - if interface[:dhcp] - valid = interface[:dhcp][:ip] == config[:dhcp_ip] && - interface[:dhcp][:lower] == config[:dhcp_lower] && - interface[:dhcp][:upper] == config[:dhcp_upper] - - raise Errors::NetworkDHCPAlreadyAttached if !valid - - @logger.debug("DHCP server already properly configured") - else - # Configure the DHCP server for the network. - @logger.debug("Creating a DHCP server...") - @env[:vm].driver.create_dhcp_server(interface[:name], config) - end - end - - return { - :adapter => config[:adapter], - :type => :hostonly, - :hostonly => interface[:name], - :mac_address => config[:mac], - :nic_type => config[:nic_type] - } - end - - def hostonly_network_config(config) - return { - :type => config[:type], - :adapter_ip => config[:adapter_ip], - :ip => config[:ip], - :netmask => config[:netmask] - } - end - - # Creates a new hostonly network that matches the network requested - # by the given host-only network configuration. - def create_hostonly_network(config) - # Create the options that are going to be used to create our - # new network. - options = config.dup - options[:ip] = options[:adapter_ip] - - @env[:vm].driver.create_host_only_network(options) - end - - # Finds a host only network that matches our configuration on VirtualBox. - # This will return nil if a matching network does not exist. - def find_matching_hostonly_network(config) - this_netaddr = network_address(config[:ip], config[:netmask]) - - @env[:vm].driver.read_host_only_interfaces.each do |interface| - if config[:name] && config[:name] == interface[:name] - return interface - elsif this_netaddr == network_address(interface[:ip], interface[:netmask]) - return interface - end - end - - nil - end - - # Verifies that a host-only network subnet would not collide with - # a bridged networking interface. - # - # If the subnets overlap in any way then the host only network - # will not work because the routing tables will force the traffic - # onto the real interface rather than the virtualbox interface. - def verify_no_bridge_collision(options) - this_netaddr = network_address(options[:ip], options[:netmask]) - - @env[:vm].driver.read_bridged_interfaces.each do |interface| - that_netaddr = network_address(interface[:ip], interface[:netmask]) - raise Errors::NetworkCollision if this_netaddr == that_netaddr && interface[:status] != "Down" - end - end - - def bridged_config(args) - options = args[0] || {} - options = {} if !options.is_a?(Hash) - - return { - :adapter => nil, - :mac => nil, - :bridge => nil, - :auto_config => true, - :use_dhcp_assigned_default_route => false - }.merge(options) - end - - def bridged_adapter(config) - # Find the bridged interfaces that are available - bridgedifs = @env[:vm].driver.read_bridged_interfaces - bridgedifs.delete_if { |interface| interface[:status] == "Down" } - - # The name of the chosen bridge interface will be assigned to this - # variable. - chosen_bridge = nil - - if config[:bridge] - @logger.debug("Bridge was directly specified in config, searching for: #{config[:bridge]}") - - # Search for a matching bridged interface - bridgedifs.each do |interface| - if interface[:name].downcase == config[:bridge].downcase - @logger.debug("Specific bridge found as configured in the Vagrantfile. Using it.") - chosen_bridge = interface[:name] - break - end - end - - # If one wasn't found, then we notify the user here. - if !chosen_bridge - @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.specific_not_found", - :bridge => config[:bridge]) - end - end - - # If we still don't have a bridge chosen (this means that one wasn't - # specified in the Vagrantfile, or the bridge specified in the Vagrantfile - # wasn't found), then we fall back to the normal means of searchign for a - # bridged network. - if !chosen_bridge - if bridgedifs.length == 1 - # One bridgable interface? Just use it. - chosen_bridge = bridgedifs[0][:name] - @logger.debug("Only one bridged interface available. Using it by default.") - else - # More than one bridgable interface requires a user decision, so - # show options to choose from. - @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.available", - :prefix => false) - bridgedifs.each_index do |index| - interface = bridgedifs[index] - @env[:ui].info("#{index + 1}) #{interface[:name]}", :prefix => false) - end - - # The range of valid choices - valid = Range.new(1, bridgedifs.length) - - # The choice that the user has chosen as the bridging interface - choice = nil - while !valid.include?(choice) - choice = @env[:ui].ask("What interface should the network bridge to? ") - choice = choice.to_i - end - - chosen_bridge = bridgedifs[choice - 1][:name] - end - end - - @logger.info("Bridging adapter #{config[:adapter]} to #{chosen_bridge}") - - # Given the choice we can now define the adapter we're using - return { - :adapter => config[:adapter], - :type => :bridged, - :bridge => chosen_bridge, - :mac_address => config[:mac], - :nic_type => config[:nic_type] - } - end - - def bridged_network_config(config) - return { - :type => :dhcp, - :use_dhcp_assigned_default_route => config[:use_dhcp_assigned_default_route] - } - end - end - end - end -end diff --git a/lib/vagrant/action/vm/nfs.rb b/lib/vagrant/action/vm/nfs.rb deleted file mode 100644 index 8aa81d31b..000000000 --- a/lib/vagrant/action/vm/nfs.rb +++ /dev/null @@ -1,196 +0,0 @@ -require 'digest/md5' -require 'fileutils' -require 'pathname' - -require 'log4r' - -module Vagrant - module Action - module VM - # Enables NFS based shared folders. `nfsd` must already be installed - # on the host machine, and NFS client must already be installed on - # the guest machine. - # - # This is a two part process: - # - # 1. Adds an entry to `/etc/exports` on the host machine using - # the host class to export the proper folder to the proper - # machine. - # 2. After boot, runs `mount` on the guest to mount the shared - # folder. - # - class NFS - def initialize(app,env) - @logger = Log4r::Logger.new("vagrant::action::vm::nfs") - @app = app - @env = env - - verify_settings if nfs_enabled? - end - - def call(env) - @env = env - - extract_folders - - if !folders.empty? - prepare_folders - export_folders - end - - @app.call(env) - - mount_folders if !folders.empty? - end - - # Returns the folders which are to be synced via NFS. - def folders - @folders ||= {} - end - - # Removes the NFS enabled shared folders from the configuration, - # so they will no longer be mounted by the actual shared folder - # task. - def extract_folders - # Load the NFS enabled shared folders - @folders = {} - @env[:vm].config.vm.shared_folders.each do |key, opts| - if opts[:nfs] - # Duplicate the options, set the hostpath, and set disabled on the original - # options so the ShareFolders middleware doesn't try to mount it. - folder = opts.dup - hostpath = Pathname.new(opts[:hostpath]).expand_path(@env[:root_path]) - - if !hostpath.directory? && opts[:create] - # Host path doesn't exist, so let's create it. - @logger.debug("Host path doesn't exist, creating: #{hostpath}") - - begin - FileUtils.mkpath(hostpath) - rescue Errno::EACCES - raise Errors::SharedFolderCreateFailed, :path => hostpath.to_s - end - end - - # Set the hostpath now that it exists. - folder[:hostpath] = hostpath.to_s - - # Assign the folder to our instance variable for later use - @folders[key] = folder - - # Disable the folder so that regular shared folders don't try to - # mount it. - opts[:disabled] = true - end - end - end - - # Prepares the settings for the NFS folders, such as setting the - # options on the NFS folders. - def prepare_folders - @folders = @folders.inject({}) do |acc, data| - key, opts = data - opts[:map_uid] = prepare_permission(:uid, opts) - opts[:map_gid] = prepare_permission(:gid, opts) - opts[:nfs_version] ||= 3 - - # The poor man's UUID. An MD5 hash here is sufficient since - # we need a 32 character "uuid" to represent the filesystem - # of an export. Hashing the host path is safe because two of - # the same host path will hash to the same fsid. - opts[:uuid] = Digest::MD5.hexdigest(opts[:hostpath]) - - acc[key] = opts - acc - end - end - - # Prepares the UID/GID settings for a single folder. - def prepare_permission(perm, opts) - key = "map_#{perm}".to_sym - return nil if opts.has_key?(key) && opts[key].nil? - - # The options on the hash get priority, then the default - # values - value = opts.has_key?(key) ? opts[key] : @env[:vm].config.nfs.send(key) - return value if value != :auto - - # Get UID/GID from folder if we've made it this far - # (value == :auto) - stat = File.stat(opts[:hostpath]) - return stat.send(perm) - end - - # Uses the host class to export the folders via NFS. This typically - # involves adding a line to `/etc/exports` for this VM, but it is - # up to the host class to define the specific behavior. - def export_folders - @env[:ui].info I18n.t("vagrant.actions.vm.nfs.exporting") - @env[:host].nfs_export(@env[:vm].uuid, guest_ip, folders) - end - - # Uses the system class to mount the NFS folders. - def mount_folders - @env[:ui].info I18n.t("vagrant.actions.vm.nfs.mounting") - - # Only mount the folders which have a guest path specified - mount_folders = {} - folders.each do |name, opts| - if opts[:guestpath] - mount_folders[name] = opts.dup - end - end - - @env[:vm].guest.mount_nfs(host_ip, mount_folders) - end - - # Returns the IP address of the first host only network adapter - # - # @return [String] - def host_ip - @env[:vm].driver.read_network_interfaces.each do |adapter, opts| - if opts[:type] == :hostonly - @env[:vm].driver.read_host_only_interfaces.each do |interface| - if interface[:name] == opts[:hostonly] - return interface[:ip] - end - end - end - end - - nil - end - - # Returns the IP address of the guest by looking at the first - # enabled host only network. - # - # @return [String] - def guest_ip - @env[:vm].config.vm.networks.each do |type, args| - if type == :hostonly && args[0].is_a?(String) - return args[0] - end - end - - nil - end - - # Checks if there are any NFS enabled shared folders. - def nfs_enabled? - @env[:vm].config.vm.shared_folders.each do |key, opts| - return true if opts[:nfs] - end - - false - end - - # Verifies that the host is set and supports NFS. - def verify_settings - raise Errors::NFSHostRequired if @env[:host].nil? - raise Errors::NFSNotSupported if !@env[:host].nfs? - raise Errors::NFSNoHostNetwork if !guest_ip - end - end - end - end -end diff --git a/lib/vagrant/action/vm/provision.rb b/lib/vagrant/action/vm/provision.rb deleted file mode 100644 index 56be8a8e6..000000000 --- a/lib/vagrant/action/vm/provision.rb +++ /dev/null @@ -1,61 +0,0 @@ -require "log4r" - -module Vagrant - module Action - module VM - class Provision - def initialize(app, env) - @logger = Log4r::Logger.new("vagrant::action::vm::provision") - @app = app - - env["provision.enabled"] = true if !env.has_key?("provision.enabled") - end - - def call(env) - @env = env - - provisioners = nil - - # We set this here so that even if this value is changed in the future, - # it stays constant to what we expect here in this moment. - enabled = env["provision.enabled"] - # Instantiate and prepare the provisioners. Preparation must happen here - # so that shared folders and such can properly take effect. - provisioners = enabled_provisioners - provisioners.map { |p| p.prepare } - - @app.call(env) - - if enabled - # Take prepared provisioners and run the provisioning - provisioners.each do |instance| - @env[:ui].info I18n.t("vagrant.actions.vm.provision.beginning", - :provisioner => instance.class) - instance.provision! - end - end - end - - def enabled_provisioners - enabled = [] - @env[:vm].config.vm.provisioners.each do |provisioner| - if @env["provision.types"] - # If we've specified types of provisioners to enable, then we - # only use those provisioners, and skip any that we haven't - # specified. - if !@env["provision.types"].include?(provisioner.shortcut.to_s) - @logger.debug("Skipping provisioner: #{provisioner.shortcut}") - next - end - end - - enabled << provisioner.provisioner.new(@env, provisioner.config) - end - - # Return the enable provisioners - enabled - end - end - end - end -end diff --git a/lib/vagrant/action/vm/provisioner_cleanup.rb b/lib/vagrant/action/vm/provisioner_cleanup.rb deleted file mode 100644 index 1c5b2f0e6..000000000 --- a/lib/vagrant/action/vm/provisioner_cleanup.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Vagrant - module Action - module VM - class ProvisionerCleanup - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - enabled_provisioners.each do |instance| - instance.cleanup - end - - @app.call(env) - end - - def enabled_provisioners - @env[:vm].config.vm.provisioners.map do |provisioner| - provisioner.provisioner.new(@env, provisioner.config) - end - end - end - end - end -end diff --git a/lib/vagrant/action/vm/prune_nfs_exports.rb b/lib/vagrant/action/vm/prune_nfs_exports.rb deleted file mode 100644 index b8b3b3061..000000000 --- a/lib/vagrant/action/vm/prune_nfs_exports.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Vagrant - module Action - module VM - class PruneNFSExports - def initialize(app, env) - @app = app - end - - def call(env) - if env[:host] - valid_ids = env[:vm].driver.read_vms - env[:host].nfs_prune(valid_ids) - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/sane_defaults.rb b/lib/vagrant/action/vm/sane_defaults.rb deleted file mode 100644 index f2dd31ab9..000000000 --- a/lib/vagrant/action/vm/sane_defaults.rb +++ /dev/null @@ -1,78 +0,0 @@ -require "log4r" - -module Vagrant - module Action - module VM - # This middleware enforces some sane defaults on the virtualbox - # VM which help with performance, stability, and in some cases - # behavior. - class SaneDefaults - def initialize(app, env) - @logger = Log4r::Logger.new("vagrant::action::vm::sanedefaults") - @app = app - end - - def call(env) - # Set the env on an instance variable so we can access it in - # helpers. - @env = env - - # Enable the host IO cache on the sata controller. Note that - # if this fails then its not a big deal, so we don't raise any - # errors. The Host IO cache vastly improves disk IO performance - # for VMs. - command = [ - "storagectl", env[:vm].uuid, - "--name", "SATA Controller", - "--hostiocache", "on" - ] - attempt_and_log(command, "Enabling the Host I/O cache on the SATA controller...") - - enable_dns_proxy = true - begin - contents = File.read("/etc/resolv.conf") - - if contents =~ /^nameserver 127\.0\.0\.1$/ - # The use of both natdnsproxy and natdnshostresolver break on - # Ubuntu 12.04 that uses resolvconf with localhost. When used - # VirtualBox will give the client dns server 10.0.2.3, while - # not binding to that address itself. Therefore disable this - # feature if host uses the resolvconf server 127.0.0.1 - @logger.info("Disabling DNS proxy since resolv.conf contains 127.0.0.1") - enable_dns_proxy = false - end - rescue Errno::ENOENT; end - - # Enable/disable the NAT DNS proxy as necessary - if enable_dns_proxy - command = [ - "modifyvm", env[:vm].uuid, - "--natdnsproxy1", "on" - ] - attempt_and_log(command, "Enable the NAT DNS proxy on adapter 1...") - else - command = [ "modifyvm", env[:vm].uuid, "--natdnsproxy1", "off" ] - attempt_and_log(command, "Disable the NAT DNS proxy on adapter 1...") - command = [ "modifyvm", env[:vm].uuid, "--natdnshostresolver1", "off" ] - attempt_and_log(command, "Disable the NAT DNS resolver on adapter 1...") - end - - @app.call(env) - end - - protected - - # This is just a helper method that executes a single command, logs - # the given string to the log, and also includes the exit status in - # the log message. - # - # @param [Array] command Command to run - # @param [String] log Log message to write. - def attempt_and_log(command, log) - result = @env[:vm].driver.execute_command(command) - @logger.info("#{log} (exit status = #{result.exit_code})") - end - end - end - end -end diff --git a/lib/vagrant/action/vm/setup_package_files.rb b/lib/vagrant/action/vm/setup_package_files.rb deleted file mode 100644 index f45210707..000000000 --- a/lib/vagrant/action/vm/setup_package_files.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'pathname' - -module Vagrant - module Action - module VM - # Sets up the mapping of files to copy into the package and verifies - # that the files can be properly copied. - class SetupPackageFiles - def initialize(app, env) - @app = app - - env["package.include"] ||= [] - env["package.vagrantfile"] ||= nil - end - - def call(env) - files = {} - env["package.include"].each do |file| - source = Pathname.new(file) - dest = nil - - # If the source is relative then we add the file as-is to the include - # directory. Otherwise, we copy only the file into the root of the - # include directory. Kind of strange, but seems to match what people - # expect based on history. - if source.relative? - dest = source - else - dest = source.basename - end - - # Assign the mapping - files[file] = dest - end - - if env["package.vagrantfile"] - # Vagrantfiles are treated special and mapped to a specific file - files[env["package.vagrantfile"]] = "_Vagrantfile" - end - - # Verify the mapping - files.each do |from, _| - raise Errors::PackageIncludeMissing, :file => from if !File.exist?(from) - end - - # Save the mapping - env["package.files"] = files - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/share_folders.rb b/lib/vagrant/action/vm/share_folders.rb deleted file mode 100644 index 29dc4ccf8..000000000 --- a/lib/vagrant/action/vm/share_folders.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'pathname' - -require 'log4r' - -module Vagrant - module Action - module VM - class ShareFolders - def initialize(app, env) - @logger = Log4r::Logger.new("vagrant::action::vm::share_folders") - @app = app - end - - def call(env) - @env = env - - prepare_folders - create_metadata - - @app.call(env) - - mount_shared_folders - end - - # This method returns an actual list of VirtualBox shared - # folders to create and their proper path. - def shared_folders - @env[:vm].config.vm.shared_folders.inject({}) do |acc, data| - key, value = data - - next acc if value[:disabled] - - # This to prevent overwriting the actual shared folders data - value = value.dup - acc[key] = value - acc - end - end - - # Prepares the shared folders by verifying they exist and creating them - # if they don't. - def prepare_folders - shared_folders.each do |name, options| - hostpath = Pathname.new(options[:hostpath]).expand_path(@env[:root_path]) - - if !hostpath.directory? && options[:create] - # Host path doesn't exist, so let's create it. - @logger.debug("Host path doesn't exist, creating: #{hostpath}") - - begin - hostpath.mkpath - rescue Errno::EACCES - raise Errors::SharedFolderCreateFailed, :path => hostpath.to_s - end - end - end - end - - def create_metadata - @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.creating") - - folders = [] - shared_folders.each do |name, data| - folders << { - :name => name, - :hostpath => File.expand_path(data[:hostpath], @env[:root_path]), - :transient => data[:transient] - } - end - - @env[:vm].driver.share_folders(folders) - end - - def mount_shared_folders - @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.mounting") - - # short guestpaths first, so we don't step on ourselves - folders = shared_folders.sort_by do |name, data| - if data[:guestpath] - data[:guestpath].length - else - # A long enough path to just do this at the end. - 10000 - end - end - - # Go through each folder and mount - folders.each do |name, data| - if data[:guestpath] - # Guest path specified, so mount the folder to specified point - @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", - :name => name, - :guest_path => data[:guestpath])) - - # Dup the data so we can pass it to the guest API - data = data.dup - - # Calculate the owner and group - data[:owner] ||= @env[:vm].config.ssh.username - data[:group] ||= @env[:vm].config.ssh.username - - # Mount the actual folder - @env[:vm].guest.mount_shared_folder(name, data[:guestpath], data) - else - # If no guest path is specified, then automounting is disabled - @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", - :name => name)) - end - end - end - end - end - end -end From 85a4fb82a88806b232e64a8aed1f97f31e8dc9c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Aug 2012 21:04:37 -0700 Subject: [PATCH 52/60] `vagrant package` a single VM works! --- plugins/commands/package/command.rb | 23 ++++---- plugins/providers/virtualbox/action.rb | 26 +++++++++ plugins/providers/virtualbox/action/export.rb | 56 +++++++++++++++++++ .../providers/virtualbox/action/package.rb | 20 +++++++ .../virtualbox/action/package_vagrantfile.rb | 33 +++++++++++ .../virtualbox/action/setup_package_files.rb | 51 +++++++++++++++++ 6 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 plugins/providers/virtualbox/action/export.rb create mode 100644 plugins/providers/virtualbox/action/package.rb create mode 100644 plugins/providers/virtualbox/action/package_vagrantfile.rb create mode 100644 plugins/providers/virtualbox/action/setup_package_files.rb diff --git a/plugins/commands/package/command.rb b/plugins/commands/package/command.rb index d8b5e35fb..e1f14721a 100644 --- a/plugins/commands/package/command.rb +++ b/plugins/commands/package/command.rb @@ -6,25 +6,25 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant package [vm-name] [--base name] [--output name.box]" - opts.separator " [--include one,two,three] [--vagrantfile file]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant package [vm-name] [--base name] [--output name.box]" + o.separator " [--include one,two,three] [--vagrantfile file]" - opts.separator "" + o.separator "" - opts.on("--base NAME", "Name of a VM in virtualbox to package as a base box") do |b| + o.on("--base NAME", "Name of a VM in virtualbox to package as a base box") do |b| options[:base] = b end - opts.on("--output NAME", "Name of the file to output") do |o| - options[:output] = o + o.on("--output NAME", "Name of the file to output") do |output| + options[:output] = output end - opts.on("--include x,y,z", Array, "Additional files to package with the box.") do |i| + o.on("--include x,y,z", Array, "Additional files to package with the box.") do |i| options[:include] = i end - opts.on("--vagrantfile file", "Vagrantfile to package with the box.") do |v| + o.on("--vagrantfile file", "Vagrantfile to package with the box.") do |v| options[:vagrantfile] = v end end @@ -42,7 +42,7 @@ module VagrantPlugins # Success, exit status 0 0 - end + end protected @@ -55,7 +55,6 @@ module VagrantPlugins def package_target(name, options) with_target_vms(name, :single_target => true) do |vm| - raise Vagrant::Errors::VMNotCreatedError if !vm.created? @logger.debug("Packaging VM: #{vm.name}") package_vm(vm, options) end @@ -68,7 +67,7 @@ module VagrantPlugins acc end - vm.package(opts) + vm.action(:package, opts) end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 47f5021c0..a775c72e3 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -22,6 +22,7 @@ module VagrantPlugins autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) + autoload :Export, File.expand_path("../action/export", __FILE__) autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :HostName, File.expand_path("../action/host_name", __FILE__) @@ -34,11 +35,14 @@ module VagrantPlugins autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) autoload :Network, File.expand_path("../action/network", __FILE__) autoload :NFS, File.expand_path("../action/nfs", __FILE__) + autoload :Package, File.expand_path("../action/package", __FILE__) + autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) autoload :Provision, File.expand_path("../action/provision", __FILE__) autoload :ProvisionerCleanup, File.expand_path("../action/provisioner_cleanup", __FILE__) autoload :PruneNFSExports, File.expand_path("../action/prune_nfs_exports", __FILE__) autoload :Resume, File.expand_path("../action/resume", __FILE__) autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) + autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) autoload :ShareFolders, File.expand_path("../action/share_folders", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) @@ -116,6 +120,28 @@ module VagrantPlugins end end + # This action packages the virtual machine into a single box file. + def self.action_package + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use SetupPackageFiles + b2.use CheckAccessible + b2.use action_halt + b2.use ClearForwardedPorts + b2.use ClearSharedFolders + b2.use Export + b2.use PackageVagrantfile + b2.use Package + end + end + end + # This action just runs the provisioners on the machine. def self.action_provision Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/export.rb b/plugins/providers/virtualbox/action/export.rb new file mode 100644 index 000000000..2bf9f3353 --- /dev/null +++ b/plugins/providers/virtualbox/action/export.rb @@ -0,0 +1,56 @@ +require "fileutils" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class Export + attr_reader :temp_dir + + def initialize(app, env) + @app = app + end + + def call(env) + @env = env + + raise Vagrant::Errors::VMPowerOffToPackage if @env[:machine].provider.state != :poweroff + + setup_temp_dir + export + + @app.call(env) + + recover(env) # called to cleanup temp directory + end + + def recover(env) + if temp_dir && File.exist?(temp_dir) + FileUtils.rm_rf(temp_dir) + end + end + + def setup_temp_dir + @env[:ui].info I18n.t("vagrant.actions.vm.export.create_dir") + @temp_dir = @env["export.temp_dir"] = @env[:tmp_path].join(Time.now.to_i.to_s) + FileUtils.mkpath(@env["export.temp_dir"]) + end + + def export + @env[:ui].info I18n.t("vagrant.actions.vm.export.exporting") + @env[:machine].provider.driver.export(ovf_path) do |progress| + @env[:ui].clear_line + @env[:ui].report_progress(progress.percent, 100, false) + end + + # Clear the line a final time so the next data can appear + # alone on the line. + @env[:ui].clear_line + end + + def ovf_path + File.join(@env["export.temp_dir"], "box.ovf") + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/package.rb b/plugins/providers/virtualbox/action/package.rb new file mode 100644 index 000000000..5a6c581ce --- /dev/null +++ b/plugins/providers/virtualbox/action/package.rb @@ -0,0 +1,20 @@ +require 'vagrant/action/general/package' + +module VagrantPlugins + module ProviderVirtualBox + module Action + class Package < Vagrant::Action::General::Package + # Doing this so that we can test that the parent is properly + # called in the unit tests. + alias_method :general_call, :call + def call(env) + # Just match up a couple environmental variables so that + # the superclass will do the right thing. Then, call the + # superclass + env["package.directory"] = env["export.temp_dir"] + general_call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/package_vagrantfile.rb b/plugins/providers/virtualbox/action/package_vagrantfile.rb new file mode 100644 index 000000000..0517718ee --- /dev/null +++ b/plugins/providers/virtualbox/action/package_vagrantfile.rb @@ -0,0 +1,33 @@ +require 'vagrant/util/template_renderer' + +module VagrantPlugins + module ProviderVirtualBox + module Action + class PackageVagrantfile + # For TemplateRenderer + include Vagrant::Util + + def initialize(app, env) + @app = app + end + + def call(env) + @env = env + create_vagrantfile + @app.call(env) + end + + # This method creates the auto-generated Vagrantfile at the root of the + # box. This Vagrantfile contains the MAC address so that the user doesn't + # have to worry about it. + def create_vagrantfile + File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| + f.write(TemplateRenderer.render("package_Vagrantfile", { + :base_mac => @env[:machine].provider.driver.read_mac_address + })) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/setup_package_files.rb b/plugins/providers/virtualbox/action/setup_package_files.rb new file mode 100644 index 000000000..93e1fdd89 --- /dev/null +++ b/plugins/providers/virtualbox/action/setup_package_files.rb @@ -0,0 +1,51 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SetupPackageFiles + def initialize(app, env) + @app = app + + env["package.include"] ||= [] + env["package.vagrantfile"] ||= nil + end + + def call(env) + files = {} + env["package.include"].each do |file| + source = Pathname.new(file) + dest = nil + + # If the source is relative then we add the file as-is to the include + # directory. Otherwise, we copy only the file into the root of the + # include directory. Kind of strange, but seems to match what people + # expect based on history. + if source.relative? + dest = source + else + dest = source.basename + end + + # Assign the mapping + files[file] = dest + end + + if env["package.vagrantfile"] + # Vagrantfiles are treated special and mapped to a specific file + files[env["package.vagrantfile"]] = "_Vagrantfile" + end + + # Verify the mapping + files.each do |from, _| + raise Vagrant::Errors::PackageIncludeMissing, + :file => from if !File.exist?(from) + end + + # Save the mapping + env["package.files"] = files + + @app.call(env) + end + end + end + end +end From fcffcb2ee0b2b103b2547f2a7236fe5ba3971c13 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Aug 2012 21:55:25 -0700 Subject: [PATCH 53/60] Delete more unused actions --- lib/vagrant/action/env/set.rb | 21 -------- lib/vagrant/action/vm/export.rb | 57 -------------------- lib/vagrant/action/vm/package.rb | 23 -------- lib/vagrant/action/vm/package_vagrantfile.rb | 36 ------------- 4 files changed, 137 deletions(-) delete mode 100644 lib/vagrant/action/env/set.rb delete mode 100644 lib/vagrant/action/vm/export.rb delete mode 100644 lib/vagrant/action/vm/package.rb delete mode 100644 lib/vagrant/action/vm/package_vagrantfile.rb diff --git a/lib/vagrant/action/env/set.rb b/lib/vagrant/action/env/set.rb deleted file mode 100644 index 38e23915f..000000000 --- a/lib/vagrant/action/env/set.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Vagrant - module Action - module Env - # A middleware which just sets up the environment with some - # options which are passed to it. - class Set - def initialize(app, env, options=nil) - @app = app - @options = options || {} - end - - def call(env) - # Merge the options that were given to us - env.merge!(@options) - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/export.rb b/lib/vagrant/action/vm/export.rb deleted file mode 100644 index 8228f86df..000000000 --- a/lib/vagrant/action/vm/export.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'fileutils' - -module Vagrant - module Action - module VM - class Export - attr_reader :temp_dir - - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env = env - - raise Errors::VMPowerOffToPackage if @env["vm"].state != :poweroff - - setup_temp_dir - export - - @app.call(env) - - recover(env) # called to cleanup temp directory - end - - def recover(env) - if temp_dir && File.exist?(temp_dir) - FileUtils.rm_rf(temp_dir) - end - end - - def setup_temp_dir - @env[:ui].info I18n.t("vagrant.actions.vm.export.create_dir") - @temp_dir = @env["export.temp_dir"] = @env[:tmp_path].join(Time.now.to_i.to_s) - FileUtils.mkpath(@env["export.temp_dir"]) - end - - def export - @env[:ui].info I18n.t("vagrant.actions.vm.export.exporting") - @env[:vm].driver.export(ovf_path) do |progress| - @env[:ui].clear_line - @env[:ui].report_progress(progress.percent, 100, false) - end - - # Clear the line a final time so the next data can appear - # alone on the line. - @env[:ui].clear_line - end - - def ovf_path - File.join(@env["export.temp_dir"], "box.ovf") - end - end - end - end -end diff --git a/lib/vagrant/action/vm/package.rb b/lib/vagrant/action/vm/package.rb deleted file mode 100644 index d392d6514..000000000 --- a/lib/vagrant/action/vm/package.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'vagrant/action/general/package' - -module Vagrant - module Action - module VM - # A subclass of {General::Package} which simply makes sure that - # the package directory is set to the directory which the VM - # was exported to. - class Package < General::Package - # Doing this so that we can test that the parent is properly - # called in the unit tests. - alias_method :general_call, :call - def call(env) - # Just match up a couple environmental variables so that - # the superclass will do the right thing. Then, call the - # superclass - env["package.directory"] = env["export.temp_dir"] - general_call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/package_vagrantfile.rb b/lib/vagrant/action/vm/package_vagrantfile.rb deleted file mode 100644 index 17869091f..000000000 --- a/lib/vagrant/action/vm/package_vagrantfile.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'vagrant/util/template_renderer' - -module Vagrant - module Action - module VM - # Puts a generated Vagrantfile into the package directory so that - # it can be included in the package. - class PackageVagrantfile - # For TemplateRenderer - include Util - - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env = env - create_vagrantfile - @app.call(env) - end - - # This method creates the auto-generated Vagrantfile at the root of the - # box. This Vagrantfile contains the MAC address so that the user doesn't - # have to worry about it. - def create_vagrantfile - File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| - f.write(TemplateRenderer.render("package_Vagrantfile", { - :base_mac => @env["vm"].driver.read_mac_address - })) - end - end - end - end - end -end From fa4cf63462be1b1ed6c5d9dba25717c4dad2a5ae Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Aug 2012 21:55:47 -0700 Subject: [PATCH 54/60] Remove the actions that are unavailable now from autoload --- lib/vagrant/action.rb | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 482cf82e3..caafa1d47 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -22,49 +22,9 @@ module Vagrant autoload :SSHRun, "vagrant/action/builtin/ssh_run" end - module Env - autoload :Set, 'vagrant/action/env/set' - end - module General - autoload :CheckVirtualbox, 'vagrant/action/general/check_virtualbox' autoload :Package, 'vagrant/action/general/package' autoload :Validate, 'vagrant/action/general/validate' end - - module VM - autoload :Boot, 'vagrant/action/vm/boot' - autoload :CheckAccessible, 'vagrant/action/vm/check_accessible' - autoload :CheckBox, 'vagrant/action/vm/check_box' - autoload :CheckGuestAdditions, 'vagrant/action/vm/check_guest_additions' - autoload :CheckPortCollisions, 'vagrant/action/vm/check_port_collisions' - autoload :CleanMachineFolder, 'vagrant/action/vm/clean_machine_folder' - autoload :ClearForwardedPorts, 'vagrant/action/vm/clear_forwarded_ports' - autoload :ClearNetworkInterfaces, 'vagrant/action/vm/clear_network_interfaces' - autoload :ClearSharedFolders, 'vagrant/action/vm/clear_shared_folders' - autoload :Customize, 'vagrant/action/vm/customize' - autoload :DefaultName, 'vagrant/action/vm/default_name' - autoload :Destroy, 'vagrant/action/vm/destroy' - autoload :DestroyUnusedNetworkInterfaces, 'vagrant/action/vm/destroy_unused_network_interfaces' - autoload :DiscardState, 'vagrant/action/vm/discard_state' - autoload :Export, 'vagrant/action/vm/export' - autoload :ForwardPorts, 'vagrant/action/vm/forward_ports' - autoload :Halt, 'vagrant/action/vm/halt' - autoload :HostName, 'vagrant/action/vm/host_name' - autoload :Import, 'vagrant/action/vm/import' - autoload :MatchMACAddress, 'vagrant/action/vm/match_mac_address' - autoload :Network, 'vagrant/action/vm/network' - autoload :NFS, 'vagrant/action/vm/nfs' - autoload :Package, 'vagrant/action/vm/package' - autoload :PackageVagrantfile, 'vagrant/action/vm/package_vagrantfile' - autoload :Provision, 'vagrant/action/vm/provision' - autoload :ProvisionerCleanup, 'vagrant/action/vm/provisioner_cleanup' - autoload :PruneNFSExports, 'vagrant/action/vm/prune_nfs_exports' - autoload :Resume, 'vagrant/action/vm/resume' - autoload :SaneDefaults, 'vagrant/action/vm/sane_defaults' - autoload :ShareFolders, 'vagrant/action/vm/share_folders' - autoload :SetupPackageFiles, 'vagrant/action/vm/setup_package_files' - autoload :Suspend, 'vagrant/action/vm/suspend' - end end end From 47fe278667316f96222d593dc9a9d89ee23eadc0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 18 Aug 2012 16:13:14 -0700 Subject: [PATCH 55/60] `vagrant box add` works again. Box verification remove temporarily. The built-in middleware sequences will now be hardcoded onto Vagrant::Action. Other plugins can hook into these sequences to provide verification and so on. So the VirtualBox plugin will hook into that action sequence and add verification. --- lib/vagrant/action.rb | 17 +-- lib/vagrant/action/box/add.rb | 31 ---- lib/vagrant/action/box/download.rb | 84 ----------- lib/vagrant/action/box/verify.rb | 24 ---- lib/vagrant/action/builtin.rb | 157 --------------------- lib/vagrant/action/builtin/box_add.rb | 75 ++++++++++ lib/vagrant/downloaders/base.rb | 3 - lib/vagrant/downloaders/file.rb | 6 +- plugins/commands/box/command/add.rb | 11 +- test/unit/vagrant/downloaders/base_test.rb | 4 - test/unit/vagrant/downloaders/file_test.rb | 26 ++-- 11 files changed, 108 insertions(+), 330 deletions(-) delete mode 100644 lib/vagrant/action/box/add.rb delete mode 100644 lib/vagrant/action/box/download.rb delete mode 100644 lib/vagrant/action/box/verify.rb delete mode 100644 lib/vagrant/action/builtin.rb create mode 100644 lib/vagrant/action/builtin/box_add.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index caafa1d47..a29e8b893 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -6,25 +6,26 @@ module Vagrant autoload :Runner, 'vagrant/action/runner' autoload :Warden, 'vagrant/action/warden' - module Box - autoload :Add, 'vagrant/action/box/add' - autoload :Download, 'vagrant/action/box/download' - autoload :Verify, 'vagrant/action/box/verify' - end - # Builtin contains middleware classes that are shipped with Vagrant-core # and are thus available to all plugins as a "standard library" of sorts. module Builtin - autoload :Call, "vagrant/action/builtin/call" + autoload :BoxAdd, "vagrant/action/builtin/box_add" + autoload :Call, "vagrant/action/builtin/call" autoload :Confirm, "vagrant/action/builtin/confirm" autoload :EnvSet, "vagrant/action/builtin/env_set" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" - autoload :SSHRun, "vagrant/action/builtin/ssh_run" + autoload :SSHRun, "vagrant/action/builtin/ssh_run" end module General autoload :Package, 'vagrant/action/general/package' autoload :Validate, 'vagrant/action/general/validate' end + + def self.action_box_add + Builder.new.tap do |b| + b.use Builtin::BoxAdd + end + end end end diff --git a/lib/vagrant/action/box/add.rb b/lib/vagrant/action/box/add.rb deleted file mode 100644 index e80d52970..000000000 --- a/lib/vagrant/action/box/add.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Vagrant - module Action - module Box - # Adds a downloaded box file to the environment's box collection. - # This handles unpacking the box. See {BoxCollection#add} for more - # information. - class Add - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env[:ui].info I18n.t("vagrant.actions.box.add.adding", :name => env[:box_name]) - - begin - env[:box_collection].add(env[:box_download_temp_path], env[:box_name]) - rescue Vagrant::Errors::BoxUpgradeRequired - # Upgrade the box - env[:box_collection].upgrade(env[:box_name]) - - # Try adding it again - retry - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/box/download.rb b/lib/vagrant/action/box/download.rb deleted file mode 100644 index 144546f27..000000000 --- a/lib/vagrant/action/box/download.rb +++ /dev/null @@ -1,84 +0,0 @@ -module Vagrant - module Action - module Box - class Download - BASENAME = "box" - - include Util - - attr_reader :temp_path - - def initialize(app, env) - @app = app - @env = env - @env["download.classes"] ||= [] - @env["download.classes"] += [Downloaders::HTTP, Downloaders::File] - @downloader = nil - end - - def call(env) - @env = env - - download if instantiate_downloader - @app.call(@env) - - recover(env) # called in both cases to cleanup workspace - end - - def instantiate_downloader - # Assign to a temporary variable since this is easier to type out, - # since it is used so many times. - classes = @env["download.classes"] - - # Find the class to use. - classes.each_index do |i| - klass = classes[i] - - # Use the class if it matches the given URI or if this - # is the last class... - if classes.length == (i + 1) || klass.match?(@env[:box_url]) - @env[:ui].info I18n.t("vagrant.actions.box.download.with", :class => klass.to_s) - @downloader = klass.new(@env[:ui]) - break - end - end - - # This line should never be reached, but we'll keep this here - # just in case for now. - raise Errors::BoxDownloadUnknownType if !@downloader - - @downloader.prepare(@env[:box_url]) - true - end - - def download - with_tempfile do |tempfile| - download_to(tempfile) - @temp_path = @env[:box_download_temp_path] = tempfile.path - end - end - - def recover(env) - if temp_path && File.exist?(temp_path) - env[:ui].info I18n.t("vagrant.actions.box.download.cleaning") - File.unlink(temp_path) - end - end - - def with_tempfile - File.open(box_temp_path, Platform.tar_file_options) do |tempfile| - yield tempfile - end - end - - def box_temp_path - @env[:tmp_path].join(BASENAME + Time.now.to_i.to_s) - end - - def download_to(f) - @downloader.download!(@env[:box_url], f) - end - end - end - end -end diff --git a/lib/vagrant/action/box/verify.rb b/lib/vagrant/action/box/verify.rb deleted file mode 100644 index 082cf4fc0..000000000 --- a/lib/vagrant/action/box/verify.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Vagrant - module Action - module Box - class Verify - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env[:ui].info I18n.t("vagrant.actions.box.verify.verifying") - - box = env[:box_collection].find(env[:box_name], :virtualbox) - driver = Driver::VirtualBox.new(nil) - if !driver.verify_image(box.directory.join("box.ovf").to_s) - raise Errors::BoxVerificationFailed - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/builtin.rb b/lib/vagrant/action/builtin.rb deleted file mode 100644 index b047ef43e..000000000 --- a/lib/vagrant/action/builtin.rb +++ /dev/null @@ -1,157 +0,0 @@ -module Vagrant - module Action - # A registry object containing the built-in middleware stacks. - class Builtin < Registry - def initialize - # Properly initialize the registry object - super - - # Register all the built-in stacks - register_builtin! - end - - protected - - def register_builtin! - # We do this so that the blocks below have a variable to access the - # outer registry. - registry = self - - # provision - Provisions a running VM - register(:provision) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::Provision - end - end - - # start - Starts a VM, assuming it already exists on the - # environment. - register(:start) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::CleanMachineFolder - use VM::ClearForwardedPorts - use VM::CheckPortCollisions, :port_collision_handler => :correct - use VM::ForwardPorts - use VM::Provision - use VM::PruneNFSExports - use VM::NFS - use VM::ClearSharedFolders - use VM::ShareFolders - use VM::ClearNetworkInterfaces - use VM::Network - use VM::HostName - use VM::SaneDefaults - use VM::Customize - use VM::Boot - end - end - - # halt - Halts the VM, attempting gracefully but then forcing - # a restart if fails. - register(:halt) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::DiscardState - use VM::Halt - end - end - - # suspend - Suspends the VM - register(:suspend) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::Suspend - end - end - - # resume - Resume a VM - register(:resume) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::CheckPortCollisions - use VM::Resume - end - end - - # reload - Halts then restarts the VM - register(:reload) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use registry.get(:halt) - use registry.get(:start) - end - end - - # up - Imports, prepares, then starts a fresh VM. - register(:up) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::CheckBox - use VM::Import - use VM::CheckGuestAdditions - use VM::DefaultName - use VM::MatchMACAddress - use registry.get(:start) - end - end - - # destroy - Halts, cleans up, and destroys an existing VM - register(:destroy) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use registry.get(:halt), :force => true - use VM::ProvisionerCleanup - use VM::PruneNFSExports - use VM::Destroy - use VM::CleanMachineFolder - use VM::DestroyUnusedNetworkInterfaces - end - end - - # package - Export and package the VM - register(:package) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::SetupPackageFiles - use VM::CheckAccessible - use registry.get(:halt) - use VM::ClearForwardedPorts - use VM::ClearSharedFolders - use VM::Export - use VM::PackageVagrantfile - use VM::Package - end - end - - # box_add - Download and add a box. - register(:box_add) do - Builder.new do - use General::CheckVirtualbox - use Box::Download - use Box::Add - use Box::Verify - end - end - end - end - end -end diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb new file mode 100644 index 000000000..364cd27e6 --- /dev/null +++ b/lib/vagrant/action/builtin/box_add.rb @@ -0,0 +1,75 @@ +require "vagrant/util/platform" + +module Vagrant + module Action + module Builtin + # This middleware will download a remote box and add it to the + # given box collection. + class BoxAdd + def initialize(app, env) + @app = app + end + + def call(env) + # Instantiate the downloader + downloader = download_klass(env[:box_url]).new(env[:ui]) + env[:ui].info I18n.t("vagrant.actions.box.download.with", + :class => downloader.class.to_s) + + # Download the box to a temporary path. We store the temporary + # path as an instance variable so that the `#recover` method can + # access it. + @temp_path = env[:tmp_path].join("box" + Time.now.to_i.to_s) + File.open(@temp_path, Vagrant::Util::Platform.tar_file_options) do |f| + downloader.download!(env[:box_url], f) + end + + # Add the box + env[:ui].info I18n.t("vagrant.actions.box.add.adding", :name => env[:box_name]) + begin + env[:box_collection].add(@temp_path, env[:box_name]) + rescue Vagrant::Errors::BoxUpgradeRequired + # Upgrade the box + env[:box_collection].upgrade(env[:box_name]) + + # Try adding it again + retry + end + + # Call the 'recover' method in all cases to clean up the + # downloaded temporary file. + recover(env) + + # Carry on! + @app.call(env) + end + + def download_klass(url) + # This is hardcoded for now. In the future I'd like to make this + # pluginnable as well. + classes = [Downloaders::HTTP, Downloaders::File] + + # Find the class to use. + classes.each_index do |i| + klass = classes[i] + + # Use the class if it matches the given URI or if this + # is the last class... + return klass if classes.length == (i + 1) || klass.match?(url) + end + + # If no downloader knows how to download this file, then we + # raise an exception. + raise Errors::BoxDownloadUnknownType + end + + def recover(env) + if @temp_path && File.exist?(@temp_path) + env[:ui].info I18n.t("vagrant.actions.box.download.cleaning") + File.unlink(@temp_path) + end + end + end + end + end +end diff --git a/lib/vagrant/downloaders/base.rb b/lib/vagrant/downloaders/base.rb index 12fe8903d..5cc3cb980 100644 --- a/lib/vagrant/downloaders/base.rb +++ b/lib/vagrant/downloaders/base.rb @@ -14,9 +14,6 @@ module Vagrant # handle. def self.match?(url); false; end - # Called prior to execution so any error checks can be done - def prepare(source_url); end - # Downloads the source file to the destination file. It is up to # implementors of this class to handle the logic. def download!(source_url, destination_file); end diff --git a/lib/vagrant/downloaders/file.rb b/lib/vagrant/downloaders/file.rb index bbd87ce4d..63ca0ba56 100644 --- a/lib/vagrant/downloaders/file.rb +++ b/lib/vagrant/downloaders/file.rb @@ -9,11 +9,9 @@ module Vagrant ::File.file?(::File.expand_path(uri)) end - def prepare(source_url) - raise Errors::DownloaderFileDoesntExist if !::File.file?(::File.expand_path(source_url)) - end - def download!(source_url, destination_file) + raise Errors::DownloaderFileDoesntExist if !::File.file?(::File.expand_path(source_url)) + @ui.info I18n.t("vagrant.downloaders.file.download") FileUtils.cp(::File.expand_path(source_url), destination_file.path) end diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index 996074b26..f2af90389 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -7,11 +7,11 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant box add " - opts.separator "" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant box add " + o.separator "" - opts.on("-f", "--force", "Overwrite an existing box if it exists.") do |f| + o.on("-f", "--force", "Overwrite an existing box if it exists.") do |f| options[:force] = f end end @@ -28,8 +28,7 @@ module VagrantPlugins existing.destroy! if existing end - # Invoke the "box_add" middleware sequence. - @env.action_runner.run(:box_add, { + @env.action_runner.run(Vagrant::Action.action_box_add, { :box_name => argv[0], :box_provider => :virtualbox, :box_url => argv[1] diff --git a/test/unit/vagrant/downloaders/base_test.rb b/test/unit/vagrant/downloaders/base_test.rb index 963578d96..43e27d8b7 100644 --- a/test/unit/vagrant/downloaders/base_test.rb +++ b/test/unit/vagrant/downloaders/base_test.rb @@ -8,10 +8,6 @@ describe Vagrant::Downloaders::Base do described_class.match?("foo").should_not be end - it "should implement `prepare`" do - instance.prepare("foo").should be_nil - end - it "should implement `download!`" do instance.download!("foo", "bar").should be_nil end diff --git a/test/unit/vagrant/downloaders/file_test.rb b/test/unit/vagrant/downloaders/file_test.rb index 170fe8989..ad4fb3f03 100644 --- a/test/unit/vagrant/downloaders/file_test.rb +++ b/test/unit/vagrant/downloaders/file_test.rb @@ -3,9 +3,15 @@ require File.expand_path("../../../base", __FILE__) require "tempfile" describe Vagrant::Downloaders::File do + include_context "unit" + let(:ui) { double("ui") } let(:instance) { described_class.new(ui) } + before(:each) do + ui.stub(:info) + end + describe "matching" do it "should match an existing file" do described_class.match?(__FILE__).should be @@ -19,33 +25,37 @@ describe Vagrant::Downloaders::File do old_home = ENV["HOME"] begin # Create a temporary file - temp = Tempfile.new("vagrant") + temp = temporary_file # Set our home directory to be this directory so we can use # "~" paths - ENV["HOME"] = File.dirname(temp.path) + ENV["HOME"] = File.dirname(temp.to_s) # Test that we can find the temp file - described_class.match?("~/#{File.basename(temp.path)}").should be + described_class.match?("~/#{File.basename(temp.to_s)}").should be ensure ENV["HOME"] = old_home end end end - describe "preparing" do + describe "downloading" do + let(:destination_file) { temporary_file.open("w") } + it "should raise an exception if the file does not exist" do path = File.join(__FILE__, "nopenopenope") File.exist?(path).should_not be - expect { instance.prepare(path) }.to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) + expect { instance.download!(path, destination_file) }. + to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) end it "should raise an exception if the file is a directory" do path = File.dirname(__FILE__) File.should be_directory(path) - expect { instance.prepare(path) }.to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) + expect { instance.download!(path, destination_file) }. + to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) end it "should find files that use shell expansions" do @@ -59,15 +69,13 @@ describe Vagrant::Downloaders::File do ENV["HOME"] = File.dirname(temp.path) # Test that we can find the temp file - expect { instance.prepare("~/#{File.basename(temp.path)}") }. + expect { instance.download!("~/#{File.basename(temp.path)}", destination_file) }. to_not raise_error ensure ENV["HOME"] = old_home end end - end - describe "downloading" do it "should copy the source to the destination" do pending "setup paths" end From 78d79de5c4d74c3cf81dc3c74869e13f7710c594 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 18 Aug 2012 16:19:13 -0700 Subject: [PATCH 56/60] Comment the action_box_add method --- lib/vagrant/action.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index a29e8b893..0df155d44 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -22,6 +22,11 @@ module Vagrant autoload :Validate, 'vagrant/action/general/validate' end + # This is the action that will add a box from a URL. This middleware + # sequence is built-in to Vagrant. Plugins can hook into this like any + # other middleware sequence. This is particularly useful for provider + # plugins, which can hook in to do things like verification of boxes + # that are downloaded. def self.action_box_add Builder.new.tap do |b| b.use Builtin::BoxAdd From a7b298b6473ce2995efb381a70adbc05c0d1ba9e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 18 Aug 2012 19:36:50 -0700 Subject: [PATCH 57/60] Remove Vagrant::VM --- lib/vagrant.rb | 1 - lib/vagrant/vm.rb | 205 ---------------------------------------------- 2 files changed, 206 deletions(-) delete mode 100644 lib/vagrant/vm.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 1e160ab47..c7fd558a3 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -80,7 +80,6 @@ module Vagrant autoload :TestHelpers, 'vagrant/test_helpers' autoload :UI, 'vagrant/ui' autoload :Util, 'vagrant/util' - autoload :VM, 'vagrant/vm' # These are the various plugin versions and their components in # a lazy loaded Hash-like structure. diff --git a/lib/vagrant/vm.rb b/lib/vagrant/vm.rb deleted file mode 100644 index d35c25535..000000000 --- a/lib/vagrant/vm.rb +++ /dev/null @@ -1,205 +0,0 @@ -require 'log4r' - -module Vagrant - class VM - include Vagrant::Util - - attr_reader :uuid - attr_reader :env - attr_reader :name - attr_reader :vm - attr_reader :box - attr_reader :config - attr_reader :driver - - def initialize(name, env, config, opts=nil) - @logger = Log4r::Logger.new("vagrant::vm") - - @name = name - @vm = nil - @env = env - @config = config - @box = nil - @box = env.boxes.find(config.vm.box, :virtualbox) if config.vm.box - - opts ||= {} - if opts[:base] - # The name is the ID we use. - @uuid = name - else - # Load the UUID if its saved. - active = env.local_data[:active] || {} - @uuid = active[@name.to_s] - end - - # Reload ourselves to get the state - reload! - - # Load the associated guest. - load_guest! - @loaded_guest_distro = false - end - - # Loads the guest associated with the VM. The guest class is - # responsible for OS-specific functionality. More information - # can be found by reading the documentation on {Vagrant::Guest::Base}. - # - # **This method should never be called manually.** - def load_guest!(guest=nil) - guest ||= config.vm.guest - @logger.info("Loading guest: #{guest}") - - if guest.is_a?(Class) - raise Errors::VMGuestError, :_key => :invalid_class, :guest => guest.to_s if !(guest <= Vagrant.plugin("1", :guest)) - @guest = guest.new(self) - elsif guest.is_a?(Symbol) - # Look for the guest as a registered plugin - guest_klass = nil - Vagrant.plugin("1").registered.each do |plugin| - if plugin.guest.has_key?(guest) - guest_klass = plugin.guest[guest] - break - end - end - - raise Errors::VMGuestError, :_key => :unknown_type, :guest => guest.to_s if !guest_klass - @guest = guest_klass.new(self) - else - raise Errors::VMGuestError, :unspecified - end - end - - # Returns a channel object to communicate with the virtual - # machine. - def channel - @channel ||= Communication::SSH.new(self) - end - - # Returns the guest for this VM, loading the distro of the system if - # we can. - def guest - if !@loaded_guest_distro && state == :running - # Load the guest distro for the first time - result = @guest.distro_dispatch - load_guest!(result) - @loaded_guest_distro = true - @logger.info("Guest class: #{@guest.class}") - end - - @guest - end - - # Access the {Vagrant::SSH} object associated with this VM, which - # is used to get SSH credentials with the virtual machine. - def ssh - @ssh ||= SSH.new(self) - end - - # Returns the state of the VM as a symbol. - # - # @return [Symbol] - def state - return :not_created if !@uuid - state = @driver.read_state - return :not_created if !state - return state - end - - # Returns a boolean true if the VM has been created, otherwise - # returns false. - # - # @return [Boolean] - def created? - state != :not_created - end - - # Sets the currently active VM for this VM. If the VM is a valid, - # created virtual machine, then it will also update the local data - # to persist the VM. Otherwise, it will remove itself from the - # local data (if it exists). - def uuid=(value) - env.local_data[:active] ||= {} - if value - env.local_data[:active][name.to_s] = value - else - env.local_data[:active].delete(name.to_s) - end - - # Commit the local data so that the next time vagrant is initialized, - # it realizes the VM exists - env.local_data.commit - - # Store the uuid and reload the instance - @uuid = value - reload! - end - - def reload! - begin - @driver = Driver::VirtualBox.new(@uuid) - rescue Driver::VirtualBox::VMNotFound - # Clear the UUID since this VM doesn't exist. - @uuid = nil - - # Reset the driver. This shouldn't raise a VMNotFound since we won't - # feed it a UUID. - @driver = Driver::VirtualBox.new - end - end - - def package(options=nil) - run_action(:package, { "validate" => false }.merge(options || {})) - end - - def up(options=nil) - run_action(:up, options) - end - - def start(options=nil) - return if state == :running - return resume if state == :saved - - run_action(:start, options) - end - - def halt(options=nil) - run_action(:halt, options) - end - - def reload(options=nil) - run_action(:reload, options) - end - - def provision - run_action(:provision) - end - - def destroy - run_action(:destroy) - end - - def suspend - run_action(:suspend) - end - - def resume - run_action(:resume) - end - - def ui - return @_ui if defined?(@_ui) - @_ui = @env.ui.dup - @_ui.resource = @name - @_ui - end - - def run_action(name, options=nil) - options = { - :vm => self, - :ui => ui - }.merge(options || {}) - - env.action_runner.run(name, options) - end - end -end From cc7768c535beb9e37ec294521ae4a917c9e5abf9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 18 Aug 2012 20:06:50 -0700 Subject: [PATCH 58/60] Trivial whitespace changes --- lib/vagrant/machine.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 0f8e8671c..2c489370c 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -47,15 +47,15 @@ module Vagrant # @param [Environment] env The environment that this machine is a # part of. def initialize(name, provider_cls, config, box, env) - @logger = Log4r::Logger.new("vagrant::machine") + @logger = Log4r::Logger.new("vagrant::machine") @logger.info("Initializing machine: #{name}") @logger.info(" - Provider: #{provider_cls}") @logger.info(" - Box: #{box}") - @name = name - @box = box - @config = config - @env = env + @name = name + @box = box + @config = config + @env = env # Read the ID, which is usually in local storage @id = nil From ba0e426507ea600875ee8346cb2473d857b134a3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Aug 2012 18:51:36 -0700 Subject: [PATCH 59/60] Get vagrant package --base working in some hacky way. `vagrant package --base` is deprecated for a future feature so I didn't want to waste any brain cycles on how to do this the "right" way since a new system will be introduced to do this sort of thing in teh future. --- lib/vagrant/machine.rb | 10 ++++++++-- plugins/commands/package/command.rb | 13 +++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 2c489370c..6dd8c95b7 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -46,7 +46,7 @@ module Vagrant # @param [Box] box The box that is backing this virtual machine. # @param [Environment] env The environment that this machine is a # part of. - def initialize(name, provider_cls, config, box, env) + def initialize(name, provider_cls, config, box, env, base=false) @logger = Log4r::Logger.new("vagrant::machine") @logger.info("Initializing machine: #{name}") @logger.info(" - Provider: #{provider_cls}") @@ -59,7 +59,13 @@ module Vagrant # Read the ID, which is usually in local storage @id = nil - @id = @env.local_data[:active][@name.to_s] if @env.local_data[:active] + + # XXX: This is temporary. This will be removed very soon. + if base + @id = name + else + @id = @env.local_data[:active][@name.to_s] if @env.local_data[:active] + end # Initializes the provider last so that it has access to all the # state we setup on this machine. diff --git a/plugins/commands/package/command.rb b/plugins/commands/package/command.rb index e1f14721a..02338aa9d 100644 --- a/plugins/commands/package/command.rb +++ b/plugins/commands/package/command.rb @@ -47,8 +47,17 @@ module VagrantPlugins protected def package_base(options) - vm = Vagrant::VM.new(options[:base], @env, @env.config.global, :base => true) - raise Vagrant::Errors::BaseVMNotFound, :name => options[:base] if !vm.created? + # XXX: This whole thing is hardcoded and very temporary. The whole + # `vagrant package --base` process is deprecated for something much + # better in the future. We just hardcode this to keep VirtualBox working + # for now. + provider = nil + Vagrant.plugin("1").registered.each do |plugin| + provider = plugin.provider.get(:virtualbox) + break if provider + end + + vm = Vagrant::Machine.new(options[:base], provider, @env.config.global, nil, @env, true) @logger.debug("Packaging base VM: #{vm.name}") package_vm(vm, options) end From 1cdd69bc9178287b762c2b68d586035965fa568c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Aug 2012 19:17:19 -0700 Subject: [PATCH 60/60] All tests passing --- test/unit/vagrant/environment_test.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index 4a79ced92..9a7c227af 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -8,8 +8,18 @@ require "support/tempdir" describe Vagrant::Environment do include_context "unit" - let(:home_path) { Pathname.new(Tempdir.new.path) } - let(:instance) { described_class.new(:home_path => home_path) } + let(:env) do + isolated_environment.tap do |e| + e.box2("base", :virtualbox) + e.vagrantfile <<-VF + Vagrant.configure("1") do |config| + config.vm.box = "base" + end + VF + end + end + + let(:instance) { env.create_vagrant_env } describe "current working directory" do it "is the cwd by default" do @@ -102,6 +112,7 @@ describe Vagrant::Environment do environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant::Config.run do |config| + config.vm.box = "base" config.vm.define :foo config.vm.define :bar, :primary => true end