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)
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
error_key(:cfengine_bootstrap_failed)
end

View File

@ -261,6 +261,22 @@ en:
issues. The error from Bundler is:
%{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: |-
Failed to bootstrap CFEngine. Please see the output above to
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]
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
before(:each) do
register_guest(:foo, nil, true)