From 6afb4326fa4b4387828064daac0367f4ad38abb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Feb 2013 15:34:04 -0800 Subject: [PATCH] Move NFS to a built-in middleware --- lib/vagrant/action.rb | 1 + lib/vagrant/action/builtin/nfs.rb | 117 +++++++++++ lib/vagrant/errors.rb | 12 +- plugins/guests/linux/guest.rb | 12 +- plugins/kernel_v2/config/vm.rb | 22 ++- plugins/providers/virtualbox/action.rb | 3 +- plugins/providers/virtualbox/action/nfs.rb | 185 ------------------ .../virtualbox/action/prepare_nfs_settings.rb | 51 +++++ .../virtualbox/action/share_folders.rb | 3 +- templates/locales/en.yml | 27 +-- 10 files changed, 219 insertions(+), 214 deletions(-) create mode 100644 lib/vagrant/action/builtin/nfs.rb delete mode 100644 plugins/providers/virtualbox/action/nfs.rb create mode 100644 plugins/providers/virtualbox/action/prepare_nfs_settings.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 8de536d67..37473fa57 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -16,6 +16,7 @@ module Vagrant autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt" autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :Lock, "vagrant/action/builtin/lock" + autoload :NFS, "vagrant/action/builtin/nfs" autoload :Provision, "vagrant/action/builtin/provision" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHRun, "vagrant/action/builtin/ssh_run" diff --git a/lib/vagrant/action/builtin/nfs.rb b/lib/vagrant/action/builtin/nfs.rb new file mode 100644 index 000000000..672641084 --- /dev/null +++ b/lib/vagrant/action/builtin/nfs.rb @@ -0,0 +1,117 @@ +require 'digest/md5' +require 'fileutils' +require 'pathname' + +require "log4r" + +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 + + raise Errors::NFSNoHostIP if !env[:nfs_host_ip] + raise Errors::NFSNoGuestIP if !env[:nfs_machine_ip] + + 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 + + # Set the hostpath back on the options and save it + opts[:hostpath] = hostpath.to_s + folders[id] = opts + end + + if !folders.empty? + # 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, env[:nfs_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.mount_nfs(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 + + # 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]) + 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/errors.rb b/lib/vagrant/errors.rb index edc8fa4b1..474c274c1 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -251,16 +251,12 @@ module Vagrant error_key(:not_found, "vagrant.actions.vm.host_only_network") end - class NFSHostRequired < VagrantError - error_key(:host_required, "vagrant.actions.vm.nfs") + class NFSNoGuestIP < VagrantError + error_key(:nfs_no_guest_ip) end - class NFSNotSupported < VagrantError - error_key(:not_supported, "vagrant.actions.vm.nfs") - end - - class NFSNoHostNetwork < VagrantError - error_key(:no_host_network, "vagrant.actions.vm.nfs") + class NFSNoHostIP < VagrantError + error_key(:nfs_no_host_ip) end class NoEnvironmentError < VagrantError diff --git a/plugins/guests/linux/guest.rb b/plugins/guests/linux/guest.rb index cb53875ac..9dcb65ad8 100644 --- a/plugins/guests/linux/guest.rb +++ b/plugins/guests/linux/guest.rb @@ -1,10 +1,13 @@ require 'log4r' require "vagrant" +require "vagrant/util/retryable" module VagrantPlugins module GuestLinux class Guest < Vagrant.plugin("2", :guest) + include Vagrant::Util::Retryable + class LinuxError < Vagrant::Errors::VagrantError error_namespace("vagrant.guest.linux") end @@ -55,9 +58,12 @@ module VagrantPlugins # Do the actual creating and mounting @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) + + retryable(:on => LinuxError, :tries => 3, :sleep => 1) do + @vm.communicate.sudo("mount -o vers=#{opts[:nfs_version]} #{ip}:'#{opts[:hostpath]}' #{real_guestpath}", + :error_class => LinuxError, + :error_key => :mount_nfs_fail) + end end end diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 551fcebc5..64211ffaa 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -195,6 +195,7 @@ module VagrantPlugins errors << I18n.t("vagrant.config.vm.box_not_found", :name => box) if \ box && !box_url && !machine.box + has_nfs = false @synced_folders.each do |id, options| hostpath = Pathname.new(options[:hostpath]).expand_path(machine.env.root_path) @@ -203,10 +204,23 @@ module VagrantPlugins :path => options[:hostpath]) end - if options[:nfs] && (options[:owner] || options[:group]) - # Owner/group don't work with NFS - errors << I18n.t("vagrant.config.vm.shared_folder_nfs_owner_group", - :path => options[:hostpath]) + if options[:nfs] + has_nfs = true + + if options[:owner] || options[:group] + # Owner/group don't work with NFS + errors << I18n.t("vagrant.config.vm.shared_folder_nfs_owner_group", + :path => options[:hostpath]) + end + end + end + + if has_nfs + if !machine.env.host + errors << I18n.t("vagrant.config.vm.nfs_requires_host") + else + errors << I18n.t("vagrant.config.vm.nfs_not_supported") if \ + !machine.env.host.nfs? end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 542c7ee9d..84f2ae7e0 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -33,9 +33,9 @@ module VagrantPlugins 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__) autoload :Package, File.expand_path("../action/package", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) + autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __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__) @@ -61,6 +61,7 @@ module VagrantPlugins b.use CheckPortCollisions b.use PruneNFSExports b.use NFS + b.use PrepareNFSSettings b.use ClearSharedFolders b.use ShareFolders b.use ClearNetworkInterfaces diff --git a/plugins/providers/virtualbox/action/nfs.rb b/plugins/providers/virtualbox/action/nfs.rb deleted file mode 100644 index a750260c4..000000000 --- a/plugins/providers/virtualbox/action/nfs.rb +++ /dev/null @@ -1,185 +0,0 @@ -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.synced_folders.each do |id, 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[id] = 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| - id, 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[id] = 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 == :private_network && 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.synced_folders.each do |id, options| - return true if options[: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/prepare_nfs_settings.rb b/plugins/providers/virtualbox/action/prepare_nfs_settings.rb new file mode 100644 index 000000000..9c49e7652 --- /dev/null +++ b/plugins/providers/virtualbox/action/prepare_nfs_settings.rb @@ -0,0 +1,51 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class PrepareNFSSettings + def initialize(app,env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::vm::nfs") + end + + def call(env) + @app.call(env) + + env[:nfs_host_ip] = read_host_ip(env[:machine]) + env[:nfs_machine_ip] = read_machine_ip(env[:machine]) + end + + # Returns the IP address of the first host only network adapter + # + # @param [Machine] machine + # @return [String] + def read_host_ip(machine) + machine.provider.driver.read_network_interfaces.each do |adapter, opts| + if opts[:type] == :hostonly + 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 read_machine_ip(machine) + machine.config.vm.networks.each do |type, args| + if type == :private_network && args[0].is_a?(String) + return args[0] + end + end + + nil + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/share_folders.rb b/plugins/providers/virtualbox/action/share_folders.rb index 1a41ea9d7..a5122bd1b 100644 --- a/plugins/providers/virtualbox/action/share_folders.rb +++ b/plugins/providers/virtualbox/action/share_folders.rb @@ -27,7 +27,8 @@ module VagrantPlugins def shared_folders {}.tap do |result| @env[:machine].config.vm.synced_folders.each do |id, data| - next if data[:disabled] + # Ignore NFS shared folders + next if data[:nfs] # This to prevent overwriting the actual shared folders data result[id] = data.dup diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 8e5aeb0e8..6267f8753 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -162,6 +162,12 @@ en: A multi-vm environment is required for name specification to this command. multi_vm_target_required: |- This command requires a specific VM name to target in a multi-VM environment. + nfs_no_guest_ip: |- + No guest IP was given to the Vagrant core NFS helper. This is an + internal error that should be reported as a bug. + nfs_no_host_ip: |- + No host IP was given to the Vagrant core NFS helper. This is + an internal error that should be reported as a bug. 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 @@ -406,6 +412,15 @@ en: network_ip_ends_one: |- The host only network IP '%{ip}' must not end in a 1, as this is reserved for the host machine. + nfs_not_supported: |- + It appears your machine doesn't support NFS, or there is not an + adapter to enable NFS on this machine for Vagrant. Please verify + that `nfsd` is installed on your machine, and try again. If you're + on Windows, NFS isn't supported. If the problem persists, please + contact Vagrant support. + nfs_requires_host: |- + Using NFS shared folders requires a host to be specified + using `config.vagrant.host`. shared_folder_hostpath_missing: |- The host path of the shared folder is missing: %{path} shared_folder_nfs_owner_group: |- @@ -697,18 +712,6 @@ en: specified network manually. preparing: "Preparing host only network..." nfs: - host_required: |- - A host class is required for NFS shared folders. By default, these - are auto-detected, but can be overridden with `config.vagrant.host`. - There is currently no host class loaded. - no_host_network: |- - NFS shared folders requires that host only networking is enabled - with a static IP. Please enable host only network and assign a - static IP via `config.vm.network`. - not_supported: |- - The host class is reporting that NFS is not supported by this host, - or `nfsd` may not be installed. Please verify that `nfsd` is installed - on your machine, and retry. exporting: Exporting NFS shared folders... mounting: Mounting NFS shared folders... persist: