From 1f6095f912cb95e3fd0edb22823a4fb9aba20ce6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Jan 2014 16:12:12 -0800 Subject: [PATCH] core: Vagrant::CapabilityHost is a module for adding capabilities to things --- lib/vagrant/capability_host.rb | 175 ++++++++++++++++++++++ lib/vagrant/errors.rb | 16 ++ templates/locales/en.yml | 16 ++ test/unit/vagrant/capability_host_test.rb | 166 ++++++++++++++++++++ test/unit/vagrant/guest_test.rb | 118 --------------- 5 files changed, 373 insertions(+), 118 deletions(-) create mode 100644 lib/vagrant/capability_host.rb create mode 100644 test/unit/vagrant/capability_host_test.rb diff --git a/lib/vagrant/capability_host.rb b/lib/vagrant/capability_host.rb new file mode 100644 index 000000000..4c77930fa --- /dev/null +++ b/lib/vagrant/capability_host.rb @@ -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>] 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>] 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>] + 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 diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index ce21aef7b..fc30dfe1a 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -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 diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 384d1220d..6ca7b8e0b 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -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. diff --git a/test/unit/vagrant/capability_host_test.rb b/test/unit/vagrant/capability_host_test.rb new file mode 100644 index 000000000..d35e81afb --- /dev/null +++ b/test/unit/vagrant/capability_host_test.rb @@ -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 diff --git a/test/unit/vagrant/guest_test.rb b/test/unit/vagrant/guest_test.rb index 4dcaab414..6dd9cbf8c 100644 --- a/test/unit/vagrant/guest_test.rb +++ b/test/unit/vagrant/guest_test.rb @@ -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)