diff --git a/lib/vagrant/action/builtin/mixin_synced_folders.rb b/lib/vagrant/action/builtin/mixin_synced_folders.rb index 0710ca883..7ffa64d03 100644 --- a/lib/vagrant/action/builtin/mixin_synced_folders.rb +++ b/lib/vagrant/action/builtin/mixin_synced_folders.rb @@ -1,7 +1,11 @@ +require 'vagrant/util/scoped_hash_override' + module Vagrant module Action module Builtin module MixinSyncedFolders + include Vagrant::Util::ScopedHashOverride + # This goes over all the registered synced folder types and returns # the highest priority implementation that is usable for this machine. def default_synced_folder_type(machine, plugins) @@ -96,6 +100,13 @@ module Vagrant folders.delete("") 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 end end diff --git a/lib/vagrant/action/builtin/synced_folders.rb b/lib/vagrant/action/builtin/synced_folders.rb index 7c7802ca2..ce6a1cc85 100644 --- a/lib/vagrant/action/builtin/synced_folders.rb +++ b/lib/vagrant/action/builtin/synced_folders.rb @@ -1,7 +1,6 @@ require "log4r" require 'vagrant/util/platform' -require 'vagrant/util/scoped_hash_override' require_relative "mixin_synced_folders" @@ -12,7 +11,6 @@ module Vagrant # the appropriate synced folder plugin. class SyncedFolders include MixinSyncedFolders - include Vagrant::Util::ScopedHashOverride def initialize(app, env) @app = app @@ -28,9 +26,6 @@ module Vagrant fs.each do |id, data| # Log every implementation and their paths @logger.info(" - #{id}: #{data[:hostpath]} => #{data[:guestpath]}") - - # Scope hash override - fs[id] = scoped_hash_override(data, impl_name) end end diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 1f2329d91..2ed27db6b 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -324,7 +324,7 @@ module Vagrant # Returns the state of this machine. The state is queried from the # backing provider, so it can be any arbitrary symbol. # - # @return [Symbol] + # @return [MachineState] def state result = @provider.state raise Errors::MachineStateInvalid if !result.is_a?(MachineState) diff --git a/plugins/synced_folders/rsync/command/rsync.rb b/plugins/synced_folders/rsync/command/rsync.rb new file mode 100644 index 000000000..6703ffaaa --- /dev/null +++ b/plugins/synced_folders/rsync/command/rsync.rb @@ -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 diff --git a/plugins/synced_folders/rsync/helper.rb b/plugins/synced_folders/rsync/helper.rb new file mode 100644 index 000000000..9627801e4 --- /dev/null +++ b/plugins/synced_folders/rsync/helper.rb @@ -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 diff --git a/plugins/synced_folders/rsync/plugin.rb b/plugins/synced_folders/rsync/plugin.rb index d74658c31..a39bccc68 100644 --- a/plugins/synced_folders/rsync/plugin.rb +++ b/plugins/synced_folders/rsync/plugin.rb @@ -9,6 +9,11 @@ module VagrantPlugins The Rsync synced folder plugin will sync folders via rsync. EOF + command("rsync", primary: false) do + require_relative "command/rsync" + Command::Rsync + end + synced_folder("rsync", 5) do require_relative "synced_folder" SyncedFolder diff --git a/plugins/synced_folders/rsync/synced_folder.rb b/plugins/synced_folders/rsync/synced_folder.rb index 87413afa9..416a1b06c 100644 --- a/plugins/synced_folders/rsync/synced_folder.rb +++ b/plugins/synced_folders/rsync/synced_folder.rb @@ -3,6 +3,8 @@ require "log4r" require "vagrant/util/subprocess" require "vagrant/util/which" +require_relative "helper" + module VagrantPlugins module SyncedFolderRSync class SyncedFolder < Vagrant.plugin("2", :synced_folder) @@ -33,65 +35,7 @@ module VagrantPlugins end 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 + RsyncHelper.rsync_single(machine, ssh_info, folder_opts) end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 0d41d8e23..5eb503e8d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -85,6 +85,9 @@ en: %{names} provisioner_cleanup: |- 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: |- Rsyncing folder: %{hostpath} => %{guestpath} rsync_folder_excludes: " - Exclude: %{excludes}" diff --git a/test/unit/base.rb b/test/unit/base.rb index 0934e1b9d..ea5c12f65 100644 --- a/test/unit/base.rb +++ b/test/unit/base.rb @@ -31,6 +31,9 @@ end # Vagrantfile anywhere, or at least this minimizes those chances. 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 # to detect a host for every test. Vagrant.plugin("2").manager.registered.dup.each do |plugin| diff --git a/test/unit/plugins/synced_folders/rsync/command/rsync_test.rb b/test/unit/plugins/synced_folders/rsync/command/rsync_test.rb new file mode 100644 index 000000000..7f0cc50e8 --- /dev/null +++ b/test/unit/plugins/synced_folders/rsync/command/rsync_test.rb @@ -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 diff --git a/test/unit/plugins/synced_folders/rsync/helper_test.rb b/test/unit/plugins/synced_folders/rsync/helper_test.rb new file mode 100644 index 000000000..d846558ca --- /dev/null +++ b/test/unit/plugins/synced_folders/rsync/helper_test.rb @@ -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 diff --git a/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb b/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb index 0c2aba551..2b7df8781 100644 --- a/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb +++ b/test/unit/plugins/synced_folders/rsync/synced_folder_test.rb @@ -16,6 +16,8 @@ describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do let(:host) { double("host") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + let(:helper_class) { VagrantPlugins::SyncedFolderRSync::RsyncHelper } + before do machine.env.stub(host: host) machine.stub(guest: guest) @@ -55,7 +57,7 @@ describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do ] folders.each do |_, opts| - subject.should_receive(:rsync_single). + helper_class.should_receive(:rsync_single). with(machine, ssh_info, opts). ordered end @@ -63,81 +65,4 @@ describe VagrantPlugins::SyncedFolderRSync::SyncedFolder do 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 diff --git a/test/unit/vagrant/action/builtin/mixin_synced_folders_test.rb b/test/unit/vagrant/action/builtin/mixin_synced_folders_test.rb index 97b64b168..6d96990f7 100644 --- a/test/unit/vagrant/action/builtin/mixin_synced_folders_test.rb +++ b/test/unit/vagrant/action/builtin/mixin_synced_folders_test.rb @@ -98,5 +98,16 @@ describe Vagrant::Action::Builtin::MixinSyncedFolders do result.length.should == 1 result[:default].length.should == 1 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 diff --git a/test/unit/vagrant/action/builtin/synced_folders_test.rb b/test/unit/vagrant/action/builtin/synced_folders_test.rb index e09ae181b..23c77f2c9 100644 --- a/test/unit/vagrant/action/builtin/synced_folders_test.rb +++ b/test/unit/vagrant/action/builtin/synced_folders_test.rb @@ -90,27 +90,5 @@ describe Vagrant::Action::Builtin::SyncedFolders do order.should == [:prepare, :enable] 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 diff --git a/website/docs/source/v2/cli/non-primary.html.md b/website/docs/source/v2/cli/non-primary.html.md index 76b3cd63c..0dd43539b 100644 --- a/website/docs/source/v2/cli/non-primary.html.md +++ b/website/docs/source/v2/cli/non-primary.html.md @@ -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 non-primary subcommands. They're executed just like any other subcommand: `vagrant COMMAND`. + +The list of non-primary commands is below. Click on any command to learn +more about it. + +* [rsync](/v2/cli/rsync.html) diff --git a/website/docs/source/v2/cli/rsync.html.md b/website/docs/source/v2/cli/rsync.html.md new file mode 100644 index 000000000..7d8f420ba --- /dev/null +++ b/website/docs/source/v2/cli/rsync.html.md @@ -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). diff --git a/website/docs/source/v2/synced-folders/rsync.html.md b/website/docs/source/v2/synced-folders/rsync.html.md index 8963d2c56..8839df37e 100644 --- a/website/docs/source/v2/synced-folders/rsync.html.md +++ b/website/docs/source/v2/synced-folders/rsync.html.md @@ -44,7 +44,6 @@ end ## 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. +The rsync sync is done only during a `vagrant up` or `vagrant reload`. Vagrant +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).