diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 52164b462..6a164ac4e 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -24,6 +24,7 @@ module Vagrant autoload :SetHostname, "vagrant/action/builtin/set_hostname" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHRun, "vagrant/action/builtin/ssh_run" + autoload :SyncedFolders, "vagrant/action/builtin/synced_folders" autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator" end diff --git a/lib/vagrant/action/builtin/synced_folders.rb b/lib/vagrant/action/builtin/synced_folders.rb new file mode 100644 index 000000000..47f5b7782 --- /dev/null +++ b/lib/vagrant/action/builtin/synced_folders.rb @@ -0,0 +1,139 @@ +require "log4r" + +module Vagrant + module Action + module Builtin + # This middleware will setup the synced folders for the machine using + # the appropriate synced folder plugin. + class SyncedFolders + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::synced_folders") + end + + def call(env) + folders = synced_folders(env[:machine]) + + # Log all the folders that we have enabled and with what + # implementation... + folders.each do |impl, fs| + @logger.info("Synced Folder Implementation: #{impl}") + fs.each do |id, data| + @logger.info(" - #{id}: #{data[:hostpath]} => #{data[:guestpath]}") + end + end + + # Go through each folder and make sure to create it if + # it does not exist on host + folders.each do |impl, fs| + fs.each do |id, data| + data[:hostpath] = File.expand_path(data[:hostpath], env[:root_path]) + + # Don't do anything else if this directory exists or its + # not flagged to auto-create + next if File.directory?(data[:hostpath]) || !data[:create] + @logger.info("Creating shared folder host directory: #{data[:hostpath]}") + begin + Pathname.new(data[:hostpath]).mkpath + rescue Errno::EACCES + raise Vagrant::Errors::SharedFolderCreateFailed, + path: data[:hostpath] + end + end + end + + # Go through each folder and prepare the folders + folders.each do |impl, fs| + @logger.info("Invoking synced folder prepare for: #{impl}") + impl.new.prepare(env[:machine], fs) + end + + # Continue, we need the VM to be booted. + @app.call(env) + + # Once booted, setup the folder contents + folders.each do |impl, fs| + @logger.info("Invoking synced folder enable: #{impl}") + impl.new.enable(env[:machine], fs) + end + end + + # This goes over all the registered synced folder types and returns + # the highest priority implementation that is usable for this machine. + def default_synced_folder_type(machine, plugins) + ordered = [] + + # First turn the plugins into an array + plugins.each do |key, data| + impl = data[0] + priority = data[1] + + ordered << [priority, impl] + end + + # Order the plugins by priority + ordered = ordered.sort { |a, b| b[0] <=> a[0] }.map { |p| p[1] } + + # Find the proper implementation + ordered.each do |impl| + return impl if impl.new.usable?(machine) + end + + return nil + end + + # This returns the available synced folder implementations. This + # is a separate method so that it can be easily stubbed by tests. + def plugins + @plugins ||= Vagrant.plugin("2").manager.synced_folders + end + + # This returns the set of shared folders that should be done for + # this machine. It returns the folders in a hash keyed by the + # implementation class for the synced folders. + def synced_folders(machine) + folders = {} + + # Determine all the synced folders as well as the implementation + # they're going to use. + machine.config.vm.synced_folders.each do |id, data| + # Ignore disabled synced folders + next if data[:disabled] + + impl = ["", 0] + impl = plugins[data[:type].to_sym] if data[:type] + + if impl == nil + # This should never happen because configuration validation + # should catch this case. But we put this here as an assert + raise "Internal error. Report this as a bug. Invalid: #{data[:type]}" + end + + # The implementation class rather than the priority, since the + # array is [class, priority]. + impl = impl[0] + + # Keep track of this shared folder by the implementation. + folders[impl] ||= {} + folders[impl][id] = data.dup + end + + # If we have folders with the "default" key, then determine the + # most appropriate implementation for this. + if folders.has_key?("") && !folders[""].empty? + default_impl = default_synced_folder_type(machine, plugins) + if !default_impl + types = plugins.to_hash.keys.map { |t| t.to_s }.sort.join(", ") + raise Errors::NoDefaultSyncedFolderImpl, types: types + end + + folders[default_impl] = folders[""] + folders.delete("") + end + + return folders + end + end + end + end +end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 8fe583e0d..625637883 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -364,6 +364,10 @@ module Vagrant error_key(:nfs_no_hostonly_network) end + class NoDefaultSyncedFolderImpl < VagrantError + error_key(:no_default_synced_folder_impl) + end + class NoEnvironmentError < VagrantError error_key(:no_env) end diff --git a/lib/vagrant/plugin/v2/components.rb b/lib/vagrant/plugin/v2/components.rb index 8a62401af..f66fdce43 100644 --- a/lib/vagrant/plugin/v2/components.rb +++ b/lib/vagrant/plugin/v2/components.rb @@ -34,7 +34,7 @@ module Vagrant # This contains all the synced folder implementations by name. # - # @return [Registry] + # @return [Registry>] attr_reader :synced_folders def initialize diff --git a/lib/vagrant/plugin/v2/manager.rb b/lib/vagrant/plugin/v2/manager.rb index b6e79e427..1e71e0359 100644 --- a/lib/vagrant/plugin/v2/manager.rb +++ b/lib/vagrant/plugin/v2/manager.rb @@ -142,6 +142,17 @@ module Vagrant end end + # This returns all synced folder implementations. + # + # @return [Registry] + def synced_folders + Registry.new.tap do |result| + @registered.each do |plugin| + result.merge!(plugin.components.synced_folders) + end + end + end + # This registers a plugin. This should _NEVER_ be called by the public # and should only be called from within Vagrant. Vagrant will # automatically register V2 plugins when a name is set on the diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 762218255..5999ec4ba 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -387,6 +387,14 @@ en: NFS requires a host-only network with a static IP to be created. Please add a host-only network with a static IP to the machine for NFS to work. + no_default_synced_folder_impl: |- + No synced folder implementation is available for your synced folders! + Please consult the documentation to learn why this may be the case. + You may force a synced folder implementation by specifying a "type:" + option for the synced folders. Available synced folder implementations + are listed below. + + %{types} no_env: |- A Vagrant environment is required to run this command. Run `vagrant init` to set one up in this directory, or change to a directory with a diff --git a/test/unit/vagrant/action/builtin/synced_folders_test.rb b/test/unit/vagrant/action/builtin/synced_folders_test.rb new file mode 100644 index 000000000..14e41d26a --- /dev/null +++ b/test/unit/vagrant/action/builtin/synced_folders_test.rb @@ -0,0 +1,150 @@ +require "pathname" +require "tmpdir" + +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::SyncedFolders do + let(:app) { lambda { |env| } } + let(:env) { { :machine => machine, :ui => ui } } + let(:machine) do + double("machine").tap do |machine| + machine.stub(:config).and_return(machine_config) + end + end + + let(:machine_config) do + double("machine_config").tap do |top_config| + top_config.stub(:vm => vm_config) + end + end + + let(:vm_config) { double("machine_vm_config") } + + let(:ui) do + double("ui").tap do |result| + result.stub(:info) + end + end + + subject { described_class.new(app, env) } + + # This creates a synced folder implementation. + def impl(usable, name) + Class.new(Vagrant.plugin("2", :synced_folder)) do + define_method(:name) do + name + end + + define_method(:usable?) do |machine| + usable + end + end + end + + describe "call" do + let(:synced_folders) { {} } + + before do + env[:root_path] = Pathname.new(Dir.mktmpdir) + subject.stub(:synced_folders => synced_folders) + end + + it "should create on the host if specified" do + synced_folders[impl(true, "good")] = { + "root" => { + hostpath: "foo", + }, + + "other" => { + hostpath: "bar", + create: true, + } + } + + subject.call(env) + + env[:root_path].join("foo").should_not be_directory + env[:root_path].join("bar").should be_directory + end + + it "should invoke prepare then enable" do + order = [] + sf = Class.new(impl(true, "good")) do + define_method(:prepare) do |machine, folders| + order << :prepare + end + + define_method(:enable) do |machine, folders| + order << :enable + end + end + + synced_folders[sf] = { + "root" => { + hostpath: "foo", + }, + + "other" => { + hostpath: "bar", + create: true, + } + } + + subject.call(env) + + order.should == [:prepare, :enable] + end + end + + describe "default_synced_folder_type" do + it "returns the usable implementation" do + plugins = { + "bad" => [impl(false, "bad"), 0], + "nope" => [impl(true, "nope"), 1], + "good" => [impl(true, "good"), 5], + } + + result = subject.default_synced_folder_type(machine, plugins) + result.new.name.should == "good" + end + end + + describe "synced_folders" do + let(:folders) { {} } + let(:plugins) { {} } + + before do + plugins[:default] = [impl(true, "default"), 10] + plugins[:nfs] = [impl(true, "nfs"), 5] + + subject.stub(:plugins => plugins) + vm_config.stub(:synced_folders => folders) + end + + it "should raise exception if bad type is given" do + folders["root"] = { type: "bad" } + + expect { subject.synced_folders(machine) }. + to raise_error(StandardError) + end + + it "should return the proper set of folders" do + folders["root"] = {} + folders["nfs"] = { type: "nfs" } + + result = subject.synced_folders(machine) + result.length.should == 2 + result[plugins[:default][0]].should == { "root" => folders["root"] } + result[plugins[:nfs][0]].should == { "nfs" => folders["nfs"] } + end + + it "should ignore disabled folders" do + folders["root"] = {} + folders["foo"] = { disabled: true } + + result = subject.synced_folders(machine) + result.length.should == 1 + result[plugins[:default][0]].length.should == 1 + end + end +end diff --git a/test/unit/vagrant/plugin/v2/manager_test.rb b/test/unit/vagrant/plugin/v2/manager_test.rb index d0ab6fd58..d726f49b4 100644 --- a/test/unit/vagrant/plugin/v2/manager_test.rb +++ b/test/unit/vagrant/plugin/v2/manager_test.rb @@ -171,4 +171,21 @@ describe Vagrant::Plugin::V2::Manager do instance.provider_configs[:foo].should == "foo" instance.provider_configs[:bar].should == "bar" end + + it "should enumerate all registered synced folder implementations" do + pA = plugin do |p| + p.synced_folder("foo") { "bar" } + end + + pB = plugin do |p| + p.synced_folder("bar", 50) { "baz" } + end + + instance.register(pA) + instance.register(pB) + + instance.synced_folders.to_hash.length.should == 2 + instance.synced_folders[:foo].should == ["bar", 10] + instance.synced_folders[:bar].should == ["baz", 50] + end end