Merge pull request #2561 from emyl/synced-folder-cleanup-v2

core: Enhance the synced folder plugin implementation with a cleanup routine
This commit is contained in:
Mitchell Hashimoto 2013-12-03 18:08:42 -08:00
commit ca521887eb
7 changed files with 277 additions and 95 deletions

View File

@ -25,6 +25,7 @@ module Vagrant
autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHExec, "vagrant/action/builtin/ssh_exec"
autoload :SSHRun, "vagrant/action/builtin/ssh_run" autoload :SSHRun, "vagrant/action/builtin/ssh_run"
autoload :SyncedFolders, "vagrant/action/builtin/synced_folders" autoload :SyncedFolders, "vagrant/action/builtin/synced_folders"
autoload :SyncedFolderCleanup, "vagrant/action/builtin/synced_folder_cleanup"
autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator" autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator"
end end

View File

@ -0,0 +1,102 @@
module Vagrant
module Action
module Builtin
module MixinSyncedFolders
# 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)
ordered = []
# First turn the plugins into an array
plugins.each do |key, data|
impl = data[0]
priority = data[1]
ordered << [priority, key, impl]
end
# Order the plugins by priority
ordered = ordered.sort { |a, b| b[0] <=> a[0] }
# Find the proper implementation
ordered.each do |_, key, impl|
return key if impl.new.usable?(machine)
end
return nil
end
# This finds the options in the env that are set for a given
# synced folder type.
def impl_opts(name, env)
{}.tap do |result|
env.each do |k, v|
if k.to_s.start_with?("#{name}_")
k = k.dup if !k.is_a?(Symbol)
v = v.dup if !v.is_a?(Symbol)
result[k] = v
end
end
end
end
# This returns the available synced folder implementations. This
# is a separate method so that it can be easily stubbed by tests.
def plugins
@plugins ||= Vagrant.plugin("2").manager.synced_folders
end
# This returns the set of shared folders that should be done for
# this machine. It returns the folders in a hash keyed by the
# implementation class for the synced folders.
def synced_folders(machine)
folders = {}
# Determine all the synced folders as well as the implementation
# they're going to use.
machine.config.vm.synced_folders.each do |id, data|
# Ignore disabled synced folders
next if data[:disabled]
impl = ""
impl = data[:type].to_sym if data[:type]
if impl != ""
impl_class = plugins[impl]
if !impl_class
# This should never happen because configuration validation
# should catch this case. But we put this here as an assert
raise "Internal error. Report this as a bug. Invalid: #{data[:type]}"
end
if !impl_class[0].new.usable?(machine)
# Verify that explicitly defined shared folder types are
# actually usable.
raise Errors::SyncedFolderUnusable, type: data[:type].to_s
end
end
# Keep track of this shared folder by the implementation.
folders[impl] ||= {}
folders[impl][id] = data.dup
end
# If we have folders with the "default" key, then determine the
# most appropriate implementation for this.
if folders.has_key?("") && !folders[""].empty?
default_impl = default_synced_folder_type(machine, plugins)
if !default_impl
types = plugins.to_hash.keys.map { |t| t.to_s }.sort.join(", ")
raise Errors::NoDefaultSyncedFolderImpl, types: types
end
folders[default_impl] = folders[""]
folders.delete("")
end
return folders
end
end
end
end
end

View File

@ -0,0 +1,32 @@
require "log4r"
require_relative "mixin_synced_folders"
module Vagrant
module Action
module Builtin
# This middleware will run cleanup tasks for synced folders using
# the appropriate synced folder plugin.
class SyncedFolderCleanup
include MixinSyncedFolders
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::action::builtin::synced_folder_cleanup")
end
def call(env)
folders = synced_folders(env[:machine])
# Go through each folder and do cleanup
folders.each_key do |impl_name|
@logger.info("Invoking synced folder cleanup for: #{impl_name}")
plugins[impl_name.to_sym][0].new.cleanup(env)
end
@app.call(env)
end
end
end
end
end

View File

@ -3,12 +3,15 @@ require "log4r"
require 'vagrant/util/platform' require 'vagrant/util/platform'
require 'vagrant/util/scoped_hash_override' require 'vagrant/util/scoped_hash_override'
require_relative "mixin_synced_folders"
module Vagrant module Vagrant
module Action module Action
module Builtin module Builtin
# This middleware will setup the synced folders for the machine using # This middleware will setup the synced folders for the machine using
# the appropriate synced folder plugin. # the appropriate synced folder plugin.
class SyncedFolders class SyncedFolders
include MixinSyncedFolders
include Vagrant::Util::ScopedHashOverride include Vagrant::Util::ScopedHashOverride
def initialize(app, env) def initialize(app, env)
@ -70,101 +73,6 @@ module Vagrant
plugins[impl_name.to_sym][0].new.enable(env[:machine], fs, impl_opts(impl_name, env)) plugins[impl_name.to_sym][0].new.enable(env[:machine], fs, impl_opts(impl_name, env))
end end
end end
# 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)
ordered = []
# First turn the plugins into an array
plugins.each do |key, data|
impl = data[0]
priority = data[1]
ordered << [priority, key, impl]
end
# Order the plugins by priority
ordered = ordered.sort { |a, b| b[0] <=> a[0] }
# Find the proper implementation
ordered.each do |_, key, impl|
return key if impl.new.usable?(machine)
end
return nil
end
# This finds the options in the env that are set for a given
# synced folder type.
def impl_opts(name, env)
{}.tap do |result|
env.each do |k, v|
if k.to_s.start_with?("#{name}_")
k = k.dup if !k.is_a?(Symbol)
v = v.dup if !v.is_a?(Symbol)
result[k] = v
end
end
end
end
# This returns the available synced folder implementations. This
# is a separate method so that it can be easily stubbed by tests.
def plugins
@plugins ||= Vagrant.plugin("2").manager.synced_folders
end
# This returns the set of shared folders that should be done for
# this machine. It returns the folders in a hash keyed by the
# implementation class for the synced folders.
def synced_folders(machine)
folders = {}
# Determine all the synced folders as well as the implementation
# they're going to use.
machine.config.vm.synced_folders.each do |id, data|
# Ignore disabled synced folders
next if data[:disabled]
impl = ""
impl = data[:type].to_sym if data[:type]
if impl != ""
impl_class = plugins[impl]
if !impl_class
# This should never happen because configuration validation
# should catch this case. But we put this here as an assert
raise "Internal error. Report this as a bug. Invalid: #{data[:type]}"
end
if !impl_class[0].new.usable?(machine)
# Verify that explicitly defined shared folder types are
# actually usable.
raise Errors::SyncedFolderUnusable, type: data[:type].to_s
end
end
# Keep track of this shared folder by the implementation.
folders[impl] ||= {}
folders[impl][id] = data.dup
end
# If we have folders with the "default" key, then determine the
# most appropriate implementation for this.
if folders.has_key?("") && !folders[""].empty?
default_impl = default_synced_folder_type(machine, plugins)
if !default_impl
types = plugins.to_hash.keys.map { |t| t.to_s }.sort.join(", ")
raise Errors::NoDefaultSyncedFolderImpl, types: types
end
folders[default_impl] = folders[""]
folders.delete("")
end
return folders
end
end end
end end
end end

View File

@ -11,6 +11,9 @@ module Vagrant
def enable(machine, folders, opts) def enable(machine, folders, opts)
end end
def cleanup(machine)
end
end end
end end
end end

View File

@ -60,6 +60,7 @@ module VagrantPlugins
b.use HandleForwardedPortCollisions b.use HandleForwardedPortCollisions
b.use PruneNFSExports b.use PruneNFSExports
b.use ClearSharedFolders b.use ClearSharedFolders
b.use SyncedFolderCleanup
b.use SyncedFolders b.use SyncedFolders
b.use PrepareNFSSettings b.use PrepareNFSSettings
b.use ClearNetworkInterfaces b.use ClearNetworkInterfaces

View File

@ -0,0 +1,135 @@
require "pathname"
require "tmpdir"
require File.expand_path("../../../../base", __FILE__)
describe Vagrant::Action::Builtin::SyncedFolderCleanup do
let(:app) { lambda { |env| } }
let(:env) { { :machine => machine, :ui => ui } }
let(:machine) do
double("machine").tap do |machine|
machine.stub(:config).and_return(machine_config)
end
end
let(:machine_config) do
double("machine_config").tap do |top_config|
top_config.stub(:vm => vm_config)
end
end
let(:vm_config) { double("machine_vm_config") }
let(:ui) do
double("ui").tap do |result|
result.stub(:info)
end
end
subject { described_class.new(app, env) }
# This creates a synced folder implementation.
def impl(usable, name)
Class.new(Vagrant.plugin("2", :synced_folder)) do
define_method(:name) do
name
end
define_method(:usable?) do |machine|
usable
end
end
end
describe "call" do
let(:synced_folders) { {} }
let(:plugins) { {} }
before do
plugins[:default] = [impl(true, "default"), 10]
plugins[:nfs] = [impl(true, "nfs"), 5]
env[:root_path] = Pathname.new(Dir.mktmpdir)
subject.stub(:plugins => plugins)
subject.stub(:synced_folders => synced_folders)
end
it "should invoke cleanup" do
cleaned_up = nil
tracker = Class.new(impl(true, "good")) do
define_method(:cleanup) do |machine|
cleaned_up = true
end
end
plugins[:tracker] = [tracker, 15]
synced_folders["tracker"] = {
"root" => {
hostpath: "foo",
},
"other" => {
hostpath: "bar",
create: true,
}
}
subject.call(env)
cleaned_up.should be_true
end
it "should invoke cleanup once per implementation" do
call_count = 0
trackers = []
(0..2).each do |tracker|
trackers << Class.new(impl(true, "good")) do
define_method(:cleanup) do |machine|
call_count += 1
end
end
end
plugins[:tracker_0] = [trackers[0], 15]
plugins[:tracker_1] = [trackers[1], 15]
plugins[:tracker_2] = [trackers[2], 15]
synced_folders["tracker_0"] = {
"root" => {
hostpath: "foo"
},
"other" => {
hostpath: "bar",
create: true
}
}
synced_folders["tracker_1"] = {
"root" => {
hostpath: "foo"
}
}
synced_folders["tracker_2"] = {
"root" => {
hostpath: "foo"
},
"other" => {
hostpath: "bar",
create: true
},
"another" => {
hostpath: "baz"
}
}
subject.call(env)
call_count.should == 3
end
end
end