diff --git a/lib/vagrant-parallels/cap/mount_options.rb b/lib/vagrant-parallels/cap/mount_options.rb new file mode 100644 index 00000000..8bb24af1 --- /dev/null +++ b/lib/vagrant-parallels/cap/mount_options.rb @@ -0,0 +1,39 @@ +require_relative "../util/unix_mount_helpers" + +module VagrantPlugins + module Parallels + module SyncedFolderCap + module MountOptions + extend VagrantPlugins::Parallels::Util::UnixMountHelpers + + PRL_MOUNT_TYPE = "prl_fs".freeze + + # Returns mount options for a parallels synced folder + # + # @param [Machine] machine + # @param [String] name of mount + # @param [String] path of mount on guest + # @param [Hash] hash of mount options + def self.mount_options(machine, name, guest_path, options) + 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(',') + return mount_options, mount_uid, mount_gid + end + + def self.mount_type(machine) + return PRL_MOUNT_TYPE + end + + def self.mount_name(machine, data) + data[:guestpath].gsub(/[*":<>?|\/\\]/,'_').sub(/^_/, '') + end + end + end + end +end diff --git a/lib/vagrant-parallels/errors.rb b/lib/vagrant-parallels/errors.rb index 6c3dcd6b..e6ad21a7 100644 --- a/lib/vagrant-parallels/errors.rb +++ b/lib/vagrant-parallels/errors.rb @@ -27,10 +27,6 @@ class JSONParseError < VagrantParallelsError error_key(:json_parse_error) end - class LinuxMountFailed < VagrantParallelsError - error_key(:linux_mount_failed) - end - class LinuxPrlFsInvalidOptions < VagrantParallelsError error_key(:linux_prl_fs_invalid_options) end @@ -59,6 +55,10 @@ class ParallelsInvalidVersion < VagrantParallelsError error_key(:parallels_invalid_version) end + class ParallelsMountFailed < VagrantParallelsError + error_key(:parallels_mount_failed) + end + class ParallelsNotDetected < VagrantParallelsError error_key(:parallels_not_detected) end diff --git a/lib/vagrant-parallels/guest_cap/linux/mount_parallels_shared_folder.rb b/lib/vagrant-parallels/guest_cap/linux/mount_parallels_shared_folder.rb index 2e1ec0ae..de2f0bd7 100644 --- a/lib/vagrant-parallels/guest_cap/linux/mount_parallels_shared_folder.rb +++ b/lib/vagrant-parallels/guest_cap/linux/mount_parallels_shared_folder.rb @@ -1,7 +1,19 @@ +require 'shellwords' + +require_relative "../../util/unix_mount_helpers" + module VagrantPlugins module Parallels module GuestLinuxCap class MountParallelsSharedFolder + extend VagrantPlugins::Parallels::Util::UnixMountHelpers + + # Mounts Parallels Desktop shared folder on linux guest + # + # @param [Machine] machine + # @param [String] name of mount + # @param [String] path of mount on guest + # @param [Hash] hash of mount options def self.mount_parallels_shared_folder(machine, name, guestpath, options) # Sanity check for mount options: we are not supporting # VirtualBox-specific 'fmode' and 'dmode' options @@ -15,83 +27,38 @@ def self.mount_parallels_shared_folder(machine, name, guestpath, options) end end - expanded_guest_path = machine.guest.capability( - :shell_expand_guest_path, guestpath) + guest_path = Shellwords.escape(guestpath) + mount_type = options[:plugin].capability(:mount_type) - mount_commands = [] + @@logger.debug("Mounting #{name} (#{options[:hostpath]} to #{guestpath})") - 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 - - # 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 prl_fs #{mount_options} #{name} #{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 prl_fs #{mount_options} #{name} #{expanded_guest_path}" - - # Clear prior symlink if exists - if machine.communicate.test("test -L #{expanded_guest_path}") - machine.communicate.sudo("rm #{expanded_guest_path}") - end + mount_options, mount_uid, mount_gid = options[:plugin].capability(:mount_options, name, guest_path, options) + mount_command = "mount -t #{mount_type} -o #{mount_options} #{name} #{guest_path}" # Create the guest path if it doesn't exist - machine.communicate.sudo("mkdir -p #{expanded_guest_path}") + machine.communicate.sudo("mkdir -p #{guest_path}") # Attempt to mount the folder. We retry here a few times because # it can fail early on. - attempts = 0 - while true - success = true - - mount_commands.each do |command| - no_such_device = false - status = machine.communicate.sudo(command, error_check: false) do |type, data| - no_such_device = true if type == :stderr && data =~ /No such device/i - end - - success = status == 0 && !no_such_device - break if success - end - - break if success - - attempts += 1 - if attempts > 10 - raise VagrantPlugins::Parallels::Errors::LinuxMountFailed, - command: mount_commands.join("\n") - end - - sleep 2 + stderr = "" + retryable(on: Errors::ParallelsMountFailed, tries: 3, sleep: 5) do + machine.communicate.sudo(mount_command, + error_class: Errors::ParallelsMountFailed, + error_key: :parallels_mount_failed, + command: mount_command, + output: stderr, + ) { |type, data| stderr = data if type == :stderr } end - # Emit an upstart event if we can - machine.communicate.sudo <<-EOH.gsub(/^ {10}/, "") - 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 - EOH + emit_upstart_notification(machine, guest_path) end def self.unmount_parallels_shared_folder(machine, guestpath, options) - result = machine.communicate.sudo( - "umount #{guestpath}", error_check: false) + guest_path = Shellwords.escape(guestpath) + + result = machine.communicate.sudo("umount #{guest_path}", error_check: false) if result == 0 - machine.communicate.sudo("rmdir #{guestpath}", error_check: false) + machine.communicate.sudo("rmdir #{guest_path}", error_check: false) end end diff --git a/lib/vagrant-parallels/plugin.rb b/lib/vagrant-parallels/plugin.rb index 9a3b63e1..45c39ff9 100644 --- a/lib/vagrant-parallels/plugin.rb +++ b/lib/vagrant-parallels/plugin.rb @@ -103,6 +103,21 @@ class Plugin < Vagrant.plugin('2') SyncedFolder end + synced_folder_capability(:parallels, "mount_name") do + require_relative "cap/mount_options" + SyncedFolderCap::MountOptions + end + + synced_folder_capability(:parallels, "mount_options") do + require_relative "cap/mount_options" + SyncedFolderCap::MountOptions + end + + synced_folder_capability(:parallels, "mount_type") do + require_relative "cap/mount_options" + SyncedFolderCap::MountOptions + end + # This initializes the internationalization strings. def self.setup_i18n I18n.load_path << File.expand_path('locales/en.yml', Parallels.source_root) diff --git a/lib/vagrant-parallels/synced_folder.rb b/lib/vagrant-parallels/synced_folder.rb index aa3f63c6..106ccb0a 100644 --- a/lib/vagrant-parallels/synced_folder.rb +++ b/lib/vagrant-parallels/synced_folder.rb @@ -19,14 +19,11 @@ def enable(machine, folders, _opts) end defs << { - name: os_friendly_id(id), + name: data[:plugin].capability(:mount_name, data), hostpath: hostpath.to_s, } end - # We should prepare only folders with unique hostpath values. - # Anyway, duplicates will be mounted later. - defs.uniq! { |d| d[:hostpath] } driver(machine).share_folders(defs) # short guestpaths first, so we don't step on ourselves @@ -39,8 +36,6 @@ def enable(machine, folders, _opts) end end - shf_config = driver(machine).read_shared_folders - # Parallels Shared Folder services can override Vagrant synced folder # configuration. These services should be pre-configured. if machine.guest.capability?(:prepare_psf_services) @@ -49,12 +44,8 @@ def enable(machine, folders, _opts) # Go through each folder and mount machine.ui.output(I18n.t('vagrant.actions.vm.share_folders.mounting')) - folders.each do |_ , data| - # Parallels specific: get id from the VM setting - # It allows to mount one host folder more then one time [GH-105] - id = shf_config.key(data[:hostpath]) - - if data[:guestpath] and id + folders.each do |id , data| + if data[:guestpath] # Guest path specified, so mount the folder to specified point machine.ui.detail(I18n.t('vagrant.actions.vm.share_folders.mounting_entry', guestpath: data[:guestpath], @@ -70,7 +61,11 @@ def enable(machine, folders, _opts) # Mount the actual folder machine.guest.capability( - :mount_parallels_shared_folder, id, data[:guestpath], data) + :mount_parallels_shared_folder, + data[:plugin].capability(:mount_name, data), + data[:guestpath], + data + ) else # If no guest path is specified, then automounting is disabled machine.ui.detail(I18n.t('vagrant.actions.vm.share_folders.nomount_entry', @@ -89,7 +84,7 @@ def disable(machine, folders, _opts) end # Remove the shared folders from the VM metadata - names = folders.map { |id, _data| os_friendly_id(id) } + names = folders.map { |_id, data| data[:plugin].capability(:mount_name, data) } driver(machine).unshare_folders(names) end @@ -103,11 +98,6 @@ def cleanup(machine, opts) def driver(machine) machine.provider.driver end - - def os_friendly_id(id) - # Replace chars *, ", :, <, >, ?, |, /, \ - id.gsub(/[*":<>?|\/\\]/,'_').sub(/^_/, '') - end end end end diff --git a/lib/vagrant-parallels/util/unix_mount_helpers.rb b/lib/vagrant-parallels/util/unix_mount_helpers.rb new file mode 100644 index 00000000..42d249eb --- /dev/null +++ b/lib/vagrant-parallels/util/unix_mount_helpers.rb @@ -0,0 +1,121 @@ +require "shellwords" +require "vagrant/util/retryable" + +module VagrantPlugins + module Parallels + module Util + 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: Errors::ParallelsMountFailed, + error_key: :parallels_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: Errors::ParallelsMountFailed, + error_key: :parallels_mount_failed, + command: gid_command, + output: output[:stderr] + ) { |type, data| output[type] << data if output[type] } + mount_gid = output[:stdout].split(':').at(2).to_s.chomp + self.class_variable_get(:@@logger).debug("Owner group ID (lookup): #{options[:group]} -> #{mount_gid}") + rescue Vagrant::Errors::ParallelsMountFailed + 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 + + def merge_mount_options(base, overrides) + base = base.join(",").split(",") + overrides = overrides.join(",").split(",") + b_kv = Hash[base.map{|item| item.split("=", 2) }] + o_kv = Hash[overrides.map{|item| item.split("=", 2) }] + merged = {}.tap do |opts| + (b_kv.keys + o_kv.keys).uniq.each do |key| + opts[key] = o_kv.fetch(key, b_kv[key]) + end + end + merged.map do |key, value| + [key, value].compact.join("=") + end + end + end + end + end +end diff --git a/locales/en.yml b/locales/en.yml index b2cb3f33..711fa423 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -45,14 +45,6 @@ en: disk is inconsistent, please remove it from the VM configuration. Disk image path: %{path} - linux_mount_failed: |- - Failed to mount folders in Linux guest. This is usually because - the "prl_fs" file system is not available. Please verify that - Parallels Tools are properly installed in the guest and - can work properly. If so, the VM reboot can solve a problem. - The command attempted was: - - %{command} linux_prl_fs_invalid_options: |- Failed to mount folders in Linux guest. You've specified mount options which are not supported by "prl_fs" file system. @@ -81,6 +73,19 @@ en: %{output} This is an internal error that should be reported as a bug. + parallels_mount_failed: |- + Vagrant was unable to mount Parallels Desktop shared folders. This is usually + because the filesystem "prl_fs" is not available. This filesystem is + made available via the Parallels Tools and kernel module. + Please verify that these guest tools are properly installed in the + guest. This is not a bug in Vagrant and is usually caused by a faulty + Vagrant box. For context, the command attempted was: + + %{command} + + The error output from the command was: + + %{output} parallels_no_room_for_high_level_network: |- There is no available slots on the Parallels Desktop VM for the configured high-level network interfaces. "private_network" and "public_network"