Merge pull request #2803 from mitchellh/f-rsync

rsync Synced Folder Type

This pull requests introduces the `rsync` synced folder type into Vagrant core. This synced folder will do a one-time one-directional sync from the host machine to the guest machine using rsync. This is useful in situations where NFS or native shared folders can't be setup, such as in AWS.

Pretty easy to use: `config.vm.synced_folder ".", "/vagrant", type: "rsync"` No additional configuration necessary, though additional options are available.

In the future, I'd like to add the ability to force a re-sync, as well as to watch for file changes and sync. For now, this is a basic one-time sync.

Having this in core allows remote providers such as OpenStack, AWS, RackSpace, etc. to stop hand-rolling the rsync synced folder mechanism and to rely on the core providing it.

And a shout out to @phinze because this thing is fully unit tested.

Fixes #1926
This commit is contained in:
Mitchell Hashimoto 2014-01-10 18:22:40 -08:00
commit 84108bee9a
13 changed files with 357 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,16 @@
module VagrantPlugins
module GuestLinux
module Cap
class RSync
def self.rsync_pre(machine, folder_opts)
username = machine.ssh_info[:username]
machine.communicate.tap do |comm|
comm.sudo("mkdir -p '#{folder_opts[:guestpath]}'")
comm.sudo("chown -R #{username} '#{folder_opts[:guestpath]}'")
end
end
end
end
end
end

View File

@ -45,6 +45,12 @@ module VagrantPlugins
require_relative "cap/read_ip_address"
Cap::ReadIPAddress
end
# RSync synced folders
guest_capability("linux", "rsync_pre") do
require_relative "cap/rsync"
Cap::RSync
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,95 @@
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)
ssh_info = machine.ssh_info
folders.each do |id, folder_opts|
rsync_single(machine, ssh_info, folder_opts)
end
end
# rsync_single rsync's a single folder with the given options.
def rsync_single(machine, ssh_info, 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.
excludes = ['.vagrant/']
excludes += Array(opts[:exclude]).map(&:to_s) if opts[:exclude]
excludes.uniq!
# 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] = machine.env.root_path.to_s
machine.ui.info(I18n.t(
"vagrant.rsync_folder", guestpath: guestpath, hostpath: hostpath))
if excludes.length > 1
machine.ui.info(I18n.t(
"vagrant.rsync_folder_excludes", excludes: excludes.inspect))
end
# If we have tasks to do before rsyncing, do those.
if machine.guest.capability?(:rsync_pre)
machine.guest.capability(:rsync_pre, opts)
end
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

View File

@ -80,6 +80,9 @@ en:
%{names}
provisioner_cleanup: |-
Running cleanup tasks for '%{name}' provisioner...
rsync_folder: |-
Rsyncing folder: %{hostpath} => %{guestpath}
rsync_folder_excludes: " - Exclude: %{excludes}"
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 +528,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

View File

@ -0,0 +1,141 @@
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(:guest) { double("guest") }
let(:host) { double("host") }
let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) }
before do
machine.env.stub(host: host)
machine.stub(guest: guest)
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(machine, ssh_info, 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(:ui) { machine.ui }
before do
Vagrant::Util::Subprocess.stub(execute: result)
guest.stub(capability?: false)
end
it "doesn't raise an error if it succeeds" do
subject.rsync_single(machine, ssh_info, 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(machine, ssh_info, 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(machine.env.root_path.to_s)
end
subject.rsync_single(machine, ssh_info, opts)
end
it "executes the rsync_pre capability first if it exists" do
guest.should_receive(:capability?).with(:rsync_pre).and_return(true)
guest.should_receive(:capability).with(:rsync_pre, opts).ordered
Vagrant::Util::Subprocess.should_receive(:execute).ordered.and_return(result)
subject.rsync_single(machine, ssh_info, opts)
end
context "excluding files" do
it "excludes files if given as a string" do
opts[:exclude] = "foo"
Vagrant::Util::Subprocess.should_receive(:execute).with do |*args|
index = args.find_index("foo")
expect(index).to be > 0
expect(args[index-1]).to eql("--exclude")
end
subject.rsync_single(machine, ssh_info, opts)
end
it "excludes multiple files" do
opts[:exclude] = ["foo", "bar"]
Vagrant::Util::Subprocess.should_receive(:execute).with do |*args|
index = args.find_index("foo")
expect(index).to be > 0
expect(args[index-1]).to eql("--exclude")
index = args.find_index("bar")
expect(index).to be > 0
expect(args[index-1]).to eql("--exclude")
end
subject.rsync_single(machine, ssh_info, opts)
end
end
end
end

View File

@ -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

View File

@ -173,6 +173,7 @@
<ul class="sub unstyled">
<li<%= sidebar_current("syncedfolder-basic") %>><a href="/v2/synced-folders/basic_usage.html">Basic Usage</a></li>
<li<%= sidebar_current("syncedfolder-nfs") %>><a href="/v2/synced-folders/nfs.html">NFS</a></li>
<li<%= sidebar_current("syncedfolder-rsync") %>><a href="/v2/synced-folders/rsync.html">RSync</a></li>
<li<%= sidebar_current("syncedfolder-virtualbox") %>><a href="/v2/synced-folders/virtualbox.html">VirtualBox</a></li>
</ul>
<% end %>

View File

@ -0,0 +1,50 @@
---
page_title: "RSync - Synced Folders"
sidebar_current: "syncedfolder-rsync"
---
# RSync
**Synced folder type:** `rsync`
Vagrant can use [rsync](http://en.wikipedia.org/wiki/Rsync) as a mechanism
to sync a folder to the guest machine. This synced folder type is useful
primarily in situations where other synced folder mechanisms are not available,
such as when NFS or VirtualBox shared folders aren't available in the guest
machine.
The rsync synced folder does a one-time one-way sync from the machine running
to the machine being started by Vagrant.
## Prerequisites
To use the rsync synced folder type, the machine running Vagrant must have
`rsync` (or `rsync.exe`) on the path. This executable is expected to behave
like the standard rsync tool.
## Options
The rsync synced folder type accepts the following options:
* `rsync__exclude` (string or array of strings) - A list of files or directories
to exclude from the sync. The values can be any acceptable rsync exclude
pattern. By default, the ".vagrant/" directory is excluded. We recommend
excluding revision control directories such as ".git/" as well.
## Example
The following is an example of using RSync to sync a folder:
<pre class="prettyprint">
Vagrant.configure("2") do |config|
config.vm.synced_folder ".", "/vagrant", type: "rsync",
rsync__exclude: ".git/"
end
</pre>
## Re-Syncing
The rsync sync is done only during a `vagrant up` or `vagrant reload`. It
is not currently possible to force a re-sync in any way other than reloading.
We plan on exposing a command to force a sync in a future version of Vagrant.