Merge pull request #2816 from mitchellh/f-rsync-command

"vagrant rsync" non-primary command
This commit is contained in:
Mitchell Hashimoto 2014-01-13 11:55:35 -08:00
commit 54bb182525
17 changed files with 383 additions and 169 deletions

View File

@ -1,7 +1,11 @@
require 'vagrant/util/scoped_hash_override'
module Vagrant module Vagrant
module Action module Action
module Builtin module Builtin
module MixinSyncedFolders module MixinSyncedFolders
include Vagrant::Util::ScopedHashOverride
# This goes over all the registered synced folder types and returns # This goes over all the registered synced folder types and returns
# the highest priority implementation that is usable for this machine. # the highest priority implementation that is usable for this machine.
def default_synced_folder_type(machine, plugins) def default_synced_folder_type(machine, plugins)
@ -96,6 +100,13 @@ module Vagrant
folders.delete("") folders.delete("")
end end
# Apply the scoped hash overrides to get the options
folders.each do |impl_name, fs|
fs.each do |id, data|
fs[id] = scoped_hash_override(data, impl_name)
end
end
return folders return folders
end end
end end

View File

@ -1,7 +1,6 @@
require "log4r" require "log4r"
require 'vagrant/util/platform' require 'vagrant/util/platform'
require 'vagrant/util/scoped_hash_override'
require_relative "mixin_synced_folders" require_relative "mixin_synced_folders"
@ -12,7 +11,6 @@ module Vagrant
# the appropriate synced folder plugin. # the appropriate synced folder plugin.
class SyncedFolders class SyncedFolders
include MixinSyncedFolders include MixinSyncedFolders
include Vagrant::Util::ScopedHashOverride
def initialize(app, env) def initialize(app, env)
@app = app @app = app
@ -28,9 +26,6 @@ module Vagrant
fs.each do |id, data| fs.each do |id, data|
# Log every implementation and their paths # Log every implementation and their paths
@logger.info(" - #{id}: #{data[:hostpath]} => #{data[:guestpath]}") @logger.info(" - #{id}: #{data[:hostpath]} => #{data[:guestpath]}")
# Scope hash override
fs[id] = scoped_hash_override(data, impl_name)
end end
end end

View File

@ -324,7 +324,7 @@ module Vagrant
# Returns the state of this machine. The state is queried from the # Returns the state of this machine. The state is queried from the
# backing provider, so it can be any arbitrary symbol. # backing provider, so it can be any arbitrary symbol.
# #
# @return [Symbol] # @return [MachineState]
def state def state
result = @provider.state result = @provider.state
raise Errors::MachineStateInvalid if !result.is_a?(MachineState) raise Errors::MachineStateInvalid if !result.is_a?(MachineState)

View File

@ -0,0 +1,54 @@
require 'optparse'
require "vagrant/action/builtin/mixin_synced_folders"
require_relative "../helper"
module VagrantPlugins
module SyncedFolderRSync
module Command
class Rsync < Vagrant.plugin("2", :command)
include Vagrant::Action::Builtin::MixinSyncedFolders
def self.synopsis
"syncs rsync synced folders to remote machine"
end
def execute
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant rsync [vm-name]"
o.separator ""
end
# Parse the options and return if we don't have any target.
argv = parse_options(opts)
return if !argv
# Go through each machine and perform the rsync
error = false
with_target_vms(argv) do |machine|
if !machine.communicate.ready?
machine.ui.error(I18n.t("vagrant.rsync_communicator_not_ready"))
error = true
next
end
# Determine the rsync synced folders for this machine
folders = synced_folders(machine)[:rsync]
next if !folders || folders.empty?
# Get the SSH info for this machine so we can access it
ssh_info = machine.ssh_info
# Sync them!
folders.each do |id, folder_opts|
RsyncHelper.rsync_single(machine, ssh_info, folder_opts)
end
end
return error ? 1 : 0
end
end
end
end
end

View File

@ -0,0 +1,68 @@
require "vagrant/util/subprocess"
module VagrantPlugins
module SyncedFolderRSync
# This is a helper that abstracts out the functionality of rsyncing
# folders so that it can be called from anywhere.
class RsyncHelper
def self.rsync_single(machine, ssh_info, opts)
# Folder info
guestpath = opts[:guestpath]
hostpath = opts[:hostpath]
hostpath = File.expand_path(hostpath, machine.env.root_path)
# 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",
"--delete",
"-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

@ -9,6 +9,11 @@ module VagrantPlugins
The Rsync synced folder plugin will sync folders via rsync. The Rsync synced folder plugin will sync folders via rsync.
EOF EOF
command("rsync", primary: false) do
require_relative "command/rsync"
Command::Rsync
end
synced_folder("rsync", 5) do synced_folder("rsync", 5) do
require_relative "synced_folder" require_relative "synced_folder"
SyncedFolder SyncedFolder

View File

@ -3,6 +3,8 @@ require "log4r"
require "vagrant/util/subprocess" require "vagrant/util/subprocess"
require "vagrant/util/which" require "vagrant/util/which"
require_relative "helper"
module VagrantPlugins module VagrantPlugins
module SyncedFolderRSync module SyncedFolderRSync
class SyncedFolder < Vagrant.plugin("2", :synced_folder) class SyncedFolder < Vagrant.plugin("2", :synced_folder)
@ -33,65 +35,7 @@ module VagrantPlugins
end end
folders.each do |id, folder_opts| folders.each do |id, folder_opts|
rsync_single(machine, ssh_info, folder_opts) RsyncHelper.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 end

View File

@ -85,6 +85,9 @@ en:
%{names} %{names}
provisioner_cleanup: |- provisioner_cleanup: |-
Running cleanup tasks for '%{name}' provisioner... Running cleanup tasks for '%{name}' provisioner...
rsync_communicator_not_ready: |-
The machine is reporting that it is not ready for rsync to
communiate with it. Verify that this machine is properly running.
rsync_folder: |- rsync_folder: |-
Rsyncing folder: %{hostpath} => %{guestpath} Rsyncing folder: %{hostpath} => %{guestpath}
rsync_folder_excludes: " - Exclude: %{excludes}" rsync_folder_excludes: " - Exclude: %{excludes}"

View File

@ -31,6 +31,9 @@ end
# Vagrantfile anywhere, or at least this minimizes those chances. # Vagrantfile anywhere, or at least this minimizes those chances.
ENV["VAGRANT_CWD"] = Tempdir.new.path ENV["VAGRANT_CWD"] = Tempdir.new.path
# Set the dummy provider to the default for tests
ENV["VAGRANT_DEFAULT_PROVIDER"] = "dummy"
# Unset all host plugins so that we aren't executing subprocess things # Unset all host plugins so that we aren't executing subprocess things
# to detect a host for every test. # to detect a host for every test.
Vagrant.plugin("2").manager.registered.dup.each do |plugin| Vagrant.plugin("2").manager.registered.dup.each do |plugin|

View File

@ -0,0 +1,72 @@
require_relative "../../../../base"
require Vagrant.source_root.join("plugins/synced_folders/rsync/command/rsync")
describe VagrantPlugins::SyncedFolderRSync::Command::Rsync do
include_context "unit"
let(:argv) { [] }
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(:communicator) { double("comm") }
let(:synced_folders) { {} }
let(:helper_class) { VagrantPlugins::SyncedFolderRSync::RsyncHelper }
subject do
described_class.new(argv, iso_env).tap do |s|
s.stub(synced_folders: synced_folders)
end
end
before do
iso_env.machine_names.each do |name|
m = iso_env.machine(name, iso_env.default_provider)
m.stub(communicate: communicator)
end
end
describe "#execute" do
context "with a single machine" do
let(:ssh_info) {{
private_key_path: [],
}}
let(:machine) { iso_env.machine(iso_env.machine_names[0], iso_env.default_provider) }
before do
communicator.stub(ready?: true)
machine.stub(ssh_info: ssh_info)
synced_folders[:rsync] = [
[:one, {}],
[:two, {}],
]
end
it "doesn't sync if communicator isn't ready and exits with 1" do
communicator.stub(ready?: false)
helper_class.should_receive(:rsync_single).never
expect(subject.execute).to eql(1)
end
it "rsyncs each folder and exits successfully" do
synced_folders[:rsync].each do |_, opts|
helper_class.should_receive(:rsync_single).
with(machine, ssh_info, opts).
ordered
end
expect(subject.execute).to eql(0)
end
end
end
end

View File

@ -0,0 +1,130 @@
require_relative "../../../base"
require Vagrant.source_root.join("plugins/synced_folders/rsync/helper")
describe VagrantPlugins::SyncedFolderRSync::RsyncHelper 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) }
subject { described_class }
before do
machine.stub(guest: guest)
end
describe "#rsync_single" do
let(:result) { Vagrant::Util::Subprocess::Result.new(0, "", "") }
let(:ssh_info) {{
private_key_path: [],
}}
let(:opts) {{
hostpath: "/foo",
}}
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
context "host and guest paths" do
it "syncs the hostpath to the guest path" do
opts[:hostpath] = "/foo"
opts[:guestpath] = "/bar"
Vagrant::Util::Subprocess.should_receive(:execute).with do |*args|
expect(args[args.length - 3]).to eql("/foo")
expect(args[args.length - 2]).to include("/bar")
end
subject.rsync_single(machine, ssh_info, opts)
end
it "expands the hostpath relative to the root path" do
opts[:hostpath] = "foo"
opts[:guestpath] = "/bar"
hostpath_expanded = File.expand_path(opts[:hostpath], machine.env.root_path)
Vagrant::Util::Subprocess.should_receive(:execute).with do |*args|
expect(args[args.length - 3]).to eql(hostpath_expanded)
expect(args[args.length - 2]).to include("/bar")
end
subject.rsync_single(machine, ssh_info, opts)
end
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

@ -16,6 +16,8 @@ describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do
let(:host) { double("host") } let(:host) { double("host") }
let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) }
let(:helper_class) { VagrantPlugins::SyncedFolderRSync::RsyncHelper }
before do before do
machine.env.stub(host: host) machine.env.stub(host: host)
machine.stub(guest: guest) machine.stub(guest: guest)
@ -55,7 +57,7 @@ describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do
] ]
folders.each do |_, opts| folders.each do |_, opts|
subject.should_receive(:rsync_single). helper_class.should_receive(:rsync_single).
with(machine, ssh_info, opts). with(machine, ssh_info, opts).
ordered ordered
end end
@ -63,81 +65,4 @@ describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do
subject.enable(machine, folders, {}) subject.enable(machine, folders, {})
end end
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 end

View File

@ -98,5 +98,16 @@ describe Vagrant::Action::Builtin::MixinSyncedFolders do
result.length.should == 1 result.length.should == 1
result[:default].length.should == 1 result[:default].length.should == 1
end end
it "should scope hash override the settings" do
folders["root"] = {
hostpath: "foo",
type: "nfs",
nfs__foo: "bar",
}
result = subject.synced_folders(machine)
expect(result[:nfs]["root"][:foo]).to eql("bar")
end
end end
end end

View File

@ -90,27 +90,5 @@ describe Vagrant::Action::Builtin::SyncedFolders do
order.should == [:prepare, :enable] order.should == [:prepare, :enable]
end end
it "should scope hash override the settings" do
actual = nil
tracker = Class.new(impl(true, "good")) do
define_method(:prepare) do |machine, folders, opts|
actual = folders
end
end
plugins[:tracker] = [tracker, 15]
synced_folders["tracker"] = {
"root" => {
hostpath: "foo",
tracker__foo: "bar",
},
}
subject.call(env)
actual["root"][:foo].should == "bar"
end
end end
end end

View File

@ -17,3 +17,8 @@ Note that while you have to run a special command to list the non-primary
subcommands, you don't have to do anything special to actually _run_ the subcommands, you don't have to do anything special to actually _run_ the
non-primary subcommands. They're executed just like any other subcommand: non-primary subcommands. They're executed just like any other subcommand:
`vagrant COMMAND`. `vagrant COMMAND`.
The list of non-primary commands is below. Click on any command to learn
more about it.
* [rsync](/v2/cli/rsync.html)

View File

@ -0,0 +1,11 @@
---
page_title: "vagrant rsync - Command-Line Interface"
sidebar_current: "cli-rsync"
---
# Rsync
**Command: `vagrant rsync`**
This command forces a resync of any
[rsync synced folders](/v2/synced-folders/rsync.html).

View File

@ -44,7 +44,6 @@ end
## Re-Syncing ## Re-Syncing
The rsync sync is done only during a `vagrant up` or `vagrant reload`. It The rsync sync is done only during a `vagrant up` or `vagrant reload`. Vagrant
is not currently possible to force a re-sync in any way other than reloading. does not automatically listen for changes on the filesystem and resync them.
Resyncing can be forced with a call to [vagrant rsync](/v2/cli/rsync.html).
We plan on exposing a command to force a sync in a future version of Vagrant.