Merge pull request #2781 from mitchellh/f-abstract-capability

Abstract "Capability" system so it can be used with hosts
This commit is contained in:
Mitchell Hashimoto 2014-01-07 16:35:57 -08:00
commit a83498e32a
10 changed files with 421 additions and 295 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, *args) 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

@ -43,6 +43,9 @@ module Vagrant
# error code, and the error key is used as a default message from # error code, and the error key is used as a default message from
# I18n. # I18n.
class VagrantError < StandardError class VagrantError < StandardError
# This is extra data passed into the message for translation.
attr_accessor :extra_data
def self.error_key(key=nil, namespace=nil) def self.error_key(key=nil, namespace=nil)
define_method(:error_key) { key } define_method(:error_key) { key }
error_namespace(namespace) if namespace error_namespace(namespace) if namespace
@ -57,6 +60,8 @@ module Vagrant
end end
def initialize(message=nil, *args) def initialize(message=nil, *args)
message ||= {}
@extra_data = message.dup
message = { :_key => message } if message && !message.is_a?(Hash) message = { :_key => message } if message && !message.is_a?(Hash)
message = { :_key => error_key, :_namespace => error_namespace }.merge(message || {}) message = { :_key => error_key, :_namespace => error_namespace }.merge(message || {})
@ -172,6 +177,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

@ -1,5 +1,7 @@
require "log4r" require "log4r"
require "vagrant/capability_host"
module Vagrant module Vagrant
# This class handles guest-OS specific interactions with a machine. # This class handles guest-OS specific interactions with a machine.
# It is primarily responsible for detecting the proper guest OS # It is primarily responsible for detecting the proper guest OS
@ -17,21 +19,13 @@ module Vagrant
# This system allows for maximum flexibility and pluginability for doing # This system allows for maximum flexibility and pluginability for doing
# guest OS specific operations. # guest OS specific operations.
class Guest class Guest
attr_reader :chain include CapabilityHost
# The name of the guest OS. This is available after {#detect!} is
# called.
#
# @return [Symbol]
attr_reader :name
def initialize(machine, guests, capabilities) def initialize(machine, guests, capabilities)
@logger = Log4r::Logger.new("vagrant::guest") @logger = Log4r::Logger.new("vagrant::guest")
@capabilities = capabilities @capabilities = capabilities
@chain = []
@guests = guests @guests = guests
@machine = machine @machine = machine
@name = nil
end end
# This will detect the proper guest OS for the machine and set up # This will detect the proper guest OS for the machine and set up
@ -40,52 +34,7 @@ module Vagrant
@logger.info("Detect guest for machine: #{@machine}") @logger.info("Detect guest for machine: #{@machine}")
guest_name = @machine.config.vm.guest guest_name = @machine.config.vm.guest
if guest_name initialize_capabilities!(guest_name, @guests, @capabilities, @machine)
@logger.info("Using explicit config.vm.guest value: #{guest_name}")
else
# No explicit guest was specified, so autodetect it.
guest_name = autodetect_guest
raise Errors::GuestNotDetected if !guest_name
end
if !@guests[guest_name]
# This can happen if config.vm.guest was specified with a value
# that doesn't exist.
raise Errors::GuestExplicitNotDetected, value: guest_name.to_s
end
@name = guest_name
@chain = guest_chain(@name)
end
# Tests whether the guest has the named capability.
#
# @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.
def capability(cap_name, *args)
@logger.info("Execute capability: #{cap_name} (#{@chain[0][0]})")
cap_mod = capability_module(cap_name.to_sym)
if !cap_mod
raise Errors::GuestCapabilityNotFound,
:cap => cap_name.to_s,
:guest => @chain[0][0].to_s
end
cap_method = nil
begin
cap_method = cap_mod.method(cap_name)
rescue NameError
raise Errors::GuestCapabilityInvalid,
:cap => cap_name.to_s,
:guest => @chain[0][0].to_s
end
cap_method.call(@machine, *args)
end end
# This returns whether the guest is ready to work. If this returns # This returns whether the guest is ready to work. If this returns
@ -94,98 +43,7 @@ module Vagrant
# #
# @return [Boolean] # @return [Boolean]
def ready? def ready?
!@chain.empty? !!capability_host_chain
end
protected
# Returns the registered module for a capability with the given name.
#
# @param [Symbol] cap_name
# @return [Module]
def capability_module(cap_name)
@logger.debug("Searching for cap: #{cap_name}")
@chain.each do |guest_name, guest|
@logger.debug("Checking in: #{guest_name}")
caps = @capabilities[guest_name]
if caps && caps.has_key?(cap_name)
@logger.debug("Found cap: #{cap_name} in #{guest_name}")
return caps[cap_name]
end
end
nil
end
# This autodetects the guest to use and returns the name of the guest.
# This returns nil if the guest type could not be autodetected.
#
# @return [Symbol]
def autodetect_guest
@logger.info("Autodetecting guest for machine: #{@machine}")
# Get the mapping of guests with the most parents. We start searching
# with the guests with the most parents first.
parent_count = {}
@guests.each do |name, parts|
parent_count[name] = 0
parent = parts[1]
while parent
parent_count[name] += 1
parent = @guests[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 guest names
parent_count_to_guests = {}
parent_count.each do |name, count|
parent_count_to_guests[count] ||= []
parent_count_to_guests[count] << name
end
sorted_counts = parent_count_to_guests.keys.sort.reverse
sorted_counts.each do |count|
parent_count_to_guests[count].each do |name|
@logger.debug("Trying: #{name}")
guest_info = @guests[name]
guest = guest_info[0].new
if guest.detect?(@machine)
@logger.info("Detected: #{name}!")
return name
end
end
end
return nil
end
# This returns the complete chain for the given guest.
#
# @return [Array]
def guest_chain(name)
guest_info = @guests[name]
guest = guest_info[0].new
chain = []
chain << [name, guest]
# Build the proper chain of parents if there are any.
# This allows us to do "inheritance" of capabilities later
if guest_info[1]
parent_name = guest_info[1]
parent_info = @guests[parent_name]
while parent_info
chain << [parent_name, parent_info[0].new]
parent_name = parent_info[1]
parent_info = @guests[parent_name]
end
end
return chain
end end
end end
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

@ -14,6 +14,7 @@ require "unit/support/dummy_communicator"
require "unit/support/dummy_provider" require "unit/support/dummy_provider"
require "unit/support/shared/base_context" require "unit/support/shared/base_context"
require "unit/support/shared/action_synced_folders_context" require "unit/support/shared/action_synced_folders_context"
require "unit/support/shared/capability_helpers_context"
require "unit/support/shared/virtualbox_context" require "unit/support/shared/virtualbox_context"
# Do not buffer output # Do not buffer output

View File

@ -0,0 +1,21 @@
shared_context "capability_helpers" do
def detect_class(result)
Class.new do
define_method(:detect?) do |*args|
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
end

View File

@ -0,0 +1,163 @@
require File.expand_path("../../base", __FILE__)
require "vagrant/capability_host"
describe Vagrant::CapabilityHost do
include_context "capability_helpers"
subject do
Class.new do
extend Vagrant::CapabilityHost
end
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 "passes on extra args to the detect method" do
klass = Class.new do
def detect?(*args)
raise "detect: #{args.inspect}"
end
end
hosts = {
foo: [klass, nil],
}
expect { subject.initialize_capabilities!(nil, hosts, {}, 1, 2) }.
to raise_error(RuntimeError, "detect: [1, 2]")
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

@ -32,10 +32,16 @@ describe Vagrant::Errors::VagrantError do
end end
end end
subject { klass.new } subject { klass.new(data: "yep") }
it "should use the translation for the message" do it "should use the translation for the message" do
subject.to_s.should == "foo" subject.to_s.should == "foo"
end end
it "should expose translation keys to the user" do
expect(subject.extra_data.length).to eql(1)
expect(subject.extra_data).to have_key(:data)
expect(subject.extra_data[:data]).to eql("yep")
end
end end
end end

View File

@ -3,7 +3,7 @@ require "pathname"
require File.expand_path("../../base", __FILE__) require File.expand_path("../../base", __FILE__)
describe Vagrant::Guest do describe Vagrant::Guest do
include_context "unit" include_context "capability_helpers"
let(:capabilities) { {} } let(:capabilities) { {} }
let(:guests) { {} } let(:guests) { {} }
@ -18,162 +18,27 @@ describe Vagrant::Guest do
subject { described_class.new(machine, guests, capabilities) } subject { described_class.new(machine, guests, capabilities) }
# This registers a capability with a specific guest
def register_capability(guest, capability, options=nil)
options ||= {}
cap = Class.new do
if !options[:corrupt]
define_method(capability) do |*args|
raise "cap: #{capability} #{args.inspect}"
end
end
end
capabilities[guest] ||= {}
capabilities[guest][capability] = cap.new
end
# This registers a guest with the class.
#
# @param [Symbol] name Name of the guest
# @param [Symbol] parent Name of the parent
# @param [Boolean] detect Whether or not to detect properly
def register_guest(name, parent, detect)
guest = Class.new(Vagrant.plugin("2", "guest")) do
define_method(:name) do
name
end
define_method(:detect?) do |m|
detect
end
end
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 describe "#detect!" do
it "detects the first match" do it "auto-detects if no explicit guest name given" do
register_guest(:foo, nil, false) machine.config.vm.stub(guest: nil)
register_guest(:bar, nil, true) subject.should_receive(:initialize_capabilities!).
register_guest(:baz, nil, false) with(nil, guests, capabilities, machine)
subject.detect! 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 end
it "detects those with the most parents first" do it "uses the explicit guest name if specified" do
register_guest(:foo, nil, true) machine.config.vm.stub(guest: :foo)
register_guest(:bar, :foo, true) subject.should_receive(:initialize_capabilities!).
register_guest(:baz, :bar, true) with(:foo, guests, capabilities, machine)
register_guest(:foo2, nil, true)
register_guest(:bar2, :foo2, true)
subject.detect! 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
end end
describe "#ready?" do describe "#ready?" do
before(:each) do before(:each) do
register_guest(:foo, nil, true) guests[:foo] = [detect_class(true), nil]
end end
it "should not be ready by default" do it "should not be ready by default" do

View File

@ -225,8 +225,8 @@ describe Vagrant::Machine do
it "should return the configured guest" do it "should return the configured guest" do
result = instance.guest result = instance.guest
result.should be_kind_of(Vagrant::Guest) result.should be_kind_of(Vagrant::Guest)
result.ready?.should be expect(result).to be_ready
result.chain[0][0].should == :test expect(result.capability_host_chain[0][0]).to eql(:test)
end end
end end