Merge branch 'pluggable-synced-folder'

This enables synced folder implementations to be completely standalone
plugins and simplifies the way synced folders are enabled for providers
by providing a standard `SyncedFolder` middleware that does everything.
This is all much better tested than previously.

The built-in synced folder types are now:

* "virtualbox" - VirtualBox shared folders

* "nfs" - NFS shared folders

These can now easily be extended
This commit is contained in:
Mitchell Hashimoto 2013-11-23 10:51:34 -08:00
commit 14faccfd79
28 changed files with 752 additions and 287 deletions

View File

@ -101,6 +101,7 @@ module Vagrant
c.register([:"2", :host]) { Plugin::V2::Host } c.register([:"2", :host]) { Plugin::V2::Host }
c.register([:"2", :provider]) { Plugin::V2::Provider } c.register([:"2", :provider]) { Plugin::V2::Provider }
c.register([:"2", :provisioner]) { Plugin::V2::Provisioner } c.register([:"2", :provisioner]) { Plugin::V2::Provisioner }
c.register([:"2", :synced_folder]) { Plugin::V2::SyncedFolder }
end end
# This returns a true/false showing whether we're running from the # This returns a true/false showing whether we're running from the

View File

@ -18,12 +18,12 @@ module Vagrant
autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url"
autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions" autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions"
autoload :Lock, "vagrant/action/builtin/lock" autoload :Lock, "vagrant/action/builtin/lock"
autoload :NFS, "vagrant/action/builtin/nfs"
autoload :Provision, "vagrant/action/builtin/provision" autoload :Provision, "vagrant/action/builtin/provision"
autoload :ProvisionerCleanup, "vagrant/action/builtin/provisioner_cleanup" autoload :ProvisionerCleanup, "vagrant/action/builtin/provisioner_cleanup"
autoload :SetHostname, "vagrant/action/builtin/set_hostname" autoload :SetHostname, "vagrant/action/builtin/set_hostname"
autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHExec, "vagrant/action/builtin/ssh_exec"
autoload :SSHRun, "vagrant/action/builtin/ssh_run" autoload :SSHRun, "vagrant/action/builtin/ssh_run"
autoload :SyncedFolders, "vagrant/action/builtin/synced_folders"
autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator" autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator"
end end

View File

@ -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

View File

@ -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

View File

@ -364,6 +364,10 @@ module Vagrant
error_key(:nfs_no_hostonly_network) error_key(:nfs_no_hostonly_network)
end end
class NoDefaultSyncedFolderImpl < VagrantError
error_key(:no_default_synced_folder_impl)
end
class NoEnvironmentError < VagrantError class NoEnvironmentError < VagrantError
error_key(:no_env) error_key(:no_env)
end end
@ -508,6 +512,10 @@ module Vagrant
error_key(:ssh_unavailable_windows) error_key(:ssh_unavailable_windows)
end end
class SyncedFolderUnusable < VagrantError
error_key(:synced_folder_unusable)
end
class UIExpectsTTY < VagrantError class UIExpectsTTY < VagrantError
error_key(:ui_expects_tty) error_key(:ui_expects_tty)
end end

View File

@ -17,6 +17,7 @@ module Vagrant
autoload :Plugin, "vagrant/plugin/v2/plugin" autoload :Plugin, "vagrant/plugin/v2/plugin"
autoload :Provider, "vagrant/plugin/v2/provider" autoload :Provider, "vagrant/plugin/v2/provider"
autoload :Provisioner, "vagrant/plugin/v2/provisioner" autoload :Provisioner, "vagrant/plugin/v2/provisioner"
autoload :SyncedFolder, "vagrant/plugin/v2/synced_folder"
end end
end end
end end

View File

@ -32,6 +32,11 @@ module Vagrant
# @return [Hash<Symbol, Registry>] # @return [Hash<Symbol, Registry>]
attr_reader :providers attr_reader :providers
# This contains all the synced folder implementations by name.
#
# @return [Registry<Symbol, Array<Class, Integer>>]
attr_reader :synced_folders
def initialize def initialize
# The action hooks hash defaults to [] # The action hooks hash defaults to []
@action_hooks = Hash.new { |h, k| h[k] = [] } @action_hooks = Hash.new { |h, k| h[k] = [] }
@ -40,6 +45,7 @@ module Vagrant
@guests = Registry.new @guests = Registry.new
@guest_capabilities = Hash.new { |h, k| h[k] = Registry.new } @guest_capabilities = Hash.new { |h, k| h[k] = Registry.new }
@providers = Registry.new @providers = Registry.new
@synced_folders = Registry.new
end end
end end
end end

View File

@ -142,6 +142,17 @@ module Vagrant
end end
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 # This registers a plugin. This should _NEVER_ be called by the public
# and should only be called from within Vagrant. Vagrant will # and should only be called from within Vagrant. Vagrant will
# automatically register V2 plugins when a name is set on the # automatically register V2 plugins when a name is set on the

View File

@ -194,6 +194,19 @@ module Vagrant
data[:provisioners] data[:provisioners]
end 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 # Returns the internal data associated with this plugin. This
# should NOT be called by the general public. # should NOT be called by the general public.
# #

View File

@ -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

View File

@ -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

View File

@ -294,9 +294,16 @@ module VagrantPlugins
end end
@__synced_folders.each do |id, options| @__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 # Ignore NFS on Windows
if options[:nfs] && Vagrant::Util::Platform.windows? if options[:type] == :nfs && Vagrant::Util::Platform.windows?
options[:nfs] = false options.delete(:type)
end end
end end
@ -381,7 +388,7 @@ module VagrantPlugins
:path => options[:hostpath]) :path => options[:hostpath])
end end
if options[:nfs] if options[:type] == :nfs
has_nfs = true has_nfs = true
if options[:owner] || options[:group] if options[:owner] || options[:group]

View File

@ -20,11 +20,6 @@ module VagrantPlugins
SSHConfig SSHConfig
end end
config("nfs") do
require File.expand_path("../config/nfs", __FILE__)
NFSConfig
end
config("package") do config("package") do
require File.expand_path("../config/package", __FILE__) require File.expand_path("../config/package", __FILE__)
PackageConfig PackageConfig

View File

@ -40,7 +40,6 @@ module VagrantPlugins
autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__)
autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :SetName, File.expand_path("../action/set_name", __FILE__)
autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __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__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__)
# Include the built-in modules so that we can use them as top-level # 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 PrepareForwardedPortCollisionParams
b.use HandleForwardedPortCollisions b.use HandleForwardedPortCollisions
b.use PruneNFSExports b.use PruneNFSExports
b.use NFS
b.use PrepareNFSSettings
b.use ClearSharedFolders b.use ClearSharedFolders
b.use ShareFolders b.use SyncedFolders
b.use PrepareNFSSettings
b.use ClearNetworkInterfaces b.use ClearNetworkInterfaces
b.use Network b.use Network
b.use ForwardPorts b.use ForwardPorts

View File

@ -12,7 +12,7 @@ module VagrantPlugins
using_nfs = false using_nfs = false
env[:machine].config.vm.synced_folders.each do |id, opts| env[:machine].config.vm.synced_folders.each do |id, opts|
if opts[:nfs] if opts[:type] == :nfs
using_nfs = true using_nfs = true
break break
end end

View File

@ -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

View File

@ -18,6 +18,11 @@ module VagrantPlugins
require File.expand_path("../config", __FILE__) require File.expand_path("../config", __FILE__)
Config Config
end end
synced_folder(:virtualbox) do
require File.expand_path("../synced_folder", __FILE__)
SyncedFolder
end
end end
autoload :Action, File.expand_path("../action", __FILE__) autoload :Action, File.expand_path("../action", __FILE__)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -387,6 +387,14 @@ en:
NFS requires a host-only network with a static IP to be created. 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 Please add a host-only network with a static IP to the machine
for NFS to work. 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: |- no_env: |-
A Vagrant environment is required to run this command. Run `vagrant init` 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 to set one up in this directory, or change to a directory with a
@ -572,6 +580,10 @@ en:
Port: %{port} Port: %{port}
Username: %{username} Username: %{username}
Private key: %{key_path} 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" test_key: "test value"
ui_expects_tty: |- ui_expects_tty: |-
Vagrant is attempting to interface with the UI in a way that requires Vagrant is attempting to interface with the UI in a way that requires

View File

@ -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

View File

@ -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

View File

@ -3,15 +3,19 @@ require File.expand_path("../../../../base", __FILE__)
require "vagrant/registry" require "vagrant/registry"
describe Vagrant::Plugin::V2::Components do 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 describe "configs" do
it "should have configs" do it "should have configs" do
instance.configs.should be_kind_of(Hash) subject.configs.should be_kind_of(Hash)
end end
it "should default the values to registries" do 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 end
end end

View File

@ -171,4 +171,21 @@ describe Vagrant::Plugin::V2::Manager do
instance.provider_configs[:foo].should == "foo" instance.provider_configs[:foo].should == "foo"
instance.provider_configs[:bar].should == "bar" instance.provider_configs[:bar].should == "bar"
end 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 end

View File

@ -278,6 +278,42 @@ describe Vagrant::Plugin::V2::Plugin do
end end
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 describe "plugin registration" do
let(:manager) { described_class.manager } let(:manager) { described_class.manager }

View File

@ -24,6 +24,23 @@ describe Vagrant do
end end
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 it "raises an exception if an unsupported version is given" do
expect { described_class.plugin("88") }. expect { described_class.plugin("88") }.
to raise_error(ArgumentError) to raise_error(ArgumentError)