Support properly setting up synced folders on Catalina

Since the root file system is marked as read-only, attempting to
link the shared directory to `/vagrant` will fail. If the guest
path is on the root file system and APFS is used, create the
link as a firmlink instead.
This commit is contained in:
Chris Roberts 2019-12-16 17:11:05 -08:00
parent 80c05460ab
commit 25659a6f6b
2 changed files with 254 additions and 14 deletions

View File

@ -1,3 +1,5 @@
require "securerandom"
module VagrantPlugins
module GuestDarwin
module Cap
@ -5,31 +7,102 @@ module VagrantPlugins
# we seem to be unable to ask 'mount -t vmhgfs' to mount the roots
# of specific shares, so instead we symlink from what is already
# mounted by the guest tools
# mounted by the guest tools
# (ie. the behaviour of the VMware_fusion provider prior to 0.8.x)
def self.mount_vmware_shared_folder(machine, name, guestpath, options)
# Use this variable to determine which machines
# have been registered with after hook
@apply_firmlinks ||= Hash.new{ |h, k| h[k] = {bootstrap: false, content: []} }
machine.communicate.tap do |comm|
# clear prior symlink
if comm.test("test -L \"#{guestpath}\"", sudo: true)
comm.sudo("rm -f \"#{guestpath}\"")
# check if we are dealing with an APFS root container
if comm.test("test -d /System/Volumes/Data")
parts = Pathname.new(guestpath).descend.to_a
firmlink = parts[1].to_s
firmlink.slice!(0, 1) if firmlink.start_with?("/")
if parts.size > 2
guestpath = File.join("/System/Volumes/Data", guestpath)
else
guestpath = nil
end
end
# clear prior directory if exists
if comm.test("test -d \"#{guestpath}\"", sudo: true)
comm.sudo("rm -Rf \"#{guestpath}\"")
# Remove existing symlink or directory if defined
if guestpath
if comm.test("test -L \"#{guestpath}\"")
comm.sudo("rm -f \"#{guestpath}\"")
elsif comm.test("test -d \"#{guestpath}\"")
comm.sudo("rm -Rf \"#{guestpath}\"")
end
# create intermediate directories if needed
intermediate_dir = File.dirname(guestpath)
if intermediate_dir != "/"
comm.sudo("mkdir -p \"#{intermediate_dir}\"")
end
comm.sudo("ln -s \"/Volumes/VMware Shared Folders/#{name}\" \"#{guestpath}\"")
end
# create intermediate directories if needed
intermediate_dir = File.dirname(guestpath)
if !comm.test("test -d \"#{intermediate_dir}\"", sudo: true)
comm.sudo("mkdir -p \"#{intermediate_dir}\"")
end
if firmlink && !system_firmlink?(firmlink)
if guestpath.nil?
guestpath = "/Volumes/VMware Shared Folders/#{name}"
else
guestpath = File.join("/System/Volumes/Data", firmlink)
end
# finally make the symlink
comm.sudo("ln -s \"/Volumes/VMware Shared Folders/#{name}\" \"#{guestpath}\"")
share_line = "#{firmlink}\t#{guestpath}"
# Check if the line is already defined. If so, bail since we are done
if !comm.test("[[ \"$(</etc/synthetic.conf)\" = *\"#{share_line}\"* ]]")
@apply_firmlinks[machine.id][:bootstrap] = true
end
# If we haven't already added our hook to apply firmlinks, do it now
if @apply_firmlinks[machine.id][:content].empty?
Plugin.action_hook(:apfs_firmlinks, :after_synced_folders) do |hook|
action = proc { |*_|
content = @apply_firmlinks[machine.id][:content].join("\n")
# Write out the synthetic file
comm.sudo("echo -e #{content.inspect} > /etc/synthetic.conf")
if @apply_firmlinks[:bootstrap]
# Re-bootstrap the root container to pick up firmlink updates
comm.sudo("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -B")
end
}
hook.prepend(action)
end
end
@apply_firmlinks[machine.id][:content] << share_line
end
end
end
# Check if firmlink is provided by the system
#
# @param [String] firmlink Firmlink path
# @return [Boolean]
def self.system_firmlink?(firmlink)
if !@_firmlinks
if File.exist?("/usr/share/firmlinks")
@_firmlinks = File.readlines("/usr/share/firmlinks").map do |line|
line.split.first
end
else
@_firmlinks = []
end
end
firmlink = "/#{firmlink}" if !firmlink.start_with?("/")
@_firmlinks.include?(firmlink)
end
# @private
# Reset the cached values for capability. This is not considered a public
# API and should only be used for testing.
def self.reset!
instance_variables.each(&method(:remove_instance_variable))
end
end
end
end

View File

@ -0,0 +1,167 @@
require_relative "../../../../base"
describe "VagrantPlugins::GuestDarwin::Cap::MountVmwareSharedFolder" do
let(:described_class) do
VagrantPlugins::GuestDarwin::Plugin
.components
.guest_capabilities[:darwin]
.get(:mount_vmware_shared_folder)
end
let(:machine) { double("machine", communicate: communicator, id: "MACHINE_ID") }
let(:communicator) { double("communicator") }
before do
allow(communicator).to receive(:test)
allow(communicator).to receive(:sudo)
allow(VagrantPlugins::GuestDarwin::Plugin).to receive(:action_hook)
end
describe ".mount_vmware_shared_folder" do
let(:name) { "-vagrant" }
let(:guestpath) { "/vagrant" }
let(:options) { {} }
before do
allow(described_class).to receive(:system_firmlink?)
described_class.reset!
end
after { described_class.
mount_vmware_shared_folder(machine, name, guestpath, options) }
context "with APFS root container" do
before do
expect(communicator).to receive(:test).with("test -d /System/Volumes/Data").and_return(true)
end
it "should check for existing entry" do
expect(communicator).to receive(:test).with(/synthetic\.conf/)
end
it "should register an action hook" do
expect(VagrantPlugins::GuestDarwin::Plugin).to receive(:action_hook).with(:apfs_firmlinks, :after_synced_folders)
end
context "with guest path within existing directory" do
let(:guestpath) { "/Users/vagrant/workspace" }
it "should test if guest path is a symlink" do
expect(communicator).to receive(:test).with(/test -L/)
end
it "should remove guest path if it is a symlink" do
expect(communicator).to receive(:test).with(/test -L/).and_return(true)
expect(communicator).to receive(:sudo).with(/rm -f/)
end
it "should not test if guest path is a directory if guest path is symlink" do
expect(communicator).to receive(:test).with(/test -L/).and_return(true)
expect(communicator).not_to receive(:test).with(/test -d/)
end
it "should test if guest path is directory if not a symlink" do
expect(communicator).to receive(:test).with(/test -d/)
end
it "should remove guest path if it is a directory" do
expect(communicator).to receive(:test).with(/test -d/).and_return(true)
expect(communicator).to receive(:sudo).with(/rm -Rf/)
end
it "should create the symlink to the vmware folder" do
expect(communicator).to receive(:sudo).with(/ln -s/)
end
it "should create the symlink within the writable APFS container" do
expect(communicator).to receive(:sudo).with(%r{ln -s .+/System/Volumes/Data.+})
end
it "should register an action hook" do
expect(VagrantPlugins::GuestDarwin::Plugin).to receive(:action_hook).with(:apfs_firmlinks, :after_synced_folders)
end
context "when firmlink is provided by the system" do
before { expect(described_class).to receive(:system_firmlink?).and_return(true) }
it "should not register an action hook" do
expect(VagrantPlugins::GuestDarwin::Plugin).not_to receive(:action_hook).with(:apfs_firmlinks, :after_synced_folders)
end
end
end
end
context "with non-APFS root container" do
before do
expect(communicator).to receive(:test).with("test -d /System/Volumes/Data").and_return(false)
end
it "should test if guest path is a symlink" do
expect(communicator).to receive(:test).with(/test -L/)
end
it "should remove guest path if it is a symlink" do
expect(communicator).to receive(:test).with(/test -L/).and_return(true)
expect(communicator).to receive(:sudo).with(/rm -f/)
end
it "should not test if guest path is a directory if guest path is symlink" do
expect(communicator).to receive(:test).with(/test -L/).and_return(true)
expect(communicator).not_to receive(:test).with(/test -d/)
end
it "should test if guest path is directory if not a symlink" do
expect(communicator).to receive(:test).with(/test -d/)
end
it "should remove guest path if it is a directory" do
expect(communicator).to receive(:test).with(/test -d/).and_return(true)
expect(communicator).to receive(:sudo).with(/rm -Rf/)
end
it "should create the symlink to the vmware folder" do
expect(communicator).to receive(:sudo).with(/ln -s/)
end
it "should not register an action hook" do
expect(VagrantPlugins::GuestDarwin::Plugin).not_to receive(:action_hook).with(:apfs_firmlinks, :after_synced_folders)
end
end
end
describe ".system_firmlink?" do
before { described_class.reset! }
context "when file does not exist" do
before { allow(File).to receive(:exist?).with("/usr/share/firmlinks").and_return(false) }
it "should always return false" do
expect(described_class.system_firmlink?("test")).to be_falsey
end
end
context "when file does exist" do
let(:content) {
["/Users\tUsers",
"/usr/local\tusr/local"]
}
before do
expect(File).to receive(:exist?).with("/usr/share/firmlinks").and_return(true)
expect(File).to receive(:readlines).with("/usr/share/firmlinks").and_return(content)
end
it "should return true when firmlink exists" do
expect(described_class.system_firmlink?("/Users")).to be_truthy
end
it "should return true when firmlink is not prefixed with /" do
expect(described_class.system_firmlink?("Users")).to be_truthy
end
it "should return false when firmlink does not exist" do
expect(described_class.system_firmlink?("/testing")).to be_falsey
end
end
end
end