From 38fbbb6c569e9e257c0da31681c457e015cdb490 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Jan 2014 17:40:29 -0800 Subject: [PATCH] synced_folders/rsync: Initial commit working --- .../action/builtin/mixin_synced_folders.rb | 2 +- lib/vagrant/errors.rb | 8 ++ lib/vagrant/plugin/v2/synced_folder.rb | 5 +- plugins/synced_folders/nfs/synced_folder.rb | 2 +- plugins/synced_folders/rsync/plugin.rb | 18 ++++ plugins/synced_folders/rsync/synced_folder.rb | 86 +++++++++++++++ templates/locales/en.yml | 13 +++ .../rsync/synced_folder_test.rb | 100 ++++++++++++++++++ .../shared/action_synced_folders_context.rb | 3 +- 9 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 plugins/synced_folders/rsync/plugin.rb create mode 100644 plugins/synced_folders/rsync/synced_folder.rb create mode 100644 test/unit/plugins/synced_folders/rsync/synced_folder_test.rb diff --git a/lib/vagrant/action/builtin/mixin_synced_folders.rb b/lib/vagrant/action/builtin/mixin_synced_folders.rb index aca214028..0710ca883 100644 --- a/lib/vagrant/action/builtin/mixin_synced_folders.rb +++ b/lib/vagrant/action/builtin/mixin_synced_folders.rb @@ -71,7 +71,7 @@ module Vagrant raise "Internal error. Report this as a bug. Invalid: #{data[:type]}" end - if !impl_class[0].new.usable?(machine) + if !impl_class[0].new.usable?(machine, true) # Verify that explicitly defined shared folder types are # actually usable. raise Errors::SyncedFolderUnusable, type: data[:type].to_s diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 1c5dc7c37..a0a38bf2e 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -420,6 +420,14 @@ module Vagrant error_key(:plugin_state_file_not_parsable) end + class RSyncError < VagrantError + error_key(:rsync_error) + end + + class RSyncNotFound < VagrantError + error_key(:rsync_not_found) + end + class SCPPermissionDenied < VagrantError error_key(:scp_permission_denied) end diff --git a/lib/vagrant/plugin/v2/synced_folder.rb b/lib/vagrant/plugin/v2/synced_folder.rb index 7904d47d3..cffd25626 100644 --- a/lib/vagrant/plugin/v2/synced_folder.rb +++ b/lib/vagrant/plugin/v2/synced_folder.rb @@ -7,8 +7,11 @@ module Vagrant # if this implementation can be used for this machine. This should # return true or false. # + # @param [Machine] machine + # @param [Boolean] raise_error If true, should raise an exception + # if it isn't usable. # @return [Boolean] - def usable?(machine) + def usable?(machine, raise_error=false) end # This is called before the machine is booted, allowing the diff --git a/plugins/synced_folders/nfs/synced_folder.rb b/plugins/synced_folders/nfs/synced_folder.rb index 1eabc5d82..d0757ddec 100644 --- a/plugins/synced_folders/nfs/synced_folder.rb +++ b/plugins/synced_folders/nfs/synced_folder.rb @@ -24,7 +24,7 @@ module VagrantPlugins @logger = Log4r::Logger.new("vagrant::synced_folders::nfs") end - def usable?(machine) + def usable?(machine, raise_error=false) # NFS is always available true end diff --git a/plugins/synced_folders/rsync/plugin.rb b/plugins/synced_folders/rsync/plugin.rb new file mode 100644 index 000000000..d74658c31 --- /dev/null +++ b/plugins/synced_folders/rsync/plugin.rb @@ -0,0 +1,18 @@ +require "vagrant" + +module VagrantPlugins + module SyncedFolderRSync + # This plugin implements synced folders via rsync. + class Plugin < Vagrant.plugin("2") + name "RSync synced folders" + description <<-EOF + The Rsync synced folder plugin will sync folders via rsync. + EOF + + synced_folder("rsync", 5) do + require_relative "synced_folder" + SyncedFolder + end + end + end +end diff --git a/plugins/synced_folders/rsync/synced_folder.rb b/plugins/synced_folders/rsync/synced_folder.rb new file mode 100644 index 000000000..a96a8678c --- /dev/null +++ b/plugins/synced_folders/rsync/synced_folder.rb @@ -0,0 +1,86 @@ +require "log4r" + +require "vagrant/util/subprocess" +require "vagrant/util/which" + +module VagrantPlugins + module SyncedFolderRSync + class SyncedFolder < Vagrant.plugin("2", :synced_folder) + include Vagrant::Util + + def initialize(*args) + super + + @logger = Log4r::Logger.new("vagrant::synced_folders::rsync") + end + + def usable?(machine, raise_error=false) + rsync_path = Which.which("rsync") + return true if rsync_path + return false if !raise_error + raise Vagrant::Errors::RSyncNotFound + end + + def prepare(machine, folders, opts) + # Nothing is necessary to do before VM boot. + end + + def enable(machine, folders, opts) + rootdir = machine.env.root_path.to_s + ssh_info = machine.ssh_info + + folders.each do |id, folder_opts| + rsync_single(ssh_info, rootdir, machine.ui, folder_opts) + end + end + + # rsync_single rsync's a single folder with the given options. + def rsync_single(ssh_info, rootdir, ui, opts) + # Folder info + guestpath = opts[:guestpath] + hostpath = opts[:hostpath] + + # Connection information + username = ssh_info[:username] + host = ssh_info[:host] + rsh = [ + "ssh -p #{ssh_info[:port]} -o StrictHostKeyChecking=no", + ssh_info[:private_key_path].map { |p| "-i '#{p}'" }, + ].flatten.join(" ") + + # Exclude some files by default, and any that might be configured + # by the user. + # TODO(mitchellh): allow the user to configure it + excludes = ['.vagrant/'] + + # Build up the actual command to execute + command = [ + "rsync", + "--verbose", + "--archive", + "-z", + excludes.map { |e| ["--exclude", e] }, + "-e", rsh, + hostpath, + "#{username}@#{host}:#{guestpath}" + ].flatten + + command_opts = {} + + # The working directory should be the root path + command_opts[:workdir] = rootdir + + ui.info(I18n.t( + "vagrant.rsync_folder", guestpath: guestpath, hostpath: hostpath)) + r = Vagrant::Util::Subprocess.execute(*(command + [command_opts])) + if r.exit_code != 0 + raise Vagrant::Errors::RSyncError, + command: command.join(" "), + guestpath: guestpath, + hostpath: hostpath, + stderr: r.stderr + end + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index a7bdb542c..0d0b23e9c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -80,6 +80,8 @@ en: %{names} provisioner_cleanup: |- Running cleanup tasks for '%{name}' provisioner... + rsync_folder: |- + Rsyncing folder: %{hostpath} => %{guestpath} ssh_exec_password: |- The machine you're attempting to SSH into is configured to use password-based authentication. Vagrant can't script entering the @@ -525,6 +527,17 @@ en: provisioner_flag_invalid: |- '%{name}' is not a known provisioner. Please specify a valid provisioner. + rsync_error: |- + There was an error when attempting to rsync a synced folder. + Please inspect the error message below for more info. + + Host path: %{hostpath} + Guest path: %{guestpath} + Command: %{command} + Error: %{stderr} + rsync_not_found: |- + "rsync" could not be found on your PATH. Make sure that rsync + is properly installed on your system and available on the PATH. scp_permission_denied: |- Failed to upload a file to the guest VM via SCP due to a permissions error. This is normally because the user running Vagrant doesn't have diff --git a/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb b/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb new file mode 100644 index 000000000..97a689a63 --- /dev/null +++ b/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb @@ -0,0 +1,100 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/synced_folders/rsync/synced_folder") + +describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + before do + machine.env.stub(host: host) + end + + describe "#usable?" do + it "is usable if rsync can be found" do + Vagrant::Util::Which.should_receive(:which).with("rsync").and_return(true) + expect(subject.usable?(machine)).to be_true + end + + it "is not usable if rsync cant be found" do + Vagrant::Util::Which.should_receive(:which).with("rsync").and_return(false) + expect(subject.usable?(machine)).to be_false + end + + it "raises an exception if asked to" do + Vagrant::Util::Which.should_receive(:which).with("rsync").and_return(false) + expect { subject.usable?(machine, true) }. + to raise_error(Vagrant::Errors::RSyncNotFound) + end + end + + describe "#enable" do + let(:ssh_info) { Object.new } + + before do + machine.stub(ssh_info: ssh_info) + end + + it "rsyncs each folder" do + folders = [ + [:one, {}], + [:two, {}], + ] + + folders.each do |_, opts| + subject.should_receive(:rsync_single). + with(ssh_info, machine.env.root_path.to_s, machine.ui, opts). + ordered + end + + subject.enable(machine, folders, {}) + end + end + + describe "#rsync_single" do + let(:result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } + + let(:ssh_info) {{ + private_key_path: [], + }} + let(:opts) { {} } + let(:root_path) { "foo" } + let(:ui) { machine.ui } + + before do + Vagrant::Util::Subprocess.stub(execute: result) + end + + it "doesn't raise an error if it succeeds" do + subject.rsync_single(ssh_info, root_path, ui, opts) + end + + it "raises an error if the exit code is non-zero" do + Vagrant::Util::Subprocess.stub( + execute: Vagrant::Util::Subprocess::Result.new(1, "", "")) + + expect {subject.rsync_single(ssh_info, root_path, ui, opts) }. + to raise_error(Vagrant::Errors::RSyncError) + end + + it "executes within the root path" do + Vagrant::Util::Subprocess.should_receive(:execute).with do |*args| + expect(args.last).to be_kind_of(Hash) + + opts = args.last + expect(opts[:workdir]).to eql(root_path) + end + + subject.rsync_single(ssh_info, root_path, ui, opts) + end + end +end diff --git a/test/unit/support/shared/action_synced_folders_context.rb b/test/unit/support/shared/action_synced_folders_context.rb index d63a0a038..b468cecce 100644 --- a/test/unit/support/shared/action_synced_folders_context.rb +++ b/test/unit/support/shared/action_synced_folders_context.rb @@ -6,7 +6,8 @@ shared_context "synced folder actions" do name end - define_method(:usable?) do |machine| + define_method(:usable?) do |machine, raise_error=false| + raise "#{name}: usable" if raise_error && !usable usable end end