Separate Vagrant::Action into Runner and Registry

This commit is contained in:
Mitchell Hashimoto 2011-12-09 14:22:03 -08:00
parent 5bfcbcba66
commit daf711fd80
15 changed files with 215 additions and 199 deletions

View File

@ -10,134 +10,10 @@ require 'vagrant/action/general'
require 'vagrant/action/vm'
module Vagrant
# Manages action running and registration. Every Vagrant environment
# has an instance of {Action} to allow for running in the context of
# the environment, which is accessible at {Environment#actions}. Actions
# are the foundation of most functionality in Vagrant, and are implemented
# architecturally as "middleware."
#
# # Registering an Action
#
# The main benefits of registering an action is the ability to retrieve and
# modify that registered action, as well as easily run the action. An example
# of registering an action is shown below, with a simple middleware which just
# outputs to `STDOUT`:
#
# class StdoutMiddleware
# def initialize(app, env)
# @app = app
# end
#
# def call(env)
# puts "HI!"
# @app.call(env)
# end
# end
#
# Vagrant::Action.register(:stdout, StdoutMiddleware)
#
# Then to run a registered action, assuming `env` is a loaded {Environment}:
#
# env.actions.run(:stdout)
#
# Or to retrieve the action class for any reason:
#
# Vagrant::Action[:stdout]
#
# # Running an Action
#
# There are various built-in registered actions such as `start`, `stop`, `up`,
# etc. Actions are built to be run in the context of an environment, so use
# {Environment#actions} to run all actions. Then simply call {#run}:
#
# env.actions.run(:name)
#
# Where `:name` is the name of the registered action.
#
class Action
autoload :Environment, 'vagrant/action/environment'
autoload :MultiStep, 'vagrant/action/multistep'
autoload :Step, 'vagrant/action/step'
autoload :Registry, 'vagrant/action/registry'
autoload :Runner, 'vagrant/action/runner'
autoload :Warden, 'vagrant/action/warden'
include Util
@@reported_interrupt = false
class << self
# Returns the list of registered actions.
#
# @return [Array]
def actions
@actions ||= {}
end
# Registers an action and associates it with a symbol. This
# symbol can then be referenced in other action builds and
# callbacks can be registered on that symbol.
#
# @param [Symbol] key
def register(key, callable)
actions[key.to_sym] = callable
end
# Retrieves a registered action by key.
#
# @param [Symbol] key
def [](key)
actions[key.to_sym]
end
end
# The environment to run the actions in.
attr_reader :env
# Initializes the action with the given environment which the actions
# will be run in.
#
# @param [Environment] env
def initialize(env)
@env = env
@logger = Log4r::Logger.new("vagrant::action")
end
# Runs the given callable object in the context of the environment.
# If a symbol is given as the `callable` parameter, then it is looked
# up in the registered actions list which are registered with {register}.
#
# Any options given are injected into the environment hash.
#
# @param [Object] callable An object which responds to `call`.
def run(callable_id, options=nil)
callable = callable_id
callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class)
callable = self.class.actions[callable_id] if callable_id.kind_of?(Symbol)
raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call)
action_environment = Action::Environment.new(env)
action_environment.merge!(options || {})
# Run the before action run callback, if we're not doing that already
run(:before_action_run, action_environment) if callable_id != :before_action_run
# Run the action chain in a busy block, marking the environment as
# interrupted if a SIGINT occurs, and exiting cleanly once the
# chain has been run.
int_callback = lambda do
if action_environment.interrupted?
env.ui.error I18n.t("vagrant.actions.runner.exit_immediately")
abort
end
env.ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup") if !@@reported_interrupt
action_environment.interrupt!
@@reported_interrupt = true
end
# We place a process lock around every action that is called
@logger.info("Running action: #{callable_id}")
env.lock do
Busy.busy(int_callback) { callable.call(action_environment) }
end
end
end
end

View File

@ -36,7 +36,7 @@ module Vagrant
# Use the class if it matches the given URI or if this
# is the last class...
if classes.length == (i + 1) || klass.match?(@env["box"].uri)
if classes.length == (i + 1) || klass.match?(@env["box_url"])
@env.ui.info I18n.t("vagrant.actions.box.download.with", :class => klass.to_s)
@downloader = klass.new(@env)
break
@ -47,7 +47,7 @@ module Vagrant
# just in case for now.
raise Errors::BoxDownloadUnknownType if !@downloader
@downloader.prepare(@env["box"].uri)
@downloader.prepare(@env["box_url"])
true
end
@ -76,7 +76,7 @@ module Vagrant
end
def download_to(f)
@downloader.download!(@env["box"].uri, f)
@downloader.download!(@env["box_url"], f)
end
end
end

View File

@ -5,6 +5,7 @@ module Vagrant
# all the necessary Vagrant libraries are loaded. Hopefully
# in the future this will no longer be necessary with autoloading.
def self.builtin!
return
# provision - Provisions a running VM
register(:provision, Builder.new do
use VM::CheckAccessible
@ -109,7 +110,7 @@ module Vagrant
# Other callbacks. There will be more of these in the future. For
# now, these are limited to what are needed internally.
register(:before_action_run, Builder.new do
use General::Validate
# use General::Validate
end)
end
end

View File

@ -4,32 +4,11 @@ module Vagrant
# to the `call` method of each action. This environment contains
# some helper methods for accessing the environment as well
# as being a hash, to store any additional options.
class Environment < Util::HashWithIndifferentAccess
# The {Vagrant::Environment} object represented by this
# action environment.
attr_reader :env
def initialize(env)
super() do |h,k|
# By default, try to find the key as a method on the
# environment. Gross eval use here.
begin
value = eval("h.env.#{k}")
h[k] = value
rescue Exception
nil
end
end
@env = env
class Environment < Hash
def initialize
@interrupted = false
end
# Returns a UI object from the environment
def ui
env.ui
end
# Marks an environment as interrupted (by an outside signal or
# anything). This will trigger any middleware sequences using this
# environment to halt. This is automatically set by {Action} when

View File

@ -0,0 +1,32 @@
module Vagrant
class Action
# This is the action registry, which stores action steps indexed
# by a unique name. These registry names can be used to call actions
# via the `Runner` class.
class Registry
def initialize
@actions = {}
end
# Register a callable by key.
#
# The callable should be given in a block which will be lazily evaluated
# when the action is needed.
#
# If an action by the given name already exists then it will be
# overwritten.
def register(key, &block)
@actions[key] = block
end
# Get an action by the given key.
#
# This will evaluate the block given to `register` and return the resulting
# action stack.
def get(key)
return nil if !@actions.has_key?(key)
@actions[key].call
end
end
end
end

View File

@ -0,0 +1,49 @@
require 'log4r'
require 'vagrant/util/busy'
# TODO:
# * env.ui
# * env.lock
module Vagrant
class Action
class Runner
@@reported_interrupt = false
def initialize(registry)
@registry = registry
@logger = Log4r::Logger.new("vagrant::action::runner")
end
def run(callable_id, options=nil)
callable = callable_id
callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class)
callable = @registry.get(callable_id) if callable_id.kind_of?(Symbol)
raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call)
# Create the initial environment with the options given
environment = Environment.new
environment.merge!(options || {})
# Run the action chain in a busy block, marking the environment as
# interrupted if a SIGINT occurs, and exiting cleanly once the
# chain has been run.
int_callback = lambda do
if environment.interrupted?
env.ui.error I18n.t("vagrant.actions.runner.exit_immediately")
abort
end
env.ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup") if !@@reported_interrupt
environment.interrupt!
@@reported_interrupt = true
end
# We place a process lock around every action that is called
@logger.info("Running action: #{callable_id}")
Util::Busy.busy(int_callback) { callable.call(environment) }
end
end
end
end

View File

@ -37,14 +37,6 @@ module Vagrant
directory.join(env.config.vm.box_ovf)
end
# Begins the process of adding a box to the vagrant installation. This
# method requires that `name` and `uri` be set. The logic of this method
# is kicked out to the `box_add` registered middleware.
def add
raise Errors::BoxAlreadyExists, :name => name if File.directory?(directory)
env.actions.run(:box_add, { "box" => self, "validate" => false })
end
# Begins the process of destroying this box. This cannot be undone!
def destroy
env.actions.run(:box_remove, { "box" => self, "validate" => false })

View File

@ -12,9 +12,10 @@ module Vagrant
attr_reader :directory
# Initializes the class to search for boxes in the given directory.
def initialize(directory)
def initialize(directory, action_runner)
@directory = directory
@boxes = []
@action_runner = action_runner
reload!
end
@ -29,6 +30,12 @@ module Vagrant
nil
end
# Adds a box to this collection with the given name and located
# at the given URL.
def add(name, url)
@action_runner.run(:box_add, :box_name => name, :box_url => url)
end
# Loads the list of all boxes from the source. This modifies the
# current array.
def reload!

View File

@ -5,7 +5,7 @@ module Vagrant
desc "add NAME URI", "Add a box to the system"
def add(name, uri)
Box.add(env, name, uri)
env.boxes.add(name, uri)
end
desc "remove NAME", "Remove a box from the system"

View File

@ -146,7 +146,7 @@ module Vagrant
#
# @return [BoxCollection]
def boxes
@_boxes ||= BoxCollection.new(boxes_path)
@_boxes ||= BoxCollection.new(boxes_path, action_runner)
end
# Returns the VMs associated with this environment.
@ -205,12 +205,18 @@ module Vagrant
@host ||= Hosts::Base.load(self, config.vagrant.host)
end
# Returns the {Action} class for this environment which allows actions
# to be executed (middleware chains) in the context of this environment.
# Action runner for executing actions in the context of this environment.
#
# @return [Action]
def actions
@actions ||= Action.new(self)
# @return [Action::Runner]
def action_runner
@action_runner ||= Action::Runner.new(action_registry)
end
# Action registry for registering new actions with this environment.
#
# @return [Action::Registry]
def action_registry
@action_registry ||= Action::Registry.new
end
# Loads on initial access and reads data from the global data store.

View File

@ -0,0 +1,17 @@
require File.expand_path("../../../base", __FILE__)
describe Vagrant::Action::Environment do
let(:instance) { described_class.new }
it "should be a hash" do
instance.should be_empty
instance["foo"] = "bar"
instance["foo"].should == "bar"
end
it "should keep track of interrupted state" do
instance.should_not be_interrupted
instance.interrupt!
instance.should be_interrupted
end
end

View File

@ -0,0 +1,26 @@
require File.expand_path("../../../base", __FILE__)
describe Vagrant::Action::Registry do
let(:instance) { described_class.new }
it "should return nil for nonexistent actions" do
instance.get("foo").should be_nil
end
it "should register an action without calling the block yet" do
expect do
instance.register("foo") do
raise Exception, "BOOM!"
end
end.to_not raise_error
end
it "should call and return the result of a block when asking for the action" do
object = Object.new
instance.register("foo") do
object
end
instance.get("foo").should eql(object)
end
end

View File

@ -0,0 +1,43 @@
require File.expand_path("../../../base", __FILE__)
describe Vagrant::Action::Runner do
let(:registry) do
d = double("registry")
d.stub(:get)
d
end
let(:instance) { described_class.new(registry) }
it "should raise an error if an invalid callable is given" do
expect { instance.run(7) }.to raise_error(ArgumentError, /must be a callable/)
end
it "should be able to use a Proc as a callable" do
callable = Proc.new { raise Exception, "BOOM" }
expect { instance.run(callable) }.to raise_error(Exception, "BOOM")
end
it "should be able to use a Class as a callable" do
callable = Class.new do
def initialize(app, env)
end
def call(env)
raise Exception, "BOOM"
end
end
expect { instance.run(callable) }.to raise_error(Exception, "BOOM")
end
it "should pass options into hash given to callable" do
result = nil
callable = lambda do |env|
result = env["data"]
end
instance.run(callable, "data" => "foo")
result.should == "foo"
end
end

View File

@ -4,7 +4,8 @@ describe Vagrant::BoxCollection do
include_context "unit"
let(:environment) { isolated_environment }
let(:instance) { described_class.new(environment.boxes_dir) }
let(:action_runner) { double("action runner") }
let(:instance) { described_class.new(environment.boxes_dir, action_runner) }
it "should list all available boxes" do
# No boxes yet.
@ -29,4 +30,18 @@ describe Vagrant::BoxCollection do
result.name.should == "foo"
end
end
it "should add the box" do
name = "foo"
url = "bar"
# Test the invocation of the action runner with the proper name
# and parameters. We leave the testing of the actual stack to
# acceptance tests, and individual pieces to unit tests of each
# step.
options = { :box_name => name, :box_url => url }
action_runner.should_receive(:run).with(:box_add, options)
instance.add(name, url)
end
end

View File

@ -1,27 +0,0 @@
require "test_helper"
class ActionEnvironmentTest < Test::Unit::TestCase
setup do
@klass = Vagrant::Action::Environment
@instance = @klass.new(vagrant_env)
end
should "be a hash with indifferent access" do
assert @instance.is_a?(Vagrant::Util::HashWithIndifferentAccess)
end
should "default values to those on the env" do
@instance.env.stubs(:key).returns("value")
assert_equal "value", @instance["key"]
end
should "setup the UI" do
assert_equal @instance.env.ui, @instance.ui
end
should "report interrupted if interrupt error" do
assert !@instance.interrupted?
@instance.interrupt!
assert @instance.interrupted?
end
end