Merge pull request #8122 from chrisroberts/virtualbox/uid-gid

Use uid/gid from mount_options if provided for synced folders.
This commit is contained in:
Chris Roberts 2017-03-06 14:21:11 -08:00 committed by GitHub
commit dbf01572ef
8 changed files with 200 additions and 134 deletions

View File

@ -1,29 +1,23 @@
require "shellwords" require "shellwords"
require_relative "../../../synced_folders/unix_mount_helpers"
module VagrantPlugins module VagrantPlugins
module GuestLinux module GuestLinux
module Cap module Cap
class MountSMBSharedFolder class MountSMBSharedFolder
extend SyncedFolder::UnixMountHelpers
def self.mount_smb_shared_folder(machine, name, guestpath, options) def self.mount_smb_shared_folder(machine, name, guestpath, options)
expanded_guest_path = machine.guest.capability( expanded_guest_path = machine.guest.capability(
:shell_expand_guest_path, guestpath) :shell_expand_guest_path, guestpath)
mount_commands = []
mount_device = "//#{options[:smb_host]}/#{name}" mount_device = "//#{options[:smb_host]}/#{name}"
if options[:owner].is_a? Integer mount_options = options.fetch(:mount_options, [])
mount_uid = options[:owner] detected_ids = detect_owner_group_ids(machine, guest_path, mount_options, options)
else mount_uid = detected_ids[:uid]
mount_uid = "`id -u #{options[:owner]}`" mount_gid = detected_ids[:gid]
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
# If a domain is provided in the username, separate it # If a domain is provided in the username, separate it
username, domain = (options[:smb_username] || '').split('@', 2) username, domain = (options[:smb_username] || '').split('@', 2)
@ -33,15 +27,9 @@ module VagrantPlugins
options[:mount_options] << "sec=ntlm" options[:mount_options] << "sec=ntlm"
options[:mount_options] << "credentials=/etc/smb_creds_#{name}" 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 = "-o uid=#{mount_uid},gid=#{mount_gid}"
mount_options += ",#{options[:mount_options].join(",")}" if options[:mount_options] mount_options += ",#{Array(options[:mount_options]).join(",")}" if options[:mount_options]
mount_commands << "mount -t cifs #{mount_options} #{mount_device} #{expanded_guest_path}" mount_command = "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}"
# Create the guest path if it doesn't exist # Create the guest path if it doesn't exist
machine.communicate.sudo("mkdir -p #{expanded_guest_path}") 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 # Attempt to mount the folder. We retry here a few times because
# it can fail early on. # it can fail early on.
attempts = 0
while true
success = true
stderr = "" retryable(on: Vagrant::Errors::LinuxMountFailed, tries: 10, sleep: 2) do
mount_commands.each do |command|
no_such_device = false no_such_device = false
stderr = "" stderr = ""
status = machine.communicate.sudo(command, error_check: false) do |type, data| status = machine.communicate.sudo(mount_command, error_check: false) do |type, data|
if type == :stderr if type == :stderr
no_such_device = true if data =~ /No such device/i no_such_device = true if data =~ /No such device/i
stderr += data.to_s stderr += data.to_s
end end
end end
if status != 0 || no_such_device
success = status == 0 && !no_such_device clean_command = mount_command.gsub(smb_password, "PASSWORDHIDDEN")
break if success
end
break if success
attempts += 1
if attempts > 10
command = mount_commands.join("\n")
command.gsub!(smb_password, "PASSWORDHIDDEN")
raise Vagrant::Errors::LinuxMountFailed, raise Vagrant::Errors::LinuxMountFailed,
command: command, command: clean_command,
output: stderr output: stderr
end end
sleep 2
end end
# Emit an upstart event if we can emit_upstart_notification(machine, expanded_guest_path)
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
end end
end end
end end

View File

@ -1,67 +1,23 @@
require "shellwords" require_relative "../../../synced_folders/unix_mount_helpers"
require "vagrant/util/retryable"
module VagrantPlugins module VagrantPlugins
module GuestLinux module GuestLinux
module Cap module Cap
class MountVirtualBoxSharedFolder class MountVirtualBoxSharedFolder
@@logger = Log4r::Logger.new("vagrant::guest::linux::mount_virtualbox_shared_folder") extend SyncedFolder::UnixMountHelpers
extend Vagrant::Util::Retryable
def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) def self.mount_virtualbox_shared_folder(machine, name, guestpath, options)
guest_path = Shellwords.escape(guestpath) guest_path = Shellwords.escape(guestpath)
@@logger.debug("Mounting #{name} (#{options[:hostpath]} to #{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 = 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_options = mount_options.join(',')
mount_command = "mount -t vboxsf -o #{mount_options} #{name} #{guest_path}" mount_command = "mount -t vboxsf -o #{mount_options} #{name} #{guest_path}"
@ -87,14 +43,10 @@ module VagrantPlugins
machine.communicate.sudo(chown_command) machine.communicate.sudo(chown_command)
end end
# Emit an upstart event if we can emit_upstart_notification(machine, guest_path)
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
def self.unmount_virtualbox_shared_folder(machine, guestpath, options) def self.unmount_virtualbox_shared_folder(machine, guestpath, options)
guest_path = Shellwords.escape(guestpath) guest_path = Shellwords.escape(guestpath)

View File

@ -1,10 +1,10 @@
require "vagrant/util/retryable" require_relative "../../../synced_folders/unix_mount_helpers"
module VagrantPlugins module VagrantPlugins
module GuestLinux module GuestLinux
module Cap module Cap
class NFS class NFS
extend Vagrant::Util::Retryable extend SyncedFolder::UnixMountHelpers
def self.nfs_client_installed(machine) def self.nfs_client_installed(machine)
machine.communicate.test("test -x /sbin/mount.nfs") machine.communicate.test("test -x /sbin/mount.nfs")
@ -30,18 +30,7 @@ module VagrantPlugins
machine.communicate.sudo("mkdir -p #{guest_path}") machine.communicate.sudo("mkdir -p #{guest_path}")
# Perform the mount operation and emit mount event if applicable command = "mount -o #{mount_opts} #{ip}:#{host_path} #{guest_path}"
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
# Run the command, raising a specific error. # Run the command, raising a specific error.
retryable(on: Vagrant::Errors::NFSMountFailed, tries: 3, sleep: 5) do retryable(on: Vagrant::Errors::NFSMountFailed, tries: 3, sleep: 5) do
@ -49,6 +38,8 @@ module VagrantPlugins
error_class: Vagrant::Errors::NFSMountFailed, error_class: Vagrant::Errors::NFSMountFailed,
) )
end end
emit_upstart_notification(machine, guest_path)
end end
end end
end end

View File

@ -1,10 +1,10 @@
require "vagrant/util/retryable" require_relative "../../../synced_folders/unix_mount_helpers"
module VagrantPlugins module VagrantPlugins
module GuestTinyCore module GuestTinyCore
module Cap module Cap
class MountNFS class MountNFS
extend Vagrant::Util::Retryable extend SyncedFolder::UnixMountHelpers
def self.mount_nfs_folder(machine, ip, folders) def self.mount_nfs_folder(machine, ip, folders)
folders.each do |name, opts| folders.each do |name, opts|
@ -32,12 +32,7 @@ module VagrantPlugins
error_class: Vagrant::Errors::NFSMountFailed) error_class: Vagrant::Errors::NFSMountFailed)
end end
# Emit an upstart event if we can emit_upstart_notification(machine, expanded_guest_path)
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
end end
end end
end end

View File

@ -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)}=(?<option_id>\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

View File

@ -71,7 +71,7 @@ describe "VagrantPlugins::GuestLinux::Cap::MountNFS" do
} }
cap.mount_nfs_folder(machine, ip, folders) 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}") "/sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guestpath}")
end end

View File

@ -149,6 +149,49 @@ describe "VagrantPlugins::GuestLinux::Cap::MountVirtualBoxSharedFolder" do
cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options)
end end
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 end
describe ".unmount_virtualbox_shared_folder" do describe ".unmount_virtualbox_shared_folder" do

View File

@ -100,6 +100,19 @@ config.vm.synced_folder "src/", "/srv/website",
owner: "root", group: "root" 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 ## Symbolic Links
Support for symbolic links across synced folder implementations and Support for symbolic links across synced folder implementations and