diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 9dfdac559..ee2b61a20 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -101,6 +101,7 @@ module Vagrant c.register([:"2", :host]) { Plugin::V2::Host } c.register([:"2", :provider]) { Plugin::V2::Provider } c.register([:"2", :provisioner]) { Plugin::V2::Provisioner } + c.register([:"2", :synced_folder]) { Plugin::V2::SyncedFolder } end # This returns a true/false showing whether we're running from the diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 52164b462..eec35987f 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -18,12 +18,12 @@ module Vagrant autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions" autoload :Lock, "vagrant/action/builtin/lock" - autoload :NFS, "vagrant/action/builtin/nfs" autoload :Provision, "vagrant/action/builtin/provision" autoload :ProvisionerCleanup, "vagrant/action/builtin/provisioner_cleanup" 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/nfs.rb b/lib/vagrant/action/builtin/nfs.rb deleted file mode 100644 index 3c77f6bc3..000000000 --- a/lib/vagrant/action/builtin/nfs.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'fileutils' -require 'pathname' -require 'zlib' - -require "log4r" - -require 'vagrant/util/platform' - -module Vagrant - module Action - module Builtin - # This built-in middleware exports and mounts NFS shared folders. - # - # To use this middleware, two configuration parameters must be given - # via the environment hash: - # - # - `:nfs_host_ip` - The IP of where to mount the NFS folder from. - # - `:nfs_machine_ip` - The IP of the machine where the NFS folder - # will be mounted. - # - class NFS - def initialize(app, env) - @app = app - @logger = Log4r::Logger.new("vagrant::action::builtin::nfs") - end - - def call(env) - # We forward things along first. We do everything on the tail - # end of the middleware call. - @app.call(env) - - # Used by prepare_permission, so we need to save it - @env = env - - folders = {} - env[:machine].config.vm.synced_folders.each do |id, opts| - # If this synced folder doesn't enable NFS, ignore it. - next if !opts[:nfs] - - # Expand the host path, create it if we have to and - # store away the folder. - 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 - - # Determine the real path by expanding symlinks and getting - # proper casing. We have to do this after creating it - # if it doesn't exist. - hostpath = hostpath.realpath - hostpath = Util::Platform.fs_real_path(hostpath) - - # Set the hostpath back on the options and save it - opts[:hostpath] = hostpath.to_s - folders[id] = opts - end - - if !folders.empty? - raise Errors::NFSNoHostIP if !env[:nfs_host_ip] - raise Errors::NFSNoGuestIP if !env[:nfs_machine_ip] - - machine_ip = env[:nfs_machine_ip] - machine_ip = [machine_ip] if !machine_ip.is_a?(Array) - - # Prepare the folder, this means setting up various options - # and such on the folder itself. - folders.each { |id, opts| prepare_folder(opts) } - - # Export the folders - env[:ui].info I18n.t("vagrant.actions.vm.nfs.exporting") - env[:host].nfs_export(env[:machine].id, machine_ip, folders) - - # Mount - env[:ui].info I18n.t("vagrant.actions.vm.nfs.mounting") - - # Only mount folders that have a guest path specified. - mount_folders = {} - folders.each do |id, opts| - mount_folders[id] = opts.dup if opts[:guestpath] - end - - # Mount them! - env[:machine].guest.capability( - :mount_nfs_folder, env[:nfs_host_ip], mount_folders) - end - end - - protected - - def prepare_folder(opts) - opts[:map_uid] = prepare_permission(:uid, opts) - opts[:map_gid] = prepare_permission(:gid, opts) - opts[:nfs_version] ||= 3 - - # We use a CRC32 to generate a 32-bit checksum so that the - # fsid is compatible with both old and new kernels. - opts[:uuid] = Zlib.crc32(opts[:hostpath]).to_s - 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 - end - end - end -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..0d0eb9356 --- /dev/null +++ b/lib/vagrant/action/builtin/synced_folders.rb @@ -0,0 +1,165 @@ +require "log4r" + +require 'vagrant/util/platform' + +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]) + + # Create the hostpath if it doesn't exist and we've been told to + 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 + + if File.directory?(data[:hostpath]) + data[:hostpath] = File.realpath(data[:hostpath]) + data[:hostpath] = Util::Platform.fs_real_path(data[:hostpath]).to_s + end + end + end + + # Go through each folder and prepare the folders + folders.each do |impl_name, fs| + @logger.info("Invoking synced folder prepare for: #{impl_name}") + plugins[impl_name.to_sym][0].new.prepare(env[:machine], fs, impl_opts(impl_name, env)) + end + + # Continue, we need the VM to be booted. + @app.call(env) + + # Once booted, setup the folder contents + folders.each do |impl_name, fs| + @logger.info("Invoking synced folder enable: #{impl_name}") + plugins[impl_name.to_sym][0].new.enable(env[:machine], fs, impl_opts(impl_name, env)) + 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, key, impl] + end + + # Order the plugins by priority + ordered = ordered.sort { |a, b| b[0] <=> a[0] } + + # Find the proper implementation + ordered.each do |_, key, impl| + return key if impl.new.usable?(machine) + end + + return nil + end + + # This finds the options in the env that are set for a given + # synced folder type. + def impl_opts(name, env) + {}.tap do |result| + env.each do |k, v| + if k.to_s.start_with?("#{name}_") + k = k.dup if !k.is_a?(Symbol) + v = v.dup if !v.is_a?(Symbol) + result[k] = v + end + end + end + 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 = "" + impl = data[:type].to_sym if data[:type] + + if impl != "" + impl_class = plugins[impl] + if !impl_class + # 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 + + if !impl_class[0].new.usable?(machine) + # Verify that explicitly defined shared folder types are + # actually usable. + raise Errors::SyncedFolderUnusable, type: data[:type].to_s + end + end + + # 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..6a8cd7466 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 @@ -508,6 +512,10 @@ module Vagrant error_key(:ssh_unavailable_windows) end + class SyncedFolderUnusable < VagrantError + error_key(:synced_folder_unusable) + end + class UIExpectsTTY < VagrantError error_key(:ui_expects_tty) end diff --git a/lib/vagrant/plugin/v2.rb b/lib/vagrant/plugin/v2.rb index 91803e648..1539667bd 100644 --- a/lib/vagrant/plugin/v2.rb +++ b/lib/vagrant/plugin/v2.rb @@ -17,6 +17,7 @@ module Vagrant autoload :Plugin, "vagrant/plugin/v2/plugin" autoload :Provider, "vagrant/plugin/v2/provider" autoload :Provisioner, "vagrant/plugin/v2/provisioner" + autoload :SyncedFolder, "vagrant/plugin/v2/synced_folder" end end end diff --git a/lib/vagrant/plugin/v2/components.rb b/lib/vagrant/plugin/v2/components.rb index 6b585d071..f66fdce43 100644 --- a/lib/vagrant/plugin/v2/components.rb +++ b/lib/vagrant/plugin/v2/components.rb @@ -32,6 +32,11 @@ module Vagrant # @return [Hash] attr_reader :providers + # This contains all the synced folder implementations by name. + # + # @return [Registry>] + attr_reader :synced_folders + def initialize # The action hooks hash defaults to [] @action_hooks = Hash.new { |h, k| h[k] = [] } @@ -40,6 +45,7 @@ module Vagrant @guests = Registry.new @guest_capabilities = Hash.new { |h, k| h[k] = Registry.new } @providers = Registry.new + @synced_folders = Registry.new end end end 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/lib/vagrant/plugin/v2/plugin.rb b/lib/vagrant/plugin/v2/plugin.rb index 0acc7614f..6ffb860a1 100644 --- a/lib/vagrant/plugin/v2/plugin.rb +++ b/lib/vagrant/plugin/v2/plugin.rb @@ -194,6 +194,19 @@ module Vagrant data[:provisioners] end + # Registers additional synced folder implementations. + # + # @param [String] name Name of the implementation. + # @param [Integer] priority The priority of the implementation, + # higher (big) numbers are tried before lower (small) numbers. + def self.synced_folder(name, priority=10, &block) + components.synced_folders.register(name.to_sym) do + [block.call, priority] + end + + nil + end + # Returns the internal data associated with this plugin. This # should NOT be called by the general public. # diff --git a/lib/vagrant/plugin/v2/synced_folder.rb b/lib/vagrant/plugin/v2/synced_folder.rb new file mode 100644 index 000000000..6774882c8 --- /dev/null +++ b/lib/vagrant/plugin/v2/synced_folder.rb @@ -0,0 +1,17 @@ +module Vagrant + module Plugin + module V2 + # This is the base class for a synced folder implementation. + class SyncedFolder + def usable?(machine) + end + + def prepare(machine, folders, opts) + end + + def enable(machine, folders, opts) + end + end + end + end +end diff --git a/plugins/kernel_v2/config/nfs.rb b/plugins/kernel_v2/config/nfs.rb deleted file mode 100644 index 21e7ea7bf..000000000 --- a/plugins/kernel_v2/config/nfs.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "vagrant" - -module VagrantPlugins - module Kernel_V2 - class NFSConfig < Vagrant.plugin("2", :config) - attr_accessor :map_uid - attr_accessor :map_gid - - def to_s - "NFS" - end - end - end -end diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 3be717eb0..845e5e191 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -294,9 +294,16 @@ module VagrantPlugins end @__synced_folders.each do |id, options| + if options[:nfs] + options[:type] = :nfs + end + + # Make sure the type is a symbol + options[:type] = options[:type].to_sym if options[:type] + # Ignore NFS on Windows - if options[:nfs] && Vagrant::Util::Platform.windows? - options[:nfs] = false + if options[:type] == :nfs && Vagrant::Util::Platform.windows? + options.delete(:type) end end @@ -381,7 +388,7 @@ module VagrantPlugins :path => options[:hostpath]) end - if options[:nfs] + if options[:type] == :nfs has_nfs = true if options[:owner] || options[:group] diff --git a/plugins/kernel_v2/plugin.rb b/plugins/kernel_v2/plugin.rb index 55cfcc0dc..0904481df 100644 --- a/plugins/kernel_v2/plugin.rb +++ b/plugins/kernel_v2/plugin.rb @@ -20,11 +20,6 @@ module VagrantPlugins SSHConfig end - config("nfs") do - require File.expand_path("../config/nfs", __FILE__) - NFSConfig - end - config("package") do require File.expand_path("../config/package", __FILE__) PackageConfig diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 5fd0f91a5..6ea902976 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -40,7 +40,6 @@ module VagrantPlugins autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) autoload :SetName, File.expand_path("../action/set_name", __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__) # Include the built-in modules so that we can use them as top-level @@ -60,10 +59,9 @@ module VagrantPlugins b.use PrepareForwardedPortCollisionParams b.use HandleForwardedPortCollisions b.use PruneNFSExports - b.use NFS - b.use PrepareNFSSettings b.use ClearSharedFolders - b.use ShareFolders + b.use SyncedFolders + b.use PrepareNFSSettings b.use ClearNetworkInterfaces b.use Network b.use ForwardPorts diff --git a/plugins/providers/virtualbox/action/prepare_nfs_settings.rb b/plugins/providers/virtualbox/action/prepare_nfs_settings.rb index 0aba7fdf0..03755707e 100644 --- a/plugins/providers/virtualbox/action/prepare_nfs_settings.rb +++ b/plugins/providers/virtualbox/action/prepare_nfs_settings.rb @@ -12,7 +12,7 @@ module VagrantPlugins using_nfs = false env[:machine].config.vm.synced_folders.each do |id, opts| - if opts[:nfs] + if opts[:type] == :nfs using_nfs = true break end diff --git a/plugins/providers/virtualbox/action/share_folders.rb b/plugins/providers/virtualbox/action/share_folders.rb deleted file mode 100644 index 9373716d1..000000000 --- a/plugins/providers/virtualbox/action/share_folders.rb +++ /dev/null @@ -1,128 +0,0 @@ -require "pathname" - -require "log4r" - -require "vagrant/util/platform" -require "vagrant/util/scoped_hash_override" - -module VagrantPlugins - module ProviderVirtualBox - module Action - class ShareFolders - include Vagrant::Util::ScopedHashOverride - - 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 - {}.tap do |result| - @env[:machine].config.vm.synced_folders.each do |id, data| - data = scoped_hash_override(data, :virtualbox) - - # Ignore NFS shared folders - next if data[:nfs] - - # Ignore disabled shared folders - next if data[:disabled] - - # This to prevent overwriting the actual shared folders data - result[id] = data.dup - end - end - end - - # Prepares the shared folders by verifying they exist and creating them - # if they don't. - def prepare_folders - shared_folders.each do |id, 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 |id, data| - hostpath = File.expand_path(data[:hostpath], @env[:root_path]) - hostpath = Vagrant::Util::Platform.cygwin_windows_path(hostpath) - - folders << { - :name => id, - :hostpath => hostpath, - :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 |id, 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 |id, 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", - :guest_path => data[:guestpath])) - - # Dup the data so we can pass it to the guest API - data = data.dup - - # Calculate the owner and group - ssh_info = @env[:machine].ssh_info - data[:owner] ||= ssh_info[:username] - data[:group] ||= ssh_info[:username] - - # Mount the actual folder - @env[:machine].guest.capability( - :mount_virtualbox_shared_folder, id, 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", - :host_path => data[:hostpath])) - end - end - end - end - end - end -end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index 8349c5cb1..4b9146f9e 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -18,6 +18,11 @@ module VagrantPlugins require File.expand_path("../config", __FILE__) Config end + + synced_folder(:virtualbox) do + require File.expand_path("../synced_folder", __FILE__) + SyncedFolder + end end autoload :Action, File.expand_path("../action", __FILE__) diff --git a/plugins/providers/virtualbox/synced_folder.rb b/plugins/providers/virtualbox/synced_folder.rb new file mode 100644 index 000000000..85feb1618 --- /dev/null +++ b/plugins/providers/virtualbox/synced_folder.rb @@ -0,0 +1,72 @@ +require "vagrant/util/platform" + +module VagrantPlugins + module ProviderVirtualBox + class SyncedFolder < Vagrant.plugin("2", :synced_folder) + def usable?(machine) + # These synced folders only work if the provider if VirtualBox + machine.provider_name == :virtualbox + end + + def prepare(machine, folders, _opts) + defs = [] + folders.each do |id, data| + hostpath = Vagrant::Util::Platform.cygwin_windows_path(data[:hostpath]) + + defs << { + name: id, + hostpath: hostpath.to_s, + transient: data[:transient], + } + end + + driver(machine).share_folders(defs) + end + + def enable(machine, folders, _opts) + # short guestpaths first, so we don't step on ourselves + folders = folders.sort_by do |id, 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 + machine.ui.info(I18n.t("vagrant.actions.vm.share_folders.mounting")) + folders.each do |id, data| + if data[:guestpath] + # Guest path specified, so mount the folder to specified point + machine.ui.info(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", + :guest_path => data[:guestpath])) + + # Dup the data so we can pass it to the guest API + data = data.dup + + # Calculate the owner and group + ssh_info = machine.ssh_info + data[:owner] ||= ssh_info[:username] + data[:group] ||= ssh_info[:username] + + # Mount the actual folder + machine.guest.capability( + :mount_virtualbox_shared_folder, id, data[:guestpath], data) + else + # If no guest path is specified, then automounting is disabled + machine.ui.info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", + :host_path => data[:hostpath])) + end + end + end + + protected + + # This is here so that we can stub it for tests + def driver(machine) + machine.provider.driver + end + end + end +end diff --git a/plugins/synced_folders/nfs/config.rb b/plugins/synced_folders/nfs/config.rb new file mode 100644 index 000000000..d842f78b1 --- /dev/null +++ b/plugins/synced_folders/nfs/config.rb @@ -0,0 +1,26 @@ +require "vagrant" + +module VagrantPlugins + module SyncedFolderNFS + class Config < Vagrant.plugin("2", :config) + attr_accessor :map_uid + attr_accessor :map_gid + + def initialize + super + + @map_uid = UNSET_VALUE + @map_gid = UNSET_VALUE + end + + def finalize! + @map_uid = nil if @map_uid == UNSET_VALUE + @map_gid = nil if @map_gid == UNSET_VALUE + end + + def to_s + "NFS" + end + end + end +end diff --git a/plugins/synced_folders/nfs/plugin.rb b/plugins/synced_folders/nfs/plugin.rb new file mode 100644 index 000000000..b6f1fa491 --- /dev/null +++ b/plugins/synced_folders/nfs/plugin.rb @@ -0,0 +1,23 @@ +require "vagrant" + +module VagrantPlugins + module SyncedFolderNFS + class Plugin < Vagrant.plugin("2") + name "NFS synced folders" + description <<-EOF + The NFS synced folders plugin enables you to use NFS as a synced folder + implementation. + EOF + + config("nfs") do + require File.expand_path("../config", __FILE__) + Config + end + + synced_folder("nfs", 5) do + require File.expand_path("../synced_folder", __FILE__) + SyncedFolder + end + end + end +end diff --git a/plugins/synced_folders/nfs/synced_folder.rb b/plugins/synced_folders/nfs/synced_folder.rb new file mode 100644 index 000000000..db9aa13fe --- /dev/null +++ b/plugins/synced_folders/nfs/synced_folder.rb @@ -0,0 +1,83 @@ +require 'fileutils' +require 'zlib' + +require "vagrant/util/platform" + +module VagrantPlugins + module SyncedFolderNFS + # This synced folder requires that two keys be set on the environment + # within the middleware sequence: + # + # - `:nfs_host_ip` - The IP of where to mount the NFS folder from. + # - `:nfs_machine_ip` - The IP of the machine where the NFS folder + # will be mounted. + # + class SyncedFolder < Vagrant.plugin("2", :synced_folder) + def usable?(machine) + # NFS is always available + true + end + + def prepare(machine, folders, opts) + # Nothing is necessary to do before VM boot. + end + + def enable(machine, folders, nfsopts) + raise Vagrant::Errors::NFSNoHostIP if !nfsopts[:nfs_host_ip] + raise Vagrant::Errors::NFSNoGuestIP if !nfsopts[:nfs_machine_ip] + + machine_ip = nfsopts[:nfs_machine_ip] + machine_ip = [machine_ip] if !machine_ip.is_a?(Array) + + # Prepare the folder, this means setting up various options + # and such on the folder itself. + folders.each { |id, opts| prepare_folder(machine, opts) } + + # Export the folders + machine.ui.info I18n.t("vagrant.actions.vm.nfs.exporting") + machine.env.host.nfs_export(machine.id, machine_ip, folders) + + # Mount + machine.ui.info I18n.t("vagrant.actions.vm.nfs.mounting") + + # Only mount folders that have a guest path specified. + mount_folders = {} + folders.each do |id, opts| + mount_folders[id] = opts.dup if opts[:guestpath] + end + + # Mount them! + machine.guest.capability( + :mount_nfs_folder, nfsopts[:nfs_host_ip], mount_folders) + end + + protected + + def prepare_folder(machine, opts) + opts[:map_uid] = prepare_permission(machine, :uid, opts) + opts[:map_gid] = prepare_permission(machine, :gid, opts) + opts[:nfs_version] ||= 3 + + # We use a CRC32 to generate a 32-bit checksum so that the + # fsid is compatible with both old and new kernels. + opts[:uuid] = Zlib.crc32(opts[:hostpath]).to_s + end + + # Prepares the UID/GID settings for a single folder. + def prepare_permission(machine, 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] : 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 + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 762218255..0b15212d5 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 @@ -572,6 +580,10 @@ en: Port: %{port} Username: %{username} Private key: %{key_path} + synced_folder_unusable: | + The synced folder type '%{type}' is reporting as unusable for + your current setup. Please verify you have all the proper + prerequisites for using this shared folder type and try again. test_key: "test value" ui_expects_tty: |- Vagrant is attempting to interface with the UI in a way that requires diff --git a/test/unit/plugins/providers/virtualbox/synced_folder_test.rb b/test/unit/plugins/providers/virtualbox/synced_folder_test.rb new file mode 100644 index 000000000..20a3fcfd8 --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/synced_folder_test.rb @@ -0,0 +1,38 @@ +require "vagrant" +require Vagrant.source_root.join("test/unit/base") + +require Vagrant.source_root.join("plugins/providers/virtualbox/synced_folder") + +# TODO(mitchellh): tag with v2 +describe VagrantPlugins::ProviderVirtualBox::SyncedFolder do + let(:machine) do + double("machine").tap do |m| + end + end + + subject { described_class.new } + + describe "usable" do + it "should be with virtualbox provider" do + machine.stub(provider_name: :virtualbox) + subject.should be_usable(machine) + end + + it "should not be with another provider" do + machine.stub(provider_name: :vmware_fusion) + subject.should_not be_usable(machine) + end + end + + describe "prepare" do + let(:driver) { double("driver") } + + before do + machine.stub(driver: driver) + end + + it "should share the folders" do + pending + end + end +end 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..2cb123096 --- /dev/null +++ b/test/unit/vagrant/action/builtin/synced_folders_test.rb @@ -0,0 +1,180 @@ +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) { {} } + let(:plugins) { {} } + + before do + plugins[:default] = [impl(true, "default"), 10] + plugins[:nfs] = [impl(true, "nfs"), 5] + + env[:root_path] = Pathname.new(Dir.mktmpdir) + subject.stub(:plugins => plugins) + subject.stub(:synced_folders => synced_folders) + end + + it "should create on the host if specified" do + synced_folders["default"] = { + "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 = [] + tracker = Class.new(impl(true, "good")) do + define_method(:prepare) do |machine, folders, opts| + order << :prepare + end + + define_method(:enable) do |machine, folders, opts| + order << :enable + end + end + + plugins[:tracker] = [tracker, 15] + + synced_folders["tracker"] = { + "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.should == "good" + end + end + + describe "impl_opts" do + it "should return only relevant keys" do + env = { + :foo_bar => "baz", + :bar_bar => "nope", + :foo_baz => "bar", + } + + result = subject.impl_opts("foo", env) + result.length.should == 2 + result[:foo_bar].should == "baz" + result[:foo_baz].should == "bar" + 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[:default].should == { "root" => folders["root"] } + result[:nfs].should == { "nfs" => folders["nfs"] } + end + + it "should error if an explicit type is unusable" do + plugins[:unusable] = [impl(false, "bad"), 15] + folders["root"] = { type: "unusable" } + + expect { subject.synced_folders(machine) }. + to raise_error + end + + it "should ignore disabled folders" do + folders["root"] = {} + folders["foo"] = { disabled: true } + + result = subject.synced_folders(machine) + result.length.should == 1 + result[:default].length.should == 1 + end + end +end diff --git a/test/unit/vagrant/plugin/v2/components_test.rb b/test/unit/vagrant/plugin/v2/components_test.rb index bf98ca71b..b8dd1837c 100644 --- a/test/unit/vagrant/plugin/v2/components_test.rb +++ b/test/unit/vagrant/plugin/v2/components_test.rb @@ -3,15 +3,19 @@ require File.expand_path("../../../../base", __FILE__) require "vagrant/registry" describe Vagrant::Plugin::V2::Components do - let(:instance) { described_class.new } + subject { described_class.new } + + it "should have synced folders" do + subject.synced_folders.should be_kind_of(Vagrant::Registry) + end describe "configs" do it "should have configs" do - instance.configs.should be_kind_of(Hash) + subject.configs.should be_kind_of(Hash) end it "should default the values to registries" do - instance.configs[:i_probably_dont_exist].should be_kind_of(Vagrant::Registry) + subject.configs[:i_probably_dont_exist].should be_kind_of(Vagrant::Registry) 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 diff --git a/test/unit/vagrant/plugin/v2/plugin_test.rb b/test/unit/vagrant/plugin/v2/plugin_test.rb index 0295b3242..71597988c 100644 --- a/test/unit/vagrant/plugin/v2/plugin_test.rb +++ b/test/unit/vagrant/plugin/v2/plugin_test.rb @@ -278,6 +278,42 @@ describe Vagrant::Plugin::V2::Plugin do end end + describe "synced folders" do + it "should register implementations" do + plugin = Class.new(described_class) do + synced_folder("foo") { "bar" } + end + + plugin.components.synced_folders[:foo].should == ["bar", 10] + end + + it "should be able to specify priorities" do + plugin = Class.new(described_class) do + synced_folder("foo", 50) { "bar" } + end + + plugin.components.synced_folders[:foo].should == ["bar", 50] + end + + it "should lazily register implementations" 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 + synced_folder("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.components.synced_folders[:foo] + }.to raise_error(StandardError) + end + end + describe "plugin registration" do let(:manager) { described_class.manager } diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index 84340f840..8cb0799e5 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -24,6 +24,23 @@ describe Vagrant do end end + describe "v2" do + it "returns the proper class for version 2" do + described_class.plugin("2").should == Vagrant::Plugin::V2::Plugin + end + + it "returns the proper components for version 2" do + described_class.plugin("2", :command).should == Vagrant::Plugin::V2::Command + described_class.plugin("2", :communicator).should == Vagrant::Plugin::V2::Communicator + described_class.plugin("2", :config).should == Vagrant::Plugin::V2::Config + described_class.plugin("2", :guest).should == Vagrant::Plugin::V2::Guest + described_class.plugin("2", :host).should == Vagrant::Plugin::V2::Host + described_class.plugin("2", :provider).should == Vagrant::Plugin::V2::Provider + described_class.plugin("2", :provisioner).should == Vagrant::Plugin::V2::Provisioner + described_class.plugin("2", :synced_folder).should == Vagrant::Plugin::V2::SyncedFolder + end + end + it "raises an exception if an unsupported version is given" do expect { described_class.plugin("88") }. to raise_error(ArgumentError)