diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index ab05f27ab..ee0d8ea25 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -468,6 +468,10 @@ module Vagrant error_key(:nfs_bad_exports) end + class NFSDupePerms < VagrantError + error_key(:nfs_dupe_permissions) + end + class NFSExportsFailed < VagrantError error_key(:nfs_exports_failed) end diff --git a/plugins/hosts/linux/cap/nfs.rb b/plugins/hosts/linux/cap/nfs.rb index 731399b8e..45701f02b 100644 --- a/plugins/hosts/linux/cap/nfs.rb +++ b/plugins/hosts/linux/cap/nfs.rb @@ -29,6 +29,7 @@ module VagrantPlugins nfs_start_command = env.host.capability(:nfs_start_command) nfs_opts_setup(folders) + folders = folder_dupe_check(folders) output = Vagrant::Util::TemplateRenderer.render('nfs/exports_linux', uuid: id, ips: ips, @@ -84,6 +85,37 @@ module VagrantPlugins protected + # Takes a hash of folders and removes any duplicate exports that + # share the same hostpath to avoid duplicate entries in /etc/exports + # ref: GH-4666 + def self.folder_dupe_check(folders) + return_folders = {} + # Group by hostpath to see if there are multiple exports coming + # from the same folder + export_groups = folders.values.group_by { |h| h[:hostpath] } + + # We need to check that each group key only has 1 value, + # and if not, check each nfs option. If all nfs options are the same + # we're good, otherwise throw an exception + export_groups.each do |path,group| + if group.size > 1 + # if the linux nfs options aren't all the same throw an exception + group1_opts = group.first[:linux__nfs_options] + + if !group.all? {|g| g[:linux__nfs_options] == group1_opts} + raise Vagrant::Errors::NFSDupePerms, hostpath: group.first[:hostpath] + else + # if they're the same just pick the first one + return_folders[path] = group.first + end + else + # just return folder, there are no duplicates + return_folders[path] = group.first + end + end + return_folders + end + def self.nfs_cleanup(remove_ids) return if !File.exist?(NFS_EXPORTS_PATH) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index f012557d9..4a247e778 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -912,6 +912,9 @@ en: command: %{command} stdout: %{stdout} stderr: %{stderr} + nfs_dupe_permissions: |- + You have attempted to export the same nfs host path at %{hostpath} with + different nfs permissions. Please pick one permission and reload your guest. nfs_cant_read_exports: |- Vagrant can't read your current NFS exports! The exports file should be readable by any user. This is usually caused by invalid permissions diff --git a/test/unit/plugins/hosts/linux/cap/nfs_test.rb b/test/unit/plugins/hosts/linux/cap/nfs_test.rb index eed8b7221..557a470d5 100644 --- a/test/unit/plugins/hosts/linux/cap/nfs_test.rb +++ b/test/unit/plugins/hosts/linux/cap/nfs_test.rb @@ -79,6 +79,42 @@ EOH expect(exports_content).to include("/tmp") expect(exports_content).not_to include("/var") end + + it "throws an exception with at least 2 different nfs options" do + folders = {"/vagrant"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","all_squash"]}, + "/var/www/project"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","sync"]}} + + expect { cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1"], folders) }. + to raise_error Vagrant::Errors::NFSDupePerms + end + + it "writes only 1 hostpath for multiple exports" do + folders = {"/vagrant"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","all_squash"]}, + "/var/www/otherproject"=> + {:hostpath=>"/newhome/otherproject", + :linux__nfs_options=>["rw","all_squash"]}, + "/var/www/project"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","all_squash"]}} + valid_id = SecureRandom.uuid + content =<<-EOH +\n# VAGRANT-BEGIN: #{Process.uid} #{valid_id} +"/home/vagrant" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=) +"/newhome/otherproject" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=) +# VAGRANT-END: #{Process.uid} #{valid_id} +EOH + + cap.nfs_export(env, ui, valid_id, ["127.0.0.1"], folders) + exports_content = File.read(exports_path) + expect(exports_content).to eq(content) + end + end describe ".nfs_prune" do