From 9f393fc1e0c1f0ceefb3f94b22f0bcb5b170371e Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 30 Nov 2016 18:47:08 -0800 Subject: [PATCH] Use uid/gid from mount_options if provided for synced folders. This also extracts the gid/uid detection and upstart actions into reusable module to provide consistent behavior. --- .../linux/cap/mount_smb_shared_folder.rb | 73 ++++-------- .../cap/mount_virtualbox_shared_folder.rb | 68 ++---------- plugins/guests/linux/cap/nfs.rb | 19 +--- plugins/guests/tinycore/cap/mount_nfs.rb | 11 +- plugins/synced_folders/unix_mount_helpers.rb | 105 ++++++++++++++++++ .../guests/linux/cap/mount_nfs_test.rb | 2 +- .../mount_virtual_box_shared_folder_test.rb | 43 +++++++ .../docs/synced-folders/basic_usage.html.md | 13 +++ 8 files changed, 200 insertions(+), 134 deletions(-) create mode 100644 plugins/synced_folders/unix_mount_helpers.rb diff --git a/plugins/guests/linux/cap/mount_smb_shared_folder.rb b/plugins/guests/linux/cap/mount_smb_shared_folder.rb index bbec27148..bce106c6b 100644 --- a/plugins/guests/linux/cap/mount_smb_shared_folder.rb +++ b/plugins/guests/linux/cap/mount_smb_shared_folder.rb @@ -1,29 +1,23 @@ require "shellwords" +require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class MountSMBSharedFolder + + extend SyncedFolder::UnixMountHelpers + def self.mount_smb_shared_folder(machine, name, guestpath, options) expanded_guest_path = machine.guest.capability( :shell_expand_guest_path, guestpath) - mount_commands = [] mount_device = "//#{options[:smb_host]}/#{name}" - if options[:owner].is_a? Integer - mount_uid = options[:owner] - else - mount_uid = "`id -u #{options[:owner]}`" - end - - if options[:group].is_a? Integer - mount_gid = options[:group] - mount_gid_old = options[:group] - else - mount_gid = "`getent group #{options[:group]} | cut -d: -f3`" - mount_gid_old = "`id -g #{options[:group]}`" - end + 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] # If a domain is provided in the username, separate it username, domain = (options[:smb_username] || '').split('@', 2) @@ -33,15 +27,9 @@ module VagrantPlugins options[:mount_options] << "sec=ntlm" options[:mount_options] << "credentials=/etc/smb_creds_#{name}" - # First mount command uses getent to get the group mount_options = "-o uid=#{mount_uid},gid=#{mount_gid}" - mount_options += ",#{options[:mount_options].join(",")}" if options[:mount_options] - mount_commands << "mount -t cifs #{mount_options} #{mount_device} #{expanded_guest_path}" - - # Second mount command uses the old style `id -g` - mount_options = "-o uid=#{mount_uid},gid=#{mount_gid_old}" - mount_options += ",#{options[:mount_options].join(",")}" if options[:mount_options] - mount_commands << "mount -t cifs #{mount_options} #{mount_device} #{expanded_guest_path}" + mount_options += ",#{Array(options[:mount_options]).join(",")}" if options[:mount_options] + mount_command = "mount -t cifs #{mount_options} #{mount_device} #{expanded_guest_path}" # Create the guest path if it doesn't exist machine.communicate.sudo("mkdir -p #{expanded_guest_path}") @@ -58,46 +46,25 @@ SCRIPT # Attempt to mount the folder. We retry here a few times because # it can fail early on. - attempts = 0 - while true - success = true + retryable(on: Vagrant::Errors::LinuxMountFailed, tries: 10, sleep: 2) do + no_such_device = false stderr = "" - mount_commands.each do |command| - no_such_device = false - stderr = "" - status = machine.communicate.sudo(command, error_check: false) do |type, data| - if type == :stderr - no_such_device = true if data =~ /No such device/i - stderr += data.to_s - end + status = machine.communicate.sudo(mount_command, error_check: false) do |type, data| + if type == :stderr + no_such_device = true if data =~ /No such device/i + stderr += data.to_s end - - success = status == 0 && !no_such_device - break if success end - - break if success - - attempts += 1 - if attempts > 10 - command = mount_commands.join("\n") - command.gsub!(smb_password, "PASSWORDHIDDEN") - + if status != 0 || no_such_device + clean_command = mount_command.gsub(smb_password, "PASSWORDHIDDEN") raise Vagrant::Errors::LinuxMountFailed, - command: command, + command: clean_command, output: stderr end - - sleep 2 end - # Emit an upstart event if we can - machine.communicate.sudo <<-SCRIPT -if command -v /sbin/init && /sbin/init 2>/dev/null --version | grep upstart; then - /sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT='#{expanded_guest_path}' -fi -SCRIPT + emit_upstart_notification(machine, expanded_guest_path) end end end diff --git a/plugins/guests/linux/cap/mount_virtualbox_shared_folder.rb b/plugins/guests/linux/cap/mount_virtualbox_shared_folder.rb index 4f285cff8..5744565e1 100644 --- a/plugins/guests/linux/cap/mount_virtualbox_shared_folder.rb +++ b/plugins/guests/linux/cap/mount_virtualbox_shared_folder.rb @@ -1,67 +1,23 @@ -require "shellwords" - -require "vagrant/util/retryable" +require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class MountVirtualBoxSharedFolder - @@logger = Log4r::Logger.new("vagrant::guest::linux::mount_virtualbox_shared_folder") - - extend Vagrant::Util::Retryable + 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})") - if options[:owner].to_i.to_s == options[:owner].to_s - mount_uid = options[:owner] - @@logger.debug("Owner user ID (provided): #{mount_uid}") - else - output = {stdout: '', stderr: ''} - uid_command = "id -u #{options[:owner]}" - machine.communicate.execute(uid_command, - error_class: Vagrant::Errors::VirtualBoxMountFailed, - error_key: :virtualbox_mount_failed, - command: uid_command, - output: output[:stderr] - ) { |type, data| output[type] << data if output[type] } - mount_uid = output[:stdout].chomp - @@logger.debug("Owner user ID (lookup): #{options[:owner]} -> #{mount_uid}") - end - - if options[:group].to_i.to_s == options[:group].to_s - mount_gid = options[:group] - @@logger.debug("Owner group ID (provided): #{mount_gid}") - else - begin - output = {stdout: '', stderr: ''} - gid_command = "getent group #{options[:group]}" - machine.communicate.execute(gid_command, - error_class: Vagrant::Errors::VirtualBoxMountFailed, - error_key: :virtualbox_mount_failed, - command: gid_command, - output: output[:stderr] - ) { |type, data| output[type] << data if output[type] } - mount_gid = output[:stdout].split(':').at(2) - @@logger.debug("Owner group ID (lookup): #{options[:group]} -> #{mount_gid}") - rescue Vagrant::Errors::VirtualBoxMountFailed - if options[:owner] == options[:group] - @@logger.debug("Failed to locate group `#{options[:group]}`. Group name matches owner. Fetching effective group ID.") - output = {stdout: ''} - result = machine.communicate.execute("id -g #{options[:owner]}", - error_check: false - ) { |type, data| output[type] << data if output[type] } - mount_gid = output[:stdout].chomp if result == 0 - @@logger.debug("Owner group ID (effective): #{mount_gid}") - end - raise unless mount_gid - end - end - mount_options = options.fetch(:mount_options, []) - mount_options += ["uid=#{mount_uid}", "gid=#{mount_gid}"] + 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 -t vboxsf -o #{mount_options} #{name} #{guest_path}" @@ -87,14 +43,10 @@ module VagrantPlugins machine.communicate.sudo(chown_command) end - # Emit an upstart event if we can - machine.communicate.sudo <<-EOH.gsub(/^ {12}/, "") - if command -v /sbin/init && /sbin/init 2>/dev/null --version | grep upstart; then - /sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guest_path} - fi - EOH + emit_upstart_notification(machine, guest_path) end + def self.unmount_virtualbox_shared_folder(machine, guestpath, options) guest_path = Shellwords.escape(guestpath) diff --git a/plugins/guests/linux/cap/nfs.rb b/plugins/guests/linux/cap/nfs.rb index 8dadae1f3..9263688e5 100644 --- a/plugins/guests/linux/cap/nfs.rb +++ b/plugins/guests/linux/cap/nfs.rb @@ -1,10 +1,10 @@ -require "vagrant/util/retryable" +require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class NFS - extend Vagrant::Util::Retryable + extend SyncedFolder::UnixMountHelpers def self.nfs_client_installed(machine) machine.communicate.test("test -x /sbin/mount.nfs") @@ -30,18 +30,7 @@ module VagrantPlugins machine.communicate.sudo("mkdir -p #{guest_path}") - # Perform the mount operation and emit mount event if applicable - command = <<-EOH.gsub(/^ */, '') - mount -o #{mount_opts} #{ip}:#{host_path} #{guest_path} - result=$? - if test $result -eq 0; then - if test -x /sbin/initctl && command -v /sbin/init && /sbin/init 2>/dev/null --version | grep upstart; then - /sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guest_path} - fi - else - exit $result - fi - EOH + command = "mount -o #{mount_opts} #{ip}:#{host_path} #{guest_path}" # Run the command, raising a specific error. retryable(on: Vagrant::Errors::NFSMountFailed, tries: 3, sleep: 5) do @@ -49,6 +38,8 @@ module VagrantPlugins error_class: Vagrant::Errors::NFSMountFailed, ) end + + emit_upstart_notification(machine, guest_path) end end end diff --git a/plugins/guests/tinycore/cap/mount_nfs.rb b/plugins/guests/tinycore/cap/mount_nfs.rb index 1f39631b2..e6896ad6e 100644 --- a/plugins/guests/tinycore/cap/mount_nfs.rb +++ b/plugins/guests/tinycore/cap/mount_nfs.rb @@ -1,10 +1,10 @@ -require "vagrant/util/retryable" +require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestTinyCore module Cap class MountNFS - extend Vagrant::Util::Retryable + extend SyncedFolder::UnixMountHelpers def self.mount_nfs_folder(machine, ip, folders) folders.each do |name, opts| @@ -32,12 +32,7 @@ module VagrantPlugins error_class: Vagrant::Errors::NFSMountFailed) end - # Emit an upstart event if we can - machine.communicate.sudo <<-SCRIPT -if command -v /sbin/init && /sbin/init --version | grep upstart; then - /sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT='#{expanded_guest_path}' -fi -SCRIPT + emit_upstart_notification(machine, expanded_guest_path) end end end diff --git a/plugins/synced_folders/unix_mount_helpers.rb b/plugins/synced_folders/unix_mount_helpers.rb new file mode 100644 index 000000000..97d77975a --- /dev/null +++ b/plugins/synced_folders/unix_mount_helpers.rb @@ -0,0 +1,105 @@ +require "shellwords" +require "vagrant/util/retryable" + +module VagrantPlugins + module SyncedFolder + module UnixMountHelpers + + def self.extended(klass) + if !klass.class_variable_defined?(:@@logger) + klass.class_variable_set(:@@logger, Log4r::Logger.new(klass.name.downcase)) + end + klass.extend Vagrant::Util::Retryable + end + + def detect_owner_group_ids(machine, guest_path, mount_options, options) + mount_uid = find_mount_options_id("uid", mount_options) + mount_gid = find_mount_options_id("gid", mount_options) + + if mount_uid.nil? + if options[:owner].to_i.to_s == options[:owner].to_s + mount_uid = options[:owner] + self.class_variable_get(:@@logger).debug("Owner user ID (provided): #{mount_uid}") + else + output = {stdout: '', stderr: ''} + uid_command = "id -u #{options[:owner]}" + machine.communicate.execute(uid_command, + error_class: Vagrant::Errors::VirtualBoxMountFailed, + error_key: :virtualbox_mount_failed, + command: uid_command, + output: output[:stderr] + ) { |type, data| output[type] << data if output[type] } + mount_uid = output[:stdout].chomp + self.class_variable_get(:@@logger).debug("Owner user ID (lookup): #{options[:owner]} -> #{mount_uid}") + end + else + machine.ui.warn "Detected mount owner ID within mount options. (uid: #{mount_uid} guestpath: #{guest_path})" + end + + if mount_gid.nil? + if options[:group].to_i.to_s == options[:group].to_s + mount_gid = options[:group] + self.class_variable_get(:@@logger).debug("Owner group ID (provided): #{mount_gid}") + else + begin + output = {stdout: '', stderr: ''} + gid_command = "getent group #{options[:group]}" + machine.communicate.execute(gid_command, + error_class: Vagrant::Errors::VirtualBoxMountFailed, + error_key: :virtualbox_mount_failed, + command: gid_command, + output: output[:stderr] + ) { |type, data| output[type] << data if output[type] } + mount_gid = output[:stdout].split(':').at(2) + self.class_variable_get(:@@logger).debug("Owner group ID (lookup): #{options[:group]} -> #{mount_gid}") + rescue Vagrant::Errors::VirtualBoxMountFailed + if options[:owner] == options[:group] + self.class_variable_get(:@@logger).debug("Failed to locate group `#{options[:group]}`. Group name matches owner. Fetching effective group ID.") + output = {stdout: ''} + result = machine.communicate.execute("id -g #{options[:owner]}", + error_check: false + ) { |type, data| output[type] << data if output[type] } + mount_gid = output[:stdout].chomp if result == 0 + self.class_variable_get(:@@logger).debug("Owner group ID (effective): #{mount_gid}") + end + raise unless mount_gid + end + end + else + machine.ui.warn "Detected mount group ID within mount options. (gid: #{mount_gid} guestpath: #{guest_path})" + end + {:gid => mount_gid, :uid => mount_uid} + end + + def find_mount_options_id(id_name, mount_options) + id_line = mount_options.detect{|line| line.include?("#{id_name}=")} + if id_line + match = id_line.match(/,?#{Regexp.escape(id_name)}=(?\d+),?/) + found_id = match["option_id"] + updated_id_line = [ + match.pre_match, + match.post_match + ].find_all{|string| !string.empty?}.join(',') + if updated_id_line.empty? + mount_options.delete(id_line) + else + idx = mount_options.index(id_line) + mount_options.delete(idx) + mount_options.insert(idx, updated_id_line) + end + end + found_id + end + + def emit_upstart_notification(machine, guest_path) + # Emit an upstart event if we can + machine.communicate.sudo <<-EOH.gsub(/^ {12}/, "") + if command -v /sbin/init && /sbin/init 2>/dev/null --version | grep upstart; then + /sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guest_path} + fi + EOH + end + + end + end +end diff --git a/test/unit/plugins/guests/linux/cap/mount_nfs_test.rb b/test/unit/plugins/guests/linux/cap/mount_nfs_test.rb index 7cec549f2..f1f0a7254 100644 --- a/test/unit/plugins/guests/linux/cap/mount_nfs_test.rb +++ b/test/unit/plugins/guests/linux/cap/mount_nfs_test.rb @@ -71,7 +71,7 @@ describe "VagrantPlugins::GuestLinux::Cap::MountNFS" do } cap.mount_nfs_folder(machine, ip, folders) - expect(comm.received_commands[1]).to include( + expect(comm.received_commands[2]).to include( "/sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guestpath}") end diff --git a/test/unit/plugins/guests/linux/cap/mount_virtual_box_shared_folder_test.rb b/test/unit/plugins/guests/linux/cap/mount_virtual_box_shared_folder_test.rb index d05e844a8..7c673f511 100644 --- a/test/unit/plugins/guests/linux/cap/mount_virtual_box_shared_folder_test.rb +++ b/test/unit/plugins/guests/linux/cap/mount_virtual_box_shared_folder_test.rb @@ -149,6 +149,49 @@ describe "VagrantPlugins::GuestLinux::Cap::MountVirtualBoxSharedFolder" do 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 vboxsf -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 vboxsf -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 vboxsf -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 end describe ".unmount_virtualbox_shared_folder" do diff --git a/website/source/docs/synced-folders/basic_usage.html.md b/website/source/docs/synced-folders/basic_usage.html.md index 08fa08732..022107a52 100644 --- a/website/source/docs/synced-folders/basic_usage.html.md +++ b/website/source/docs/synced-folders/basic_usage.html.md @@ -100,6 +100,19 @@ config.vm.synced_folder "src/", "/srv/website", owner: "root", group: "root" ``` +_NOTE: Owner and group IDs defined within `mount_options` will have precedence +over the `owner` and `group` options._ + +For example, given the following configuration: + +```ruby +config.vm.synced_folder ".", "/vagrant", owner: "vagrant", + group: "vagrant", mount_options: ["uid=1234", "gid=1234"] +``` + +the mounted synced folder will be owned by the user with ID `1234` and the +group with ID `1234`. The `owner` and `group` options will be ignored. + ## Symbolic Links Support for symbolic links across synced folder implementations and