diff --git a/plugins/guests/freebsd/cap/virtualbox.rb b/plugins/guests/freebsd/cap/virtualbox.rb new file mode 100644 index 000000000..df1e91437 --- /dev/null +++ b/plugins/guests/freebsd/cap/virtualbox.rb @@ -0,0 +1,76 @@ +require_relative "../../../synced_folders/unix_mount_helpers" + +module VagrantPlugins + module GuestFreeBSD + module Cap + class VirtualBox + extend SyncedFolder::UnixMountHelpers + + def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) + guest_path = Shellwords.escape(guestpath) + + @@logger.debug("Mounting #{name} (#{options[:hostpath]} to #{guestpath})") + + builtin_mount_type = "-cit vboxvfs" + addon_mount_type = "-t vboxvfs" + + mount_options = options.fetch(:mount_options, []) + detected_ids = detect_owner_group_ids(machine, guest_path, mount_options, options) + mount_uid = detected_ids[:uid] + mount_gid = detected_ids[:gid] + + mount_options << "uid=#{mount_uid}" + mount_options << "gid=#{mount_gid}" + mount_options = mount_options.join(',') + mount_command = "mount #{addon_mount_type} -o #{mount_options} #{name} #{guest_path}" + + # Create the guest path if it doesn't exist + machine.communicate.sudo("mkdir -p #{guest_path}") + + stderr = "" + result = machine.communicate.sudo(mount_command, error_check: false) do |type, data| + stderr << data if type == :stderr + end + + if result != 0 + if stderr.include?("-cit") + @@logger.info("Detected builtin vboxvfs module, modifying mount command") + mount_command.sub!(addon_mount_type, builtin_mount_type) + end + + # Attempt to mount the folder. We retry here a few times because + # it can fail early on. + stderr = "" + retryable(on: Vagrant::Errors::VirtualBoxMountFailed, tries: 3, sleep: 5) do + machine.communicate.sudo(mount_command, + error_class: Vagrant::Errors::VirtualBoxMountFailed, + error_key: :virtualbox_mount_failed, + command: mount_command, + output: stderr, + ) { |type, data| stderr = data if type == :stderr } + end + end + + # Chown the directory to the proper user. We skip this if the + # mount options contained a readonly flag, because it won't work. + if !options[:mount_options] || !options[:mount_options].include?("ro") + chown_command = "chown #{mount_uid}:#{mount_gid} #{guest_path}" + machine.communicate.sudo(chown_command) + end + + emit_upstart_notification(machine, guest_path) + end + + + def self.unmount_virtualbox_shared_folder(machine, guestpath, options) + guest_path = Shellwords.escape(guestpath) + + result = machine.communicate.sudo("umount #{guest_path}", error_check: false) + if result == 0 + machine.communicate.sudo("rmdir #{guest_path}", error_check: false) + end + end + end + end + end +end diff --git a/plugins/guests/freebsd/plugin.rb b/plugins/guests/freebsd/plugin.rb index e6311e2f6..9234bf402 100644 --- a/plugins/guests/freebsd/plugin.rb +++ b/plugins/guests/freebsd/plugin.rb @@ -50,6 +50,11 @@ module VagrantPlugins require_relative "cap/shell_expand_guest_path" Cap::ShellExpandGuestPath end + + guest_capability(:freebsd, :mount_virtualbox_shared_folder) do + require_relative "cap/virtualbox" + Cap::VirtualBox + end end end end diff --git a/test/unit/plugins/guests/bsd/cap/virtualbox_test.rb b/test/unit/plugins/guests/bsd/cap/virtualbox_test.rb new file mode 100644 index 000000000..e8ed3d0d3 --- /dev/null +++ b/test/unit/plugins/guests/bsd/cap/virtualbox_test.rb @@ -0,0 +1,41 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestBSD::Cap::VirtualBox" do + let(:caps) do + VagrantPlugins::GuestBSD::Plugin + .components + .guest_capabilities[:bsd] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + let(:mount_owner){ "vagrant" } + let(:mount_group){ "vagrant" } + let(:mount_uid){ "1000" } + let(:mount_gid){ "1000" } + let(:mount_name){ "vagrant" } + let(:mount_guest_path){ "/vagrant" } + let(:folder_options) do + { + owner: mount_owner, + group: mount_group, + hostpath: "/host/directory/path" + } + end + let(:cap){ caps.get(:mount_virtualbox_shared_folder) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".mount_virtualbox_shared_folder" do + it "raises an error as unsupported" do + expect {cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) }. + to raise_error(Vagrant::Errors::VirtualBoxMountNotSupportedBSD) + end + end +end diff --git a/test/unit/plugins/guests/freebsd/cap/virtualbox_test.rb b/test/unit/plugins/guests/freebsd/cap/virtualbox_test.rb new file mode 100644 index 000000000..062b9e4c1 --- /dev/null +++ b/test/unit/plugins/guests/freebsd/cap/virtualbox_test.rb @@ -0,0 +1,225 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestFreeBSD::Cap::VirtualBox" do + let(:caps) do + VagrantPlugins::GuestFreeBSD::Plugin + .components + .guest_capabilities[:freebsd] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + let(:mount_owner){ "vagrant" } + let(:mount_group){ "vagrant" } + let(:mount_uid){ "1000" } + let(:mount_gid){ "1000" } + let(:mount_name){ "vagrant" } + let(:mount_guest_path){ "/vagrant" } + let(:folder_options) do + { + owner: mount_owner, + group: mount_group, + hostpath: "/host/directory/path" + } + end + let(:cap){ caps.get(:mount_virtualbox_shared_folder) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".mount_virtualbox_shared_folder" do + + before do + allow(comm).to receive(:sudo).with(any_args) + allow(comm).to receive(:execute).with(any_args) + end + + it "generates the expected default mount command" do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + + it "automatically chown's the mounted directory on guest" do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) + expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_gid} #{mount_guest_path}") + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + + context "with owner user ID explicitly defined" do + + before do + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") + end + + context "with user ID provided as Integer" do + let(:mount_owner){ 2000 } + + it "generates the expected mount command using mount_owner directly" do + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_owner},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) + expect(comm).to receive(:sudo).with("chown #{mount_owner}:#{mount_gid} #{mount_guest_path}") + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + + context "with user ID provided as String" do + let(:mount_owner){ "2000" } + + it "generates the expected mount command using mount_owner directly" do + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_owner},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) + expect(comm).to receive(:sudo).with("chown #{mount_owner}:#{mount_gid} #{mount_guest_path}") + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + + end + + context "with owner group ID explicitly defined" do + + before do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + end + + context "with owner group ID provided as Integer" do + let(:mount_group){ 2000 } + + it "generates the expected mount command using mount_group directly" do + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_group} #{mount_name} #{mount_guest_path}", anything) + expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_group} #{mount_guest_path}") + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + + context "with owner group ID provided as String" do + let(:mount_group){ "2000" } + + it "generates the expected mount command using mount_group directly" do + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_group} #{mount_name} #{mount_guest_path}", anything) + expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_group} #{mount_guest_path}") + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + + end + + context "with non-existent default owner group" do + + it "fetches the effective group ID of the user" do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) + expect(comm).to receive(:execute).with("id -g #{mount_owner}", anything).and_yield(:stdout, "1").and_return(0) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + + context "with non-existent owner group" do + + it "raises an error" do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) + expect do + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end.to raise_error Vagrant::Errors::VirtualBoxMountFailed + end + end + + context "with read-only option defined" do + + it "does not chown mounted guest directory" do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o ro,uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) + expect(comm).not_to receive(:sudo).with("chown #{mount_uid}:#{mount_gid} #{mount_guest_path}") + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["ro"])) + end + end + + context "with upstart init" do + + it "emits mount event" do + expect(comm).to receive(:sudo).with(/initctl emit/) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + + context "with custom mount options" do + + let(:ui){ double(:ui) } + before do + allow(ui).to receive(:warn) + allow(machine).to receive(:ui).and_return(ui) + end + + context "with uid defined" do + let(:options_uid){ '1234' } + + it "should only include uid defined within mount options" do + expect(comm).not_to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{options_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["uid=#{options_uid}"])) + end + end + + context "with gid defined" do + let(:options_gid){ '1234' } + + it "should only include gid defined within mount options" do + expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).not_to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{options_gid} #{mount_name} #{mount_guest_path}", anything) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["gid=#{options_gid}"])) + end + end + + context "with uid and gid defined" do + let(:options_gid){ '1234' } + let(:options_uid){ '1234' } + + it "should only include uid and gid defined within mount options" do + expect(comm).not_to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) + expect(comm).not_to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{options_gid}:") + expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{options_uid},gid=#{options_gid} #{mount_name} #{mount_guest_path}", anything) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["gid=#{options_gid}", "uid=#{options_uid}"])) + end + end + end + + context "with guest builtin vboxvfs module" do + let(:vbox_stderr){ <<-EOF +mount.vboxvfs cannot be used with mainline vboxvfs; instead use: + + mount -cit vboxvfs NAME MOUNTPOINT +EOF + } + it "should perform guest mount using builtin module" do + expect(comm).to receive(:sudo).with(/mount -t vboxvfs/, any_args).and_yield(:stderr, vbox_stderr).and_return(1) + expect(comm).to receive(:sudo).with(/mount -cit/, any_args) + cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) + end + end + end + + describe ".unmount_virtualbox_shared_folder" do + + after { cap.unmount_virtualbox_shared_folder(machine, mount_guest_path, folder_options) } + + it "unmounts shared directory and deletes directory on guest" do + expect(comm).to receive(:sudo).with("umount #{mount_guest_path}", anything).and_return(0) + expect(comm).to receive(:sudo).with("rmdir #{mount_guest_path}", anything) + end + + it "does not delete guest directory if unmount fails" do + expect(comm).to receive(:sudo).with("umount #{mount_guest_path}", anything).and_return(1) + expect(comm).not_to receive(:sudo).with("rmdir #{mount_guest_path}", anything) + end + end +end