core: Vagrant::CapabilityHost is a module for adding capabilities to

things
This commit is contained in:
Mitchell Hashimoto 2014-01-07 16:12:12 -08:00
parent 72dd32e69c
commit 1f6095f912
5 changed files with 373 additions and 118 deletions

View File

@ -0,0 +1,175 @@
module Vagrant
# This module enables a class to host capabilities. Prior to being able
# to use any capabilities, the `initialize_capabilities!` method must be
# called.
#
# Capabilities allow small pieces of functionality to be plugged in using
# the Vagrant plugin model. Capabilities even allow for a certain amount
# of inheritence, where only a subset of capabilities may be implemented but
# a parent implements the rest.
#
# Capabilities are used heavily in Vagrant for host/guest interactions. For
# example, "mount_nfs_folder" is a guest-OS specific operation, so capabilities
# defer these operations to the guest.
module CapabilityHost
# Initializes the capability system by detecting the proper capability
# host to execute on and building the chain of capabilities to execute.
#
# @param [Symbol] host The host to use for the capabilities, or nil if
# we should auto-detect it.
# @param [Hash<Symbol, Array<Class, Symbol>>] hosts Potential capability
# hosts. The key is the name of the host, value[0] is a class that
# implements `#detect?` and value[1] is a parent host (if any).
# @param [Hash<Symbol, Hash<Symbol, Class>>] capabilities The capabilities
# that are supported. The key is the host of the capability. Within that
# is a hash where the key is the name of the capability and the value
# is the class/module implementing it.
def initialize_capabilities!(host, hosts, capabilities, *args)
@cap_logger = Log4r::Logger.new("vagrant::capability_host::#{self.class}")
if host && !hosts[host]
raise Errors::CapabilityHostExplicitNotDetected, value: host.to_s
end
if !host
host = autodetect_capability_host(hosts) if !host
raise Errors::CapabilityHostNotDetected if !host
end
if !hosts[host]
# This should never happen because the autodetect above uses the
# hosts hash to look up hosts. And if an explicit host is specified,
# we do another check higher up.
raise "Internal error. Host not found: #{host}"
end
name = host
host_info = hosts[name]
host = host_info[0].new
chain = []
chain << [name, host]
# Build the proper chain of parents if there are any.
# This allows us to do "inheritance" of capabilities later
if host_info[1]
parent_name = host_info[1]
parent_info = hosts[parent_name]
while parent_info
chain << [parent_name, parent_info[0].new]
parent_name = parent_info[1]
parent_info = hosts[parent_name]
end
end
@cap_host_chain = chain
@cap_args = args
@cap_caps = capabilities
true
end
# Returns the chain of hosts that will be checked for capabilities.
#
# @return [Array<Array<Symbol, Class>>]
def capability_host_chain
@cap_host_chain
end
# Tests whether the given capability is possible.
#
# @param [Symbol] cap_name Capability name
# @return [Boolean]
def capability?(cap_name)
!capability_module(cap_name.to_sym).nil?
end
# Executes the capability with the given name, optionally passing more
# arguments onwards to the capability. If the capability returns a value,
# it will be returned.
#
# @param [Symbol] cap_name Name of the capability
def capability(cap_name, *args)
@cap_logger.info("Execute capability: #{cap_name} (#{@cap_host_chain[0][0]})")
cap_mod = capability_module(cap_name.to_sym)
if !cap_mod
raise Errors::CapabilityNotFound,
:cap => cap_name.to_s,
:host => @cap_host_chain[0][0].to_s
end
cap_method = nil
begin
cap_method = cap_mod.method(cap_name)
rescue NameError
raise Errors::CapabilityInvalid,
:cap => cap_name.to_s,
:host => @cap_host_chain[0][0].to_s
end
args = @cap_args + args
cap_method.call(*args)
end
protected
def autodetect_capability_host(hosts, *args)
@cap_logger.info("Autodetecting guest for machine: #{@machine}")
# Get the mapping of hosts with the most parents. We start searching
# with the hosts with the most parents first.
parent_count = {}
hosts.each do |name, parts|
parent_count[name] = 0
parent = parts[1]
while parent
parent_count[name] += 1
parent = hosts[parent]
parent = parent[1] if parent
end
end
# Now swap around the mapping so that it is a mapping of
# count to the actual list of host names
parent_count_to_hosts = {}
parent_count.each do |name, count|
parent_count_to_hosts[count] ||= []
parent_count_to_hosts[count] << name
end
sorted_counts = parent_count_to_hosts.keys.sort.reverse
sorted_counts.each do |count|
parent_count_to_hosts[count].each do |name|
@cap_logger.debug("Trying: #{name}")
host_info = hosts[name]
host = host_info[0].new
if host.detect?(*args)
@cap_logger.info("Detected: #{name}!")
return name
end
end
end
return nil
end
# Returns the registered module for a capability with the given name.
#
# @param [Symbol] cap_name
# @return [Module]
def capability_module(cap_name)
@cap_logger.debug("Searching for cap: #{cap_name}")
@cap_host_chain.each do |host_name, host|
@cap_logger.debug("Checking in: #{host_name}")
caps = @cap_caps[host_name]
if caps && caps.has_key?(cap_name)
@cap_logger.debug("Found cap: #{cap_name} in #{host_name}")
return caps[cap_name]
end
end
nil
end
end
end

View File

@ -172,6 +172,22 @@ module Vagrant
error_key(:bundler_error) error_key(:bundler_error)
end end
class CapabilityHostExplicitNotDetected < VagrantError
error_key(:capability_host_explicit_not_detected)
end
class CapabilityHostNotDetected < VagrantError
error_key(:capability_host_not_detected)
end
class CapabilityInvalid < VagrantError
error_key(:capability_invalid)
end
class CapabilityNotFound < VagrantError
error_key(:capability_not_found)
end
class CFEngineBootstrapFailed < VagrantError class CFEngineBootstrapFailed < VagrantError
error_key(:cfengine_bootstrap_failed) error_key(:cfengine_bootstrap_failed)
end end

View File

@ -261,6 +261,22 @@ en:
issues. The error from Bundler is: issues. The error from Bundler is:
%{message} %{message}
capability_host_explicit_not_detected: |-
The explicit capability host specified of '%{value}' could not be
found.
This is an internal error that users should never see. Please report
a bug.
capability_host_not_detected: |-
The capability host could not be detected. This is an internal error
that users should never see. Please report a bug.
capability_invalid: |-
The capability '%{cap}' is invalid. This is an internal error that
users should never see. Please report a bug.
capability_not_found: |-
The capability '%{cap}' could not be found. This is an internal error
that users should never see. Please report a bug.
cfengine_bootstrap_failed: |- cfengine_bootstrap_failed: |-
Failed to bootstrap CFEngine. Please see the output above to Failed to bootstrap CFEngine. Please see the output above to
see what went wrong and address the issue. see what went wrong and address the issue.

View File

@ -0,0 +1,166 @@
require File.expand_path("../../base", __FILE__)
require "vagrant/capability_host"
describe Vagrant::CapabilityHost do
subject do
Class.new do
extend Vagrant::CapabilityHost
end
end
def detect_class(result)
Class.new do
define_method(:detect?) do
result
end
end
end
def cap_instance(name, options=nil)
options ||= {}
Class.new do
if !options[:corrupt]
define_method(name) do |*args|
raise "cap: #{name} #{args.inspect}"
end
end
end.new
end
describe "#initialize_capabilities! and #capability_host_chain" do
it "raises an error if an explicit host is not found" do
expect { subject.initialize_capabilities!(:foo, {}, {}) }.
to raise_error(Vagrant::Errors::CapabilityHostExplicitNotDetected)
end
it "raises an error if a host can't be detected" do
hosts = {
foo: [detect_class(false), nil],
bar: [detect_class(false), :foo],
}
expect { subject.initialize_capabilities!(nil, hosts, {}) }.
to raise_error(Vagrant::Errors::CapabilityHostNotDetected)
end
it "detects a basic child" do
hosts = {
foo: [detect_class(false), nil],
bar: [detect_class(true), nil],
baz: [detect_class(false), nil],
}
subject.initialize_capabilities!(nil, hosts, {})
chain = subject.capability_host_chain
expect(chain.length).to eql(1)
expect(chain[0][0]).to eql(:bar)
end
it "detects the host with the most parents (deepest) first" do
hosts = {
foo: [detect_class(true), nil],
bar: [detect_class(true), :foo],
baz: [detect_class(true), :bar],
foo2: [detect_class(true), nil],
bar2: [detect_class(true), :foo2],
}
subject.initialize_capabilities!(nil, hosts, {})
chain = subject.capability_host_chain
expect(chain.length).to eql(3)
expect(chain.map(&:first)).to eql([:baz, :bar, :foo])
end
it "detects a forced host" do
hosts = {
foo: [detect_class(false), nil],
bar: [detect_class(false), nil],
baz: [detect_class(false), nil],
}
subject.initialize_capabilities!(:bar, hosts, {})
chain = subject.capability_host_chain
expect(chain.length).to eql(1)
expect(chain[0][0]).to eql(:bar)
end
end
describe "#capability?" do
before do
host = nil
hosts = {
foo: [detect_class(true), nil],
bar: [detect_class(true), :foo],
}
caps = {
foo: { parent: Class.new },
bar: { self: Class.new },
}
subject.initialize_capabilities!(host, hosts, caps)
end
it "does not have a non-existent capability" do
expect(subject.capability?(:foo)).to be_false
end
it "has capabilities of itself" do
expect(subject.capability?(:self)).to be_true
end
it "has capabilities of parent" do
expect(subject.capability?(:parent)).to be_true
end
end
describe "capability" do
let(:caps) { {} }
def init
host = nil
hosts = {
foo: [detect_class(true), nil],
bar: [detect_class(true), :foo],
}
subject.initialize_capabilities!(host, hosts, caps)
end
it "executes the capability" do
caps[:bar] = { test: cap_instance(:test) }
init
expect { subject.capability(:test) }.
to raise_error(RuntimeError, "cap: test []")
end
it "executes the capability with arguments" do
caps[:bar] = { test: cap_instance(:test) }
init
expect { subject.capability(:test, 1) }.
to raise_error(RuntimeError, "cap: test [1]")
end
it "raises an exception if the capability doesn't exist" do
init
expect { subject.capability(:what_is_this_i_dont_even) }.
to raise_error(Vagrant::Errors::CapabilityNotFound)
end
it "raises an exception if the method doesn't exist on the module" do
caps[:bar] = { test_is_corrupt: cap_instance(:test_is_corrupt, corrupt: true) }
init
expect { subject.capability(:test_is_corrupt) }.
to raise_error(Vagrant::Errors::CapabilityInvalid)
end
end
end

View File

@ -53,124 +53,6 @@ describe Vagrant::Guest do
guests[name] = [guest, parent] guests[name] = [guest, parent]
end end
describe "#capability" do
before :each do
register_guest(:foo, nil, true)
register_guest(:bar, :foo, true)
subject.detect!
end
it "executes the capability" do
register_capability(:bar, :test)
expect { subject.capability(:test) }.
to raise_error(RuntimeError, "cap: test [machine]")
end
it "executes the capability with arguments" do
register_capability(:bar, :test)
expect { subject.capability(:test, 1) }.
to raise_error(RuntimeError, "cap: test [machine, 1]")
end
it "raises an exception if the capability doesn't exist" do
expect { subject.capability(:what_is_this_i_dont_even) }.
to raise_error(Vagrant::Errors::GuestCapabilityNotFound)
end
it "raises an exception if the method doesn't exist on the module" do
register_capability(:bar, :test_is_corrupt, corrupt: true)
expect { subject.capability(:test_is_corrupt) }.
to raise_error(Vagrant::Errors::GuestCapabilityInvalid)
end
end
describe "#capability?" do
before :each do
register_guest(:foo, nil, true)
register_guest(:bar, :foo, true)
subject.detect!
end
it "doesn't have unknown capabilities" do
subject.capability?(:what_is_this_i_dont_even).should_not be
end
it "doesn't have capabilities registered to other guests" do
register_capability(:baz, :test)
subject.capability?(:test).should_not be
end
it "has capability of detected guest" do
register_capability(:bar, :test)
subject.capability?(:test).should be
end
it "has capability of parent guests" do
register_capability(:foo, :test)
subject.capability?(:test).should be
end
end
describe "#detect!" do
it "detects the first match" do
register_guest(:foo, nil, false)
register_guest(:bar, nil, true)
register_guest(:baz, nil, false)
subject.detect!
subject.name.should == :bar
subject.chain.length.should == 1
subject.chain[0][0].should == :bar
subject.chain[0][1].name.should == :bar
end
it "detects those with the most parents first" do
register_guest(:foo, nil, true)
register_guest(:bar, :foo, true)
register_guest(:baz, :bar, true)
register_guest(:foo2, nil, true)
register_guest(:bar2, :foo2, true)
subject.detect!
subject.name.should == :baz
subject.chain.length.should == 3
subject.chain.map(&:first).should == [:baz, :bar, :foo]
subject.chain.map { |x| x[1] }.map(&:name).should == [:baz, :bar, :foo]
end
it "detects the forced guest setting" do
register_guest(:foo, nil, false)
register_guest(:bar, nil, false)
machine.config.vm.stub(:guest => :bar)
subject.detect!
subject.name.should == :bar
end
it "raises an exception if the forced guest can't be found" do
register_guest(:foo, nil, true)
machine.config.vm.stub(:guest => :bar)
expect { subject.detect! }.
to raise_error(Vagrant::Errors::GuestExplicitNotDetected)
end
it "raises an exception if no guest can be detected" do
expect { subject.detect! }.
to raise_error(Vagrant::Errors::GuestNotDetected)
end
end
describe "#ready?" do describe "#ready?" do
before(:each) do before(:each) do
register_guest(:foo, nil, true) register_guest(:foo, nil, true)