diff --git a/lib/vagrant/action/builtin.rb b/lib/vagrant/action/builtin.rb index fe0e38c43..962f5662c 100644 --- a/lib/vagrant/action/builtin.rb +++ b/lib/vagrant/action/builtin.rb @@ -9,6 +9,7 @@ module Vagrant use VM::Import use VM::Customize use VM::ForwardPorts + use VM::ShareFolders end register :up, up diff --git a/lib/vagrant/action/vm/share_folders.rb b/lib/vagrant/action/vm/share_folders.rb new file mode 100644 index 000000000..905402f58 --- /dev/null +++ b/lib/vagrant/action/vm/share_folders.rb @@ -0,0 +1,105 @@ +module Vagrant + class Action + module VM + class ShareFolders + def initialize(app, env) + @app = app + @env = env + end + + def call(env) + @env = env + + clear_shared_folders + create_metadata + + @app.call(env) + + mount_shared_folders + setup_unison + end + + # This method returns an actual list of VirtualBox shared + # folders to create and their proper path. + def shared_folders + @env.env.config.vm.shared_folders.inject({}) do |acc, data| + key, value = data + + # This to prevent overwriting the actual shared folders data + value = value.dup + + if value[:sync] + # Syncing this folder. Change the guestpath to reflect + # what we're actually mounting. + value[:original] = value.dup + value[:guestpath] = "#{value[:guestpath]}#{@env.env.config.unison.folder_suffix}" + end + + acc[key] = value + acc + end + end + + # This method returns the list of shared folders which are to + # be synced via unison. + def unison_folders + shared_folders.inject({}) do |acc, data| + key, value = data + acc[key] = value if !!value[:sync] + acc + end + end + + def clear_shared_folders + if @env["vm"].vm.shared_folders.length > 0 + @env.logger.info "Clearing previously set shared folders..." + + folders = @env["vm"].vm.shared_folders.dup + folders.each do |shared_folder| + shared_folder.destroy + end + + @env["vm"].reload! + end + end + + def create_metadata + @env.logger.info "Creating shared folders metadata..." + + shared_folders.each do |name, data| + folder = VirtualBox::SharedFolder.new + folder.name = name + folder.host_path = File.expand_path(data[:hostpath], @env.env.root_path) + @env["vm"].vm.shared_folders << folder + end + + @env["vm"].vm.save + end + + def mount_shared_folders + @env.logger.info "Mounting shared folders..." + + @env["vm"].ssh.execute do |ssh| + shared_folders.each do |name, data| + @env.logger.info "-- #{name}: #{data[:guestpath]}" + @env["vm"].system.mount_shared_folder(ssh, name, data[:guestpath]) + end + end + end + + def setup_unison + return if unison_folders.empty? + + @env["vm"].ssh.execute do |ssh| + @env["vm"].system.prepare_unison(ssh) + + @env.logger.info "Creating unison crontab entries..." + unison_folders.each do |name, data| + @env["vm"].system.create_unison(ssh, data) + end + end + end + end + end + end +end diff --git a/test/vagrant/action/vm/share_folders_test.rb b/test/vagrant/action/vm/share_folders_test.rb new file mode 100644 index 000000000..f5ba746bc --- /dev/null +++ b/test/vagrant/action/vm/share_folders_test.rb @@ -0,0 +1,217 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') + +class ShareFoldersVMActionTest < Test::Unit::TestCase + setup do + @klass = Vagrant::Action::VM::ShareFolders + @app, @env = mock_action_data + + @vm = mock("vm") + @vm.stubs(:name).returns("foo") + @vm.stubs(:ssh).returns(mock("ssh")) + @vm.stubs(:system).returns(mock("system")) + @env["vm"] = @vm + + @internal_vm = mock("internal") + @vm.stubs(:vm).returns(@internal_vm) + + @instance = @klass.new(@app, @env) + end + + def stub_shared_folders + env = mock_environment do |config| + config.vm.shared_folders.clear + + if block_given? + yield config + else + folders = [%w{foo fooguest foohost}, %w{bar barguest barhost}] + folders.each do |data| + config.vm.share_folder(*data) + end + end + end + + @env.stubs(:env).returns(env) + env.config.vm.shared_folders + end + + context "calling" do + should "run the methods in the proper order" do + before_seq = sequence("before") + @instance.expects(:clear_shared_folders).once.in_sequence(before_seq) + @instance.expects(:create_metadata).once.in_sequence(before_seq) + @app.expects(:call).with(@env).in_sequence(before_seq) + @instance.expects(:mount_shared_folders).once.in_sequence(before_seq) + @instance.expects(:setup_unison).once.in_sequence(before_seq) + + @instance.call(@env) + end + end + + context "collecting shared folders" do + setup do + File.stubs(:expand_path).returns("baz") + end + + should "return a hash of the shared folders" do + data = { + "foo" => %W[bar baz], + "bar" => %W[foo baz] + } + + stub_shared_folders do |config| + data.each do |name, value| + config.vm.share_folder(name, *value) + end + end + + result = @instance.shared_folders + assert_equal data.length, result.length + data.each do |name, value| + guest, host = value + assert_equal guest, result[name][:guestpath] + assert_equal host, result[name][:hostpath] + end + end + + should "append sync suffix if sync enabled to a folder" do + name = "foo" + guest = "bar" + host = "baz" + + stub_shared_folders do |config| + config.vm.share_folder(name, guest, host, :sync => true) + end + + result = @instance.shared_folders + assert_equal "#{guest}#{@env.env.config.unison.folder_suffix}", result[name][:guestpath] + assert_equal guest, result[name][:original][:guestpath] + end + + should "not destroy original hash" do + @folders = stub_shared_folders do |config| + config.vm.share_folder("foo", "bar", "baz", :sync => true) + end + + folder = @folders["foo"].dup + + @instance.shared_folders + assert_equal folder, @env.env.config.vm.shared_folders["foo"] + end + end + + context "unison shared folders" do + setup do + @folders = stub_shared_folders do |config| + config.vm.share_folder("foo", "bar", "baz", :sync => true) + config.vm.share_folder("bar", "foo", "baz") + end + end + + should "only return the folders marked for syncing" do + result = @instance.unison_folders + assert_equal 1, result.length + assert result.has_key?("foo") + assert !result.has_key?("bar") + end + end + + context "clearing shared folders" do + setup do + @shared_folder = mock("shared_folder") + @shared_folders = [@shared_folder] + @internal_vm.stubs(:shared_folders).returns(@shared_folders) + end + + should "call destroy on each shared folder then reload" do + destroy_seq = sequence("destroy") + @shared_folders.each do |sf| + sf.expects(:destroy).once.in_sequence(destroy_seq) + end + + @vm.expects(:reload!).once.in_sequence(destroy_seq) + @instance.clear_shared_folders + end + + should "do nothing if no shared folders existed" do + @shared_folders.clear + @vm.expects(:reload!).never + @instance.clear_shared_folders + end + end + + context "setting up shared folder metadata" do + setup do + stub_shared_folders + end + + should "add all shared folders to the VM" do + shared_folders = [] + data = %W[foo bar] + shared_folders.expects(:<<).times(data.length).with() do |sf| + hostpath = File.expand_path("#{sf.name}host", @env.env.root_path) + assert data.include?(sf.name) + assert_equal hostpath, sf.host_path + true + end + + @internal_vm.stubs(:shared_folders).returns(shared_folders) + @internal_vm.expects(:save).once + + @instance.create_metadata + end + end + + context "mounting the shared folders" do + setup do + @folders = stub_shared_folders + @ssh = mock("ssh") + @vm.ssh.stubs(:execute).yields(@ssh) + @vm.system.stubs(:mount_shared_folder) + end + + should "mount all shared folders to the VM" do + mount_seq = sequence("mount_seq") + @folders.each do |name, data| + @vm.system.expects(:mount_shared_folder).with(@ssh, name, data[:guestpath]).in_sequence(mount_seq) + end + + @instance.mount_shared_folders + end + end + + context "setting up unison" do + setup do + @ssh = mock("ssh") + @vm.ssh.stubs(:execute).yields(@ssh) + + @folders = stub_shared_folders do |config| + config.vm.share_folder("foo", "bar", "baz", :sync => true) + config.vm.share_folder("bar", "foo", "baz") + end + end + + should "do nothing if unison folders is empty" do + @instance.stubs(:unison_folders).returns({}) + @vm.ssh.expects(:execute).never + @instance.setup_unison + end + + should "prepare unison then create for each folder" do + seq = sequence("unison seq") + @vm.system.expects(:prepare_unison).with(@ssh).once.in_sequence(seq) + @instance.unison_folders.each do |name, data| + if data[:sync] + @vm.system.expects(:create_unison).with do |ssh, opts| + assert_equal @ssh, ssh + assert_equal data, opts + + true + end + end + end + + @instance.setup_unison + end + end +end