Laying the foundation for the new guest plugin

This commit is contained in:
Mitchell Hashimoto 2013-04-03 21:47:57 -07:00
parent 4ffc2c3b74
commit 52f3847b0a
31 changed files with 281 additions and 132 deletions

View File

@ -227,6 +227,10 @@ module Vagrant
error_key(:port_collision_resume)
end
class GuestNotDetected < VagrantError
error_key(:guest_not_detected)
end
class LocalDataDirectoryNotAccessible < VagrantError
error_key(:local_data_dir_not_accessible)
end

101
lib/vagrant/guest.rb Normal file
View File

@ -0,0 +1,101 @@
require "log4r"
module Vagrant
# This class handles guest-OS specific interactions with a machine.
# It is primarily responsible for detecting the proper guest OS
# implementation and then delegating capabilities.
#
# Vagrant has many tasks which require specific guest OS knowledge.
# These are implemented using a guest/capability system. Various plugins
# register as "guests" which determine the underlying OS of the system.
# Then, "guest capabilities" register themselves for a specific OS (one
# or more), and these capabilities are called.
#
# Example capabilities might be "mount_virtualbox_shared_folder" or
# "configure_networks".
#
# This system allows for maximum flexibility and pluginability for doing
# guest OS specific operations.
class Guest
attr_reader :chain
def initialize(machine, guests)
@logger = Log4r::Logger.new("vagrant::guest")
@chain = []
@guests = guests
@machine = machine
end
# This will detect the proper guest OS for the machine and set up
# the class to actually execute capabilities.
def detect!
@logger.info("Detect 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
catch(:guest_os) do
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}!")
@chain << guest
# Build the proper chain of parents if there are any.
# This allows us to do "inheritence" of capabilities later
if guest_info[1]
parent_info = @guests[guest_info[1]]
while parent_info
@chain << parent_info[0].new
parent_info = @guests[parent_info[1]]
end
end
@logger.info("Full guest chain: #{@chain.inspect}")
# Exit the search
throw :guest_os
end
end
end
end
# We shouldn't reach this point. Ideally we would detect
# all operating systems.
raise Errors::GuestNotDetected if @chain.empty?
end
# This returns whether the guest is ready to work. If this returns
# `false`, then {#detect!} should be called in order to detect the
# guest OS.
#
# @return [Boolean]
def ready?
!@chain.empty?
end
end
end

View File

@ -83,6 +83,7 @@ module Vagrant
@config = config
@data_dir = data_dir
@env = env
@guest = Guest.new(self)
@name = name
@provider_config = provider_config
@provider_name = provider_name
@ -169,32 +170,7 @@ module Vagrant
# @return [Object]
def guest
raise Errors::MachineGuestNotReady if !communicate.ready?
# Load the initial guest.
last_guest = config.vm.guest
guest = load_guest(last_guest)
# Loop and distro dispatch while there are distros.
while true
distro = guest.distro_dispatch
break if !distro
# This is just some really basic loop detection and avoiding for
# guest classes. This is just here to help implementers a bit
# avoid a situation that is fairly easy, since if you subclass
# a parent which does `distro_dispatch`, you'll end up dispatching
# forever.
if distro == last_guest
@logger.warn("Distro dispatch loop in '#{distro}'. Exiting loop.")
break
end
last_guest = distro
guest = load_guest(distro)
end
# Return the result
guest
@guest
end
# This sets the unique ID associated with this machine. This will

View File

@ -16,6 +16,11 @@ module Vagrant
# @return [Hash<Symbol, Registry>]
attr_reader :configs
# This contains all the guests and their parents.
#
# @return [Registry<Symbol, Array<Class, Symbol>>]
attr_reader :guests
# This contains all the provider plugins by name, and returns
# the provider class and options.
#
@ -27,6 +32,7 @@ module Vagrant
@action_hooks = Hash.new { |h, k| h[k] = [] }
@configs = Hash.new { |h, k| h[k] = Registry.new }
@guests = Registry.new
@providers = Registry.new
end
end

View File

@ -1,35 +1,21 @@
module Vagrant
module Plugin
module V2
# The base class for a guest. A guest represents an installed system
# within a machine that Vagrant manages. There are some portions of
# Vagrant which are OS-specific such as mountaing shared folders and
# halting the machine, and this abstraction allows the implementation
# for these to be seperate from the core of Vagrant.
# A base class for a guest OS. A guest OS is responsible for detecting
# that the guest operating system running within the machine. The guest
# can then be extended with various "guest capabilities" which are their
# own form of plugin.
#
# The guest class itself is only responsible for detecting itself,
# and may provide helpers for the capabilties.
class Guest
class BaseError < Errors::VagrantError
error_namespace("vagrant.guest.base")
end
include Vagrant::Util
# The VM which this system is tied to.
attr_reader :vm
# Initializes the system. Any subclasses MUST make sure this
# method is called on the parent. Therefore, if a subclass overrides
# `initialize`, then you must call `super`.
def initialize(vm)
@vm = vm
end
# This method is automatically called when the system is available (when
# Vagrant can successfully SSH into the machine) to give the system a chance
# to determine the distro and return a distro-specific system.
# This method is called when the machine is booted and has communication
# capabilities in order to detect whether this guest operating system
# is running within the machine.
#
# If this method returns nil, then this instance is assumed to be
# the most specific guest implementation.
def distro_dispatch
# @return [Boolean]
def guest?(machine)
false
end
# Halt the machine. This method should gracefully shut down the

View File

@ -67,7 +67,7 @@ module Vagrant
def guests
Registry.new.tap do |result|
@registered.each do |plugin|
result.merge!(plugin.guest)
result.merge!(plugin.components.guests)
end
end
end

View File

@ -124,7 +124,6 @@ module Vagrant
# without breaking anything in future versions of Vagrant.
#
# @param [String] name Configuration key.
# XXX: Document options hash
def self.config(name, scope=nil, &block)
scope ||= :top
components.configs[scope].register(name.to_sym, &block)
@ -135,14 +134,14 @@ module Vagrant
# the given key.
#
# @param [String] name Name of the guest.
def self.guest(name=UNSET_VALUE, &block)
data[:guests] ||= Registry.new
# @param [String] parent Name of the parent guest (if any)
def self.guest(name=UNSET_VALUE, parent=nil, &block)
components.guests.register(name.to_sym) do
parent = parent.to_sym if parent
# Register a new guest class only if a name was given
data[:guests].register(name.to_sym, &block) if name != UNSET_VALUE
# Return the registry
data[:guests]
[block.call, parent]
end
nil
end
# Defines an additionally available host implementation with

View File

@ -12,6 +12,10 @@ module VagrantPlugins
# Make the TemplateRenderer top-level
include Vagrant::Util
def detect?(machine)
machine.communicate.test("cat /etc/arch-release")
end
def change_host_name(name)
# Only do this if the hostname is not already set
if !vm.communicate.test("sudo hostname | grep '#{name}'")

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "Arch guest"
description "Arch guest support."
guest("arch") do
guest("arch", "linux") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -12,6 +12,10 @@ module VagrantPlugins
# Make the TemplateRenderer top-level
include Vagrant::Util
def detect?(machine)
machine.communicate.test("cat /proc/version | grep 'Debian'")
end
def configure_networks(networks)
# First, remove any previous network modifications
# from the interface file.

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "Debian guest"
description "Debian guest support."
guest("debian") do
guest("debian", "linux") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -12,6 +12,10 @@ module VagrantPlugins
# Make the TemplateRenderer top-level
include Vagrant::Util
def detect?(machine)
machine.communicate.test("grep 'Fedora release 1[678]' /etc/redhat-release")
end
def configure_networks(networks)
# Accumulate the configurations to add to the interfaces file as well
# as what interfaces we're actually configuring since we use that later.

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "Fedora guest"
description "Fedora guest support."
guest("fedora") do
guest("fedora", "linux") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -11,6 +11,11 @@ module VagrantPlugins
error_namespace("vagrant.guest.freebsd")
end
def detect?(machine)
# TODO: FreeBSD detection
false
end
def halt
begin
vm.communicate.sudo("shutdown -p now")

View File

@ -11,6 +11,10 @@ module VagrantPlugins
# Make the TemplateRenderer top-level
include Vagrant::Util
def detect?(machine)
machine.communicate.test("cat /etc/gentoo-release")
end
def configure_networks(networks)
# Remove any previous host only network additions to the interface file
vm.communicate.sudo("sed -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/conf.d/net > /tmp/vagrant-network-interfaces")

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "Gentoo guest"
description "Gentoo guest support."
guest("gentoo") do
guest("gentoo", "linux") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -18,6 +18,11 @@ module VagrantPlugins
@logger = Log4r::Logger.new("vagrant::guest::linux")
end
def detect?(machine)
# TODO: Linux detection
false
end
def distro_dispatch
@vm.communicate.tap do |comm|
if comm.test("cat /etc/debian_version")

View File

@ -5,6 +5,11 @@ require Vagrant.source_root.join("plugins/guests/linux/guest")
module VagrantPlugins
module GuestOpenBSD
class Guest < VagrantPlugins::GuestLinux::Guest
def detect?(machine)
# TODO: OpenBSD detection
false
end
def halt
vm.communicate.sudo("shutdown -p -h now")
end

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "OpenBSD guest"
description "OpenBSD guest support."
guest("openbsd") do
guest("openbsd", "linux") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -5,6 +5,10 @@ require Vagrant.source_root.join("plugins/guests/redhat/guest")
module VagrantPlugins
module GuestPld
class Guest < VagrantPlugins::GuestRedHat::Guest
def detect?(machine)
machine.communicate.test("cat /etc/pld-release")
end
def network_scripts_dir
'/etc/sysconfig/interfaces/'
end

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "PLD Linux guest"
description "PLD Linux guest support."
guest("pld") do
guest("pld", "redhat") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -12,6 +12,10 @@ module VagrantPlugins
# Make the TemplateRenderer top-level
include Vagrant::Util
def detect?(machine)
machine.communicate.test("cat /etc/redhat-release")
end
def configure_networks(networks)
# Accumulate the configurations to add to the interfaces file as
# well as what interfaces we're actually configuring since we use that

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "RedHat guest"
description "RedHat guest support."
guest("redhat") do
guest("redhat", "linux") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -11,6 +11,10 @@ module VagrantPlugins
error_namespace("vagrant.guest.solaris")
end
def detect?(machine)
machine.communicate.test("grep 'Solaris' /etc/release")
end
def configure_networks(networks)
networks.each do |network|
device = "#{vm.config.solaris.device}#{network[:interface]}"

View File

@ -5,6 +5,10 @@ require Vagrant.source_root.join("plugins/guests/redhat/guest")
module VagrantPlugins
module GuestSuse
class Guest < VagrantPlugins::GuestRedHat::Guest
def detect?(machine)
machine.communicate.test("cat /etc/SuSE-release")
end
def network_scripts_dir
'/etc/sysconfig/network/'
end

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "SUSE guest"
description "SUSE guest support."
guest("suse") do
guest("suse", "redhat") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -5,6 +5,10 @@ require Vagrant.source_root.join("plugins/guests/debian/guest")
module VagrantPlugins
module GuestUbuntu
class Guest < VagrantPlugins::GuestDebian::Guest
def detect?(machine)
machine.communicate.test("cat /proc/version | grep 'Ubuntu'")
end
def mount_shared_folder(name, guestpath, options)
# Mount it like normal
super

View File

@ -6,7 +6,7 @@ module VagrantPlugins
name "Ubuntu guest"
description "Ubuntu guest support."
guest("ubuntu") do
guest("ubuntu", "debian") do
require File.expand_path("../guest", __FILE__)
Guest
end

View File

@ -173,18 +173,12 @@ en:
of the official installers or another gem is wrongly attempting to
use Vagrant internals directly. Please properly install Vagrant to
fix this. If this error persists, please contact support.
guest:
invalid_class: |-
The specified guest class does not inherit from a proper guest
component class. The guest class must inherit from this.
The specified guest class was: %{guest}
unknown_type: |-
The specified guest type is unknown: %{guest}. Please change this
to a proper value.
unspecified: |-
A VM guest type must be specified! This is done via the `config.vm.guest`
configuration value. Please read the documentation online for more information.
guest_not_detected: |-
The guest operating system of the machine could not be detected!
Vagrant requires this knowledge to perform specific tasks such
as mounting shared folders and configuring networks. Please add
the ability to detect this guest operating system to Vagrant
by creating a plugin or reporting a bug.
home_dir_not_accessible: |-
The home directory you specified is not accessible. The home
directory that Vagrant uses must be both readable and writable.

View File

@ -0,0 +1,75 @@
require "pathname"
require File.expand_path("../../base", __FILE__)
describe Vagrant::Guest do
include_context "unit"
let(:guests) { {} }
let(:machine) { double("machine") }
subject { described_class.new(machine, guests) }
# 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 "#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.chain.length.should == 1
subject.chain[0].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.chain.length.should == 3
subject.chain.map(&:name).should == [:baz, :bar, :foo]
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)
end
it "should not be ready by default" do
subject.ready?.should_not be
end
it "should be ready after detecting" do
subject.detect!
subject.ready?.should be
end
end
end

View File

@ -197,6 +197,7 @@ describe Vagrant::Machine do
let(:communicator) do
result = double("communicator")
result.stub(:ready?).and_return(true)
result.stub(:test).and_return(false)
result
end
@ -212,63 +213,19 @@ describe Vagrant::Machine do
end
it "should return the configured guest" do
test_guest = Class.new(Vagrant.plugin("2", :guest))
test_guest = Class.new(Vagrant.plugin("2", :guest)) do
def detect?(machine)
true
end
end
register_plugin do |p|
p.guest(:test) { test_guest }
end
config.vm.guest = :test
result = instance.guest
result.should be_kind_of(test_guest)
end
it "should raise an exception if it can't find the configured guest" do
config.vm.guest = :bad
expect { instance.guest }.
to raise_error(Vagrant::Errors::VMGuestError)
end
it "should distro dispatch to the most specific guest" do
# Create the classes and dispatch the parent into the child
guest_parent = Class.new(Vagrant.plugin("2", :guest)) do
def distro_dispatch
:child
end
end
guest_child = Class.new(Vagrant.plugin("2", :guest))
# Register the classes
register_plugin do |p|
p.guest(:parent) { guest_parent }
p.guest(:child) { guest_child }
end
# Test that the result is the child
config.vm.guest = :parent
instance.guest.should be_kind_of(guest_child)
end
it "should protect against loops in the distro dispatch" do
# Create the classes and dispatch the parent into the child
guest_parent = Class.new(Vagrant.plugin("2", :guest)) do
def distro_dispatch
:parent
end
end
# Register the classes
register_plugin do |p|
p.guest(:parent) { guest_parent }
end
# Test that the result is the child
config.vm.guest = :parent
instance.guest.should be_kind_of(guest_parent)
end
end
describe "setting the ID" do