Merge branch 'runners' which brings in the "actions" model.

This commit is contained in:
Mitchell Hashimoto 2010-02-15 15:25:22 -08:00
commit 3c42db7dfe
23 changed files with 852 additions and 410 deletions

View File

@ -1,10 +1,10 @@
libdir = File.dirname(__FILE__)
$:.unshift(libdir)
PROJECT_ROOT = File.join(libdir, '..')
PROJECT_ROOT = File.join(libdir, '..') unless defined?(PROJECT_ROOT)
# The libs which must be loaded prior to the rest
%w{ftools json pathname logger virtualbox net/ssh
net/scp tarruby fileutils vagrant/util}.each do |f|
net/scp tarruby fileutils vagrant/util vagrant/actions/base}.each do |f|
require f
end

View File

@ -0,0 +1,51 @@
module Vagrant
module Actions
# Base class for any command actions. A command action handles
# executing a step or steps on a given Vagrant::VM object. The
# action should define any callbacks that it will call, or
# attach itself to some callbacks on the VM object.
class Base
attr_reader :vm
# Included so subclasses don't need to include it themselves.
include Vagrant::Util
# Initialization of the actions are done all at once. The guarantee
# is that when an action is initialized, no other action has had
# its `prepare` or `execute!` method called yet, so an action can
# setup anything it needs to with this safety. An example of this
# would be instance_evaling the vm instance to include a module so
# additionally functionality could be defined on the vm which other
# action `prepare` methods may rely on.
def initialize(vm)
@vm = vm
end
# This method is called once per action, allowing the action
# to setup any callbacks, add more events, etc. Prepare is
# called in the order the actions are defined, and the action
# itself has no control over this, so no race conditions between
# action setups should be done here.
def prepare
# Examples:
#
# Perhaps we need an additional action to go, specifically
# maybe only if a configuration is set
#
#@vm.actions << FooAction if Vagrant.config[:foo] == :bar
end
# This method is called once, after preparing, to execute the
# actual task. This method is responsible for calling any
# callbacks. Adding new actions here will have NO EFFECT, and
# adding callbacks has unpredictable effects.
def execute!
# Example code:
#
# @vm.invoke_callback(:before_oven, "cookies")
# Do lots of stuff here
# @vm.invoke_callback(:after_oven, "more", "than", "one", "option")
end
end
end
end

View File

@ -0,0 +1,20 @@
module Vagrant
module Actions
class ForwardPorts < Base
def execute!
logger.info "Forwarding ports..."
Vagrant.config.vm.forwarded_ports.each do |name, options|
logger.info "Forwarding \"#{name}\": #{options[:guestport]} => #{options[:hostport]}"
port = VirtualBox::ForwardedPort.new
port.name = name
port.hostport = options[:hostport]
port.guestport = options[:guestport]
@vm.vm.forwarded_ports << port
end
@vm.vm.save(true)
end
end
end
end

View File

@ -0,0 +1,14 @@
module Vagrant
module Actions
class Import < Base
def execute!
@vm.invoke_callback(:before_import)
logger.info "Importing base VM (#{Vagrant.config[:vm][:base]})..."
@vm.vm = VirtualBox::VM.import(File.expand_path(Vagrant.config[:vm][:base]))
@vm.invoke_callback(:after_import)
end
end
end
end

View File

@ -0,0 +1,43 @@
module Vagrant
module Actions
class MoveHardDrive < Base
def execute!
unless @vm.powered_off?
error_and_exit(<<-error)
The virtual machine must be powered off to move its disk.
error
return
end
destroy_drive_after { clone_and_attach }
end
# TODO: Better way to detect main bootup drive?
def hard_drive
@vm.storage_controllers.first.devices.first
end
def clone_and_attach
logger.info "Cloning current VM Disk to new location (#{new_image_path})..."
hard_drive.image = hard_drive.image.clone(new_image_path, Vagrant.config.vm.disk_image_format, true)
logger.info "Attaching new disk to VM ..."
@vm.vm.save
end
def destroy_drive_after
old_image = hard_drive.image
yield
logger.info "Destroying old VM Disk (#{old_image.filename})..."
old_image.destroy(true)
end
# Returns the path to the new location for the hard drive
def new_image_path
File.join(Vagrant.config.vm.hd_location, hard_drive.image.filename)
end
end
end
end

View File

@ -0,0 +1,56 @@
module Vagrant
module Actions
class Provision < Base
def execute!
chown_provisioning_folder
setup_json
setup_solo_config
run_chef_solo
end
def chown_provisioning_folder
logger.info "Setting permissions on provisioning folder..."
SSH.execute do |ssh|
ssh.exec!("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}")
end
end
def setup_json
logger.info "Generating JSON and uploading..."
json = { :project_directory => Vagrant.config.vm.project_directory }.merge(Vagrant.config.chef.json).to_json
SSH.upload!(StringIO.new(json), File.join(Vagrant.config.chef.provisioning_path, "dna.json"))
end
def setup_solo_config
solo_file = <<-solo
file_cache_path "#{Vagrant.config.chef.provisioning_path}"
cookbook_path "#{cookbooks_path}"
solo
logger.info "Uploading chef-solo configuration script..."
SSH.upload!(StringIO.new(solo_file), File.join(Vagrant.config.chef.provisioning_path, "solo.rb"))
end
def run_chef_solo
logger.info "Running chef recipes..."
SSH.execute do |ssh|
ssh.exec!("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json") do |channel, data, stream|
# TODO: Very verbose. It would be easier to save the data and only show it during
# an error, or when verbosity level is set high
logger.info("#{stream}: #{data}")
end
end
end
def cookbooks_path
File.join(Vagrant.config.chef.provisioning_path, "cookbooks")
end
def collect_shared_folders
["vagrant-provisioning", File.expand_path(Vagrant.config.chef.cookbooks_path, Env.root_path), cookbooks_path]
end
end
end
end

View File

@ -0,0 +1,47 @@
module Vagrant
module Actions
class SharedFolders < Base
def shared_folders
shared_folders = @vm.invoke_callback(:collect_shared_folders)
# Basic filtering of shared folders. Basically only verifies that
# the result is an array of 3 elements. In the future this should
# also verify that the host path exists, the name is valid,
# and that the guest path is valid.
shared_folders.collect do |folder|
if folder.is_a?(Array) && folder.length == 3
folder
else
nil
end
end.compact
end
def before_boot
logger.info "Creating shared folders metadata..."
shared_folders.each do |name, hostpath, guestpath|
folder = VirtualBox::SharedFolder.new
folder.name = name
folder.hostpath = hostpath
@vm.vm.shared_folders << folder
end
@vm.vm.save(true)
end
def after_boot
logger.info "Mounting shared folders..."
Vagrant::SSH.execute do |ssh|
shared_folders.each do |name, hostpath, guestpath|
logger.info "-- #{name}: #{guestpath}"
ssh.exec!("sudo mkdir -p #{guestpath}")
ssh.exec!("sudo mount -t vboxsf #{name} #{guestpath}")
ssh.exec!("sudo chown #{Vagrant.config.ssh.username} #{guestpath}")
end
end
end
end
end
end

View File

@ -0,0 +1,45 @@
module Vagrant
module Actions
class Start < Base
def execute!
@vm.invoke_callback(:before_boot)
# Startup the VM
boot
# Wait for it to complete booting, or error if we could
# never detect it booted up successfully
if !wait_for_boot
error_and_exit(<<-error)
Failed to connect to VM! Failed to boot?
error
end
@vm.invoke_callback(:after_boot)
end
def boot
logger.info "Booting VM..."
@vm.vm.start(:headless, true)
end
def wait_for_boot(sleeptime=5)
logger.info "Waiting for VM to boot..."
Vagrant.config[:ssh][:max_tries].to_i.times do |i|
logger.info "Trying to connect (attempt ##{i+1} of #{Vagrant.config[:ssh][:max_tries]})..."
if Vagrant::SSH.up?
logger.info "VM booted and ready for use!"
return true
end
sleep sleeptime
end
logger.info "Failed to connect to VM! Failed to boot?"
false
end
end
end
end

37
lib/vagrant/actions/up.rb Normal file
View File

@ -0,0 +1,37 @@
module Vagrant
module Actions
class Up < Base
def prepare
# Up is a "meta-action" so it really just queues up a bunch
# of other actions in its place:
steps = [Import, ForwardPorts, SharedFolders, Start]
steps.insert(1, MoveHardDrive) if Vagrant.config.vm.hd_location
steps.each do |action_klass|
@vm.add_action(action_klass)
end
end
def collect_shared_folders
# The root shared folder for the project
["vagrant-root", Env.root_path, Vagrant.config.vm.project_directory]
end
def after_import
persist
setup_mac_address
end
def persist
logger.info "Persisting the VM UUID (#{@vm.vm.uuid})..."
Env.persist_vm(@vm.vm)
end
def setup_mac_address
logger.info "Matching MAC addresses..."
@vm.vm.nics.first.macaddress = Vagrant.config[:vm][:base_mac]
@vm.vm.save(true)
end
end
end
end

View File

@ -40,7 +40,7 @@ run `vagrant down` first.
error
end
VM.up
VM.execute!(Actions::Up)
end
# Tear down a vagrant instance. This not only shuts down the instance
@ -99,7 +99,7 @@ error
end
# Export and package the current vm
#
#
# This command requires that an instance be powered off
def package(name=nil)
Env.load!

View File

@ -1,60 +0,0 @@
module Vagrant
class Provisioning
include Vagrant::Util
def initialize(vm)
@vm = vm
# Share the cookbook folder. We'll use the provisioning path exclusively for
# chef stuff.
@vm.share_folder("vagrant-provisioning", File.expand_path(Vagrant.config.chef.cookbooks_path, Env.root_path), cookbooks_path)
end
def run
chown_provisioning_folder
setup_json
setup_solo_config
run_chef_solo
end
def chown_provisioning_folder
logger.info "Setting permissions on provisioning folder..."
SSH.execute do |ssh|
ssh.exec!("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}")
end
end
def setup_json
logger.info "Generating JSON and uploading..."
json = { :project_directory => Vagrant.config.vm.project_directory }.merge(Vagrant.config.chef.json).to_json
SSH.upload!(StringIO.new(json), File.join(Vagrant.config.chef.provisioning_path, "dna.json"))
end
def setup_solo_config
solo_file = <<-solo
file_cache_path "#{Vagrant.config.chef.provisioning_path}"
cookbook_path "#{cookbooks_path}"
solo
logger.info "Uploading chef-solo configuration script..."
SSH.upload!(StringIO.new(solo_file), File.join(Vagrant.config.chef.provisioning_path, "solo.rb"))
end
def run_chef_solo
logger.info "Running chef recipes..."
SSH.execute do |ssh|
ssh.exec!("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json") do |channel, data, stream|
# TODO: Very verbose. It would be easier to save the data and only show it during
# an error, or when verbosity level is set high
logger.info("#{stream}: #{data}")
end
end
end
def cookbooks_path
File.join(Vagrant.config.chef.provisioning_path, "cookbooks")
end
end
end

View File

@ -1,16 +1,17 @@
module Vagrant
class VM
include Vagrant::Util
attr_reader :vm
attr_accessor :vm
attr_reader :actions
attr_accessor :from
class << self
# Bring up the virtual machine. Imports the base image and
# provisions it.
def up(from=Vagrant.config[:vm][:base])
# Executes a specific action
def execute!(action_klass)
vm = new
vm.from = from
vm.create
vm.add_action(action_klass)
vm.execute!
end
# Unpack the specified vm package
@ -21,20 +22,20 @@ module Vagrant
# Exit if folder of same name exists
# TODO provide a way for them to specify the directory name
error_and_exit(<<-error) if File.exists?(new_base_dir)
The directory `#{File.basename(package_path, '.*')}` already exists under #{Vagrant.config[:vagrant][:home]}. Please
remove it, rename your packaged VM file, or (TODO) specifiy an
alternate directory
The directory `#{File.basename(package_path, '.*')}` already exists under #{Vagrant.config[:vagrant][:home]}. Please
remove it, rename your packaged VM file, or (TODO) specifiy an
alternate directory
error
logger.info "Creating working dir: #{working_dir} ..."
FileUtils.mkpath(working_dir)
logger.info "Decompressing the packaged VM: #{package_path} ..."
decompress(package_path, working_dir)
logger.info "Moving the unpackaged VM to #{new_base_dir} ..."
FileUtils.mv(working_dir, Vagrant.config[:vagrant][:home])
#Return the ovf file for importation
Dir["#{new_base_dir}/*.ovf"].first
end
@ -44,14 +45,14 @@ error
Zlib::GzipReader.open(path) do |gz|
begin
gz.each_line do |line|
# If the line is a file delimiter create new file and write to it
if line =~ file_delimeter
#Write the the part of the line belonging to the previous file
if file
file.write $1
file.close
file.close
end
#Open a new file with the name contained in the delimiter
@ -80,27 +81,11 @@ error
def initialize(vm=nil)
@vm = vm
@actions = []
end
def create
share_folder("vagrant-root", Env.root_path, Vagrant.config.vm.project_directory)
# Create the provisioning object, prior to doing anything so it can
# set any configuration on the VM object prior to creation
provisioning = Provisioning.new(self)
# The path of righteousness
import
move_hd if Vagrant.config[:vm][:hd_location]
persist
setup_mac_address
forward_ports
setup_shared_folders
start
mount_shared_folders
# Once we're started, run the provisioning
provisioning.run
def add_action(action_klass)
@actions << action_klass.new(self)
end
def destroy
@ -113,112 +98,35 @@ error
@vm.destroy(:destroy_image => true)
end
def move_hd
error_and_exit(<<-error) unless @vm.powered_off?
The virtual machine must be powered off to move its disk.
error
old_image = hd.image.dup
new_image_file = Vagrant.config[:vm][:hd_location] + old_image.filename
logger.info "Cloning current VM Disk to new location (#{ new_image_file })..."
# TODO image extension default?
new_image = hd.image.clone(new_image_file , Vagrant.config[:vm][:disk_image_format], true)
hd.image = new_image
logger.info "Attaching new disk to VM ..."
@vm.save
logger.info "Destroying old VM Disk (#{ old_image.filename })..."
old_image.destroy(true)
end
def import
logger.info "Importing base VM (#{Vagrant.config[:vm][:base]})..."
@vm = VirtualBox::VM.import(@from)
end
def persist
logger.info "Persisting the VM UUID (#{@vm.uuid})..."
Env.persist_vm(@vm)
end
def setup_mac_address
logger.info "Matching MAC addresses..."
@vm.nics.first.macaddress = Vagrant.config[:vm][:base_mac]
@vm.save(true)
end
def forward_ports
logger.info "Forwarding ports..."
Vagrant.config.vm.forwarded_ports.each do |name, options|
logger.info "Forwarding \"#{name}\": #{options[:guestport]} => #{options[:hostport]}"
port = VirtualBox::ForwardedPort.new
port.name = name
port.hostport = options[:hostport]
port.guestport = options[:guestport]
@vm.forwarded_ports << port
end
@vm.save(true)
end
def setup_shared_folders
logger.info "Creating shared folders metadata..."
shared_folders.each do |name, hostpath, guestpath|
folder = VirtualBox::SharedFolder.new
folder.name = name
folder.hostpath = hostpath
@vm.shared_folders << folder
end
@vm.save(true)
end
def mount_shared_folders
logger.info "Mounting shared folders..."
Vagrant::SSH.execute do |ssh|
shared_folders.each do |name, hostpath, guestpath|
logger.info "-- #{name}: #{guestpath}"
ssh.exec!("sudo mkdir -p #{guestpath}")
ssh.exec!("sudo mount -t vboxsf #{name} #{guestpath}")
ssh.exec!("sudo chown #{Vagrant.config.ssh.username} #{guestpath}")
def execute!
# Call the prepare method on each once its
# initialized, then call the execute! method
[:prepare, :execute!].each do |method|
@actions.each do |action|
action.send(method)
end
end
end
def start(sleep_interval = 5)
logger.info "Booting VM..."
@vm.start(:headless, true)
# Now we have to wait for the boot to be successful
logger.info "Waiting for VM to boot..."
Vagrant.config[:ssh][:max_tries].to_i.times do |i|
logger.info "Trying to connect (attempt ##{i+1} of #{Vagrant.config[:ssh][:max_tries]})..."
if Vagrant::SSH.up?
logger.info "VM booted and ready for use!"
return true
end
sleep sleep_interval
def invoke_callback(name, *args)
# Attempt to call the method for the callback on each of the
# actions
results = []
@actions.each do |action|
results << action.send(name, *args) if action.respond_to?(name)
end
logger.info "Failed to connect to VM! Failed to boot?"
false
results
end
def shared_folders(clear=false)
@shared_folders = nil if clear
@shared_folders ||= []
end
def destroy
if @vm.running?
logger.info "VM is running. Forcing immediate shutdown..."
@vm.stop(true)
end
def share_folder(name, hostpath, guestpath)
shared_folders << [name, hostpath, guestpath]
logger.info "Destroying VM and associated drives..."
@vm.destroy(:destroy_image => true)
end
def saved?
@ -238,10 +146,10 @@ error
ovf_path = File.join(folder, "#{name}.ovf")
tar_path = "#{folder}.box"
logger.info "Exporting required VM files to working directory ..."
@vm.export(ovf_path)
logger.info "Packaging VM into #{tar_path} ..."
Zlib::GzipWriter.open(tar_path) do |gz|
first_file = true
@ -249,7 +157,7 @@ error
next if File.directory?(file)
# Delimit the files, and guarantee new line for next file if not the first
gz.write "#{delimiter}#{file}#{delimiter}"
File.open(File.join(folder, file)).each { |line| gz.write(line) }
File.open(File.join(folder, file)).each { |line| gz.write(line) }
first_file = false
end
end
@ -260,12 +168,6 @@ error
tar_path
end
# TODO need a better way to which controller is the hd
def hd
@vm.storage_controllers.first.devices.first
end
def powered_off?; @vm.powered_off? end
def export(filename); @vm.export(filename, {}, true) end

View File

@ -18,6 +18,7 @@ require 'contest'
require 'mocha'
class Test::Unit::TestCase
# Clears the previous config and sets up the new config
def mock_config
Vagrant::Config.instance_variable_set(:@config_runners, nil)
Vagrant::Config.instance_variable_set(:@config, nil)
@ -34,6 +35,7 @@ class Test::Unit::TestCase
config.vm.base = "foo"
config.vm.base_mac = "42"
config.vm.project_directory = "/hobo"
config.vm.disk_image_format = 'VMDK'
config.vm.forward_port("ssh", 22, 2222)
config.package.delimiter = 'V'
@ -48,6 +50,24 @@ class Test::Unit::TestCase
config.vagrant.home = '~/.home'
end
if block_given?
Vagrant::Config.run do |config|
yield config
end
end
Vagrant::Config.execute!
end
# Sets up the mocks and instantiates an action for testing
def mock_action(action_klass)
@vm = mock("vboxvm")
@mock_vm = mock("vm")
@mock_vm.stubs(:vm).returns(@vm)
@mock_vm.stubs(:vm=)
@mock_vm.stubs(:invoke_callback)
@action = action_klass.new(@mock_vm)
[@mock_vm, @vm, @action]
end
end

View File

@ -0,0 +1,32 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class BaseActionTest < Test::Unit::TestCase
should "include the util class so subclasses have access to it" do
assert Vagrant::Actions::Base.include?(Vagrant::Util)
end
context "base instance" do
setup do
@mock_vm = mock("vm")
@base = Vagrant::Actions::Base.new(@mock_vm)
end
should "allow read-only access to the VM" do
assert_equal @mock_vm, @base.vm
end
should "implement prepare which does nothing" do
assert_nothing_raised do
assert @base.respond_to?(:prepare)
@base.prepare
end
end
should "implement the execute! method which does nothing" do
assert_nothing_raised do
assert @base.respond_to?(:execute!)
@base.execute!
end
end
end
end

View File

@ -0,0 +1,25 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class ForwardPortsActionTest < Test::Unit::TestCase
setup do
@mock_vm, @vm, @action = mock_action(Vagrant::Actions::ForwardPorts)
mock_config
end
should "create a port forwarding for the VM" do
forwarded_ports = mock("forwarded_ports")
Vagrant.config.vm.forwarded_ports.each do |name, opts|
forwarded_ports.expects(:<<).with do |port|
assert_equal name, port.name
assert_equal opts[:hostport], port.hostport
assert_equal opts[:guestport], port.guestport
true
end
end
@vm.expects(:forwarded_ports).returns(forwarded_ports)
@vm.expects(:save).with(true).once
@action.execute!
end
end

View File

@ -0,0 +1,29 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class ImportActionTest < Test::Unit::TestCase
setup do
@mock_vm, @vm, @import = mock_action(Vagrant::Actions::Import)
VirtualBox::VM.stubs(:import)
end
should "invoke before/after callbacks around the import" do
callback_seq = sequence("callback_seq")
@mock_vm.expects(:invoke_callback).with(:before_import).once.in_sequence(callback_seq)
VirtualBox::VM.expects(:import).once.in_sequence(callback_seq)
@mock_vm.expects(:invoke_callback).with(:after_import).once.in_sequence(callback_seq)
@import.execute!
end
should "call import on VirtualBox::VM with the proper base" do
VirtualBox::VM.expects(:import).once
@import.execute!
end
should "set the resulting VM as the VM of the Vagrant VM object" do
new_vm = mock("new_vm")
@mock_vm.expects(:vm=).with(new_vm).once
VirtualBox::VM.expects(:import).returns(new_vm)
@import.execute!
end
end

View File

@ -0,0 +1,93 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class MoveHardDriveActionTest < Test::Unit::TestCase
setup do
@mock_vm, @vm, @action = mock_action(Vagrant::Actions::MoveHardDrive)
@hd_location = "/foo"
mock_config do |config|
File.expects(:directory?).with(@hd_location).returns(true)
config.vm.hd_location = @hd_location
end
end
context "execution" do
should "error and exit if the vm is not powered off" do
@mock_vm.expects(:powered_off?).returns(false)
@action.expects(:error_and_exit).once
@action.execute!
end
should "move the hard drive if vm is powered off" do
@mock_vm.expects(:powered_off?).returns(true)
@action.expects(:error_and_exit).never
@action.expects(:destroy_drive_after).once
@action.execute!
end
end
context "new image path" do
setup do
@hd = mock("hd")
@image = mock("image")
@filename = "foo"
@hd.stubs(:image).returns(@image)
@image.stubs(:filename).returns(@filename)
@action.stubs(:hard_drive).returns(@hd)
end
should "be the configured hd location and the existing hard drive filename" do
joined = File.join(Vagrant.config.vm.hd_location, @filename)
assert_equal joined, @action.new_image_path
end
end
context "cloning and attaching new image" do
setup do
@hd = mock("hd")
@image = mock("image")
@hd.stubs(:image).returns(@image)
@action.stubs(:hard_drive).returns(@hd)
@new_image_path = "foo"
@action.stubs(:new_image_path).returns(@new_image_path)
end
should "clone to the new path" do
new_image = mock("new_image")
@image.expects(:clone).with(@new_image_path, Vagrant.config.vm.disk_image_format, true).returns(new_image).once
@hd.expects(:image=).with(new_image).once
@vm.expects(:save).once
@action.clone_and_attach
end
end
context "destroying the old image" do
setup do
@hd = mock("hd")
@action.stubs(:hard_drive).returns(@hd)
end
should "yield the block, and destroy the old image after" do
image = mock("image")
image.stubs(:filename).returns("foo")
destroy_seq = sequence("destroy_seq")
@hd.expects(:image).returns(image).in_sequence(destroy_seq)
@hd.expects(:foo).once.in_sequence(destroy_seq)
image.expects(:destroy).with(true).once.in_sequence(destroy_seq)
@action.destroy_drive_after { @hd.foo }
end
# Ensures that the image is not destroyed in an "ensure" block
should "not destroy the image if an exception is raised" do
image = mock("image")
image.expects(:destroy).never
@hd.expects(:image).returns(image)
assert_raises(Exception) do
@action.destroy_drive_after do
raise Exception.new("FOO")
end
end
end
end
end

View File

@ -1,27 +1,27 @@
require File.join(File.dirname(__FILE__), '..', 'test_helper')
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class ProvisioningTest < Test::Unit::TestCase
class ProvisionActionTest < Test::Unit::TestCase
setup do
# Stub upload so nothing happens
@mock_vm, @vm, @action = mock_action(Vagrant::Actions::Provision)
Vagrant::SSH.stubs(:execute)
Vagrant::SSH.stubs(:upload!)
vm = mock("vm")
vm.stubs(:share_folder)
@prov = Vagrant::Provisioning.new(vm)
mock_config
end
context "initializing" do
context "shared folders" do
should "setup shared folder on VM for the cookbooks" do
File.expects(:expand_path).with(Vagrant.config.chef.cookbooks_path, Vagrant::Env.root_path).returns("foo")
Vagrant::Provisioning.any_instance.expects(:cookbooks_path).returns("bar")
vm = mock("vm")
vm.expects(:share_folder).with("vagrant-provisioning", "foo", "bar")
Vagrant::Provisioning.new(vm)
@action.expects(:cookbooks_path).returns("bar")
assert_equal ["vagrant-provisioning", "foo", "bar"], @action.collect_shared_folders
end
end
context "cookbooks path" do
should "return the proper cookbook path" do
cookbooks_path = File.join(Vagrant.config.chef.provisioning_path, "cookbooks")
assert_equal cookbooks_path, @prov.cookbooks_path
assert_equal cookbooks_path, @action.cookbooks_path
end
end
@ -30,14 +30,14 @@ class ProvisioningTest < Test::Unit::TestCase
ssh = mock("ssh")
ssh.expects(:exec!).with("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}")
Vagrant::SSH.expects(:execute).yields(ssh)
@prov.chown_provisioning_folder
@action.chown_provisioning_folder
end
end
context "generating and uploading json" do
should "convert the JSON config to JSON" do
Hash.any_instance.expects(:to_json).once.returns("foo")
@prov.setup_json
@action.setup_json
end
should "add the project directory to the JSON" do
@ -47,14 +47,14 @@ class ProvisioningTest < Test::Unit::TestCase
true
end
@prov.setup_json
@action.setup_json
end
should "upload a StringIO to dna.json" do
StringIO.expects(:new).with(anything).returns("bar")
File.expects(:join).with(Vagrant.config.chef.provisioning_path, "dna.json").once.returns("baz")
Vagrant::SSH.expects(:upload!).with("bar", "baz").once
@prov.setup_json
@action.setup_json
end
end
@ -62,19 +62,19 @@ class ProvisioningTest < Test::Unit::TestCase
should "upload properly generate the configuration file using configuration data" do
expected_config = <<-config
file_cache_path "#{Vagrant.config.chef.provisioning_path}"
cookbook_path "#{@prov.cookbooks_path}"
cookbook_path "#{@action.cookbooks_path}"
config
StringIO.expects(:new).with(expected_config).once
@prov.setup_solo_config
@action.setup_solo_config
end
should "upload this file as solo.rb to the provisioning folder" do
@prov.expects(:cookbooks_path).returns("cookbooks")
@action.expects(:cookbooks_path).returns("cookbooks")
StringIO.expects(:new).returns("foo")
File.expects(:join).with(Vagrant.config.chef.provisioning_path, "solo.rb").once.returns("bar")
Vagrant::SSH.expects(:upload!).with("foo", "bar").once
@prov.setup_solo_config
@action.setup_solo_config
end
end
@ -83,7 +83,7 @@ config
ssh = mock("ssh")
ssh.expects(:exec!).with("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json").once
Vagrant::SSH.expects(:execute).yields(ssh)
@prov.run_chef_solo
@action.run_chef_solo
end
end
end

View File

@ -0,0 +1,64 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class SharedFoldersActionTest < Test::Unit::TestCase
setup do
@mock_vm, @vm, @action = mock_action(Vagrant::Actions::SharedFolders)
mock_config
end
def stub_shared_folders
folders = [%w{foo from to}, %w{bar bfrom bto}]
@action.expects(:shared_folders).returns(folders)
folders
end
context "collecting shared folders" do
should "return the arrays that the callback returns" do
result = [[1,2,3],[4,5,6]]
@mock_vm.expects(:invoke_callback).with(:collect_shared_folders).once.returns(result)
assert_equal result, @action.shared_folders
end
should "filter out invalid results" do
result = [[1,2,3],[4,5]]
@mock_vm.expects(:invoke_callback).with(:collect_shared_folders).once.returns(result)
assert_equal [[1,2,3]], @action.shared_folders
end
end
context "setting up shared folder metadata" do
setup do
@folders = stub_shared_folders
end
should "add all shared folders to the VM" do
share_seq = sequence("share_seq")
shared_folders = mock("shared_folders")
shared_folders.expects(:<<).in_sequence(share_seq).with() { |sf| sf.name == "foo" && sf.hostpath == "from" }
shared_folders.expects(:<<).in_sequence(share_seq).with() { |sf| sf.name == "bar" && sf.hostpath == "bfrom" }
@vm.stubs(:shared_folders).returns(shared_folders)
@vm.expects(:save).with(true).once
@action.before_boot
end
end
context "mounting the shared folders" do
setup do
@folders = stub_shared_folders
end
should "mount all shared folders to the VM" do
mount_seq = sequence("mount_seq")
ssh = mock("ssh")
@folders.each do |name, hostpath, guestpath|
ssh.expects(:exec!).with("sudo mkdir -p #{guestpath}").in_sequence(mount_seq)
ssh.expects(:exec!).with("sudo mount -t vboxsf #{name} #{guestpath}").in_sequence(mount_seq)
ssh.expects(:exec!).with("sudo chown #{Vagrant.config.ssh.username} #{guestpath}").in_sequence(mount_seq)
end
Vagrant::SSH.expects(:execute).yields(ssh)
@action.after_boot
end
end
end

View File

@ -0,0 +1,50 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class StartActionTest < Test::Unit::TestCase
setup do
@mock_vm, @vm, @action = mock_action(Vagrant::Actions::Start)
@mock_vm.stubs(:invoke_callback)
mock_config
end
context "execution" do
should "invoke before callback, boot, and invoke the after callback" do
boot_seq = sequence("boot_seq")
@mock_vm.expects(:invoke_callback).with(:before_boot).once.in_sequence(boot_seq)
@action.expects(:boot).in_sequence(boot_seq)
@action.expects(:wait_for_boot).returns(true).in_sequence(boot_seq)
@mock_vm.expects(:invoke_callback).with(:after_boot).once.in_sequence(boot_seq)
@action.execute!
end
should "error and exit if the bootup failed" do
fail_boot_seq = sequence("fail_boot_seq")
@action.expects(:boot).once.in_sequence(fail_boot_seq)
@action.expects(:wait_for_boot).returns(false).in_sequence(fail_boot_seq)
@action.expects(:invoke_callback).with(:after_boot).never
@action.expects(:error_and_exit).once.in_sequence(fail_boot_seq)
@action.execute!
end
end
context "booting" do
should "start the VM in headless mode" do
@vm.expects(:start).with(:headless, true).once
@action.boot
end
end
context "waiting for boot" do
should "repeatedly ping the SSH port and return false with no response" do
seq = sequence('pings')
Vagrant::SSH.expects(:up?).times(Vagrant.config[:ssh][:max_tries].to_i - 1).returns(false).in_sequence(seq)
Vagrant::SSH.expects(:up?).once.returns(true).in_sequence(seq)
assert @action.wait_for_boot(0)
end
should "ping the max number of times then just return" do
Vagrant::SSH.expects(:up?).times(Vagrant.config[:ssh][:max_tries].to_i).returns(false)
assert !@action.wait_for_boot(0)
end
end
end

View File

@ -0,0 +1,71 @@
require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
class UpActionTest < Test::Unit::TestCase
setup do
@mock_vm, @vm, @action = mock_action(Vagrant::Actions::Up)
mock_config
end
context "sub-actions" do
setup do
@default_order = [Vagrant::Actions::Import, Vagrant::Actions::ForwardPorts, Vagrant::Actions::SharedFolders, Vagrant::Actions::Start]
end
def setup_action_expectations
default_seq = sequence("default_seq")
@default_order.each do |action|
@mock_vm.expects(:add_action).with(action).once.in_sequence(default_seq)
end
end
should "do the proper actions by default" do
setup_action_expectations
@action.prepare
end
should "add in the action to move hard drive if config is set" do
mock_config do |config|
File.expects(:directory?).with("foo").returns(true)
config.vm.hd_location = "foo"
end
@default_order.insert(1, Vagrant::Actions::MoveHardDrive)
setup_action_expectations
@action.prepare
end
end
context "callbacks" do
should "call persist and mac address setup after import" do
boot_seq = sequence("boot")
@action.expects(:persist).once.in_sequence(boot_seq)
@action.expects(:setup_mac_address).once.in_sequence(boot_seq)
@action.after_import
end
should "setup the root directory shared folder" do
expected = ["vagrant-root", Vagrant::Env.root_path, Vagrant.config.vm.project_directory]
assert_equal expected, @action.collect_shared_folders
end
end
context "persisting" do
should "persist the VM with Env" do
@vm.stubs(:uuid)
Vagrant::Env.expects(:persist_vm).with(@vm).once
@action.persist
end
end
context "setting up MAC address" do
should "match the mac address with the base" do
nic = mock("nic")
nic.expects(:macaddress=).once
@vm.expects(:nics).returns([nic]).once
@vm.expects(:save).with(true).once
@action.setup_mac_address
end
end
end

View File

@ -31,7 +31,7 @@ class CommandsTest < Test::Unit::TestCase
context "up" do
setup do
Vagrant::Env.stubs(:persisted_vm).returns(nil)
Vagrant::VM.stubs(:up)
Vagrant::VM.stubs(:execute!)
end
should "require load the environment" do
@ -46,8 +46,8 @@ class CommandsTest < Test::Unit::TestCase
Vagrant::Commands.up
end
should "call up on VM" do
Vagrant::VM.expects(:up).once
should "call the up action on VM" do
Vagrant::VM.expects(:execute!).with(Vagrant::Actions::Up).once
Vagrant::Commands.up
end
end
@ -149,13 +149,13 @@ class CommandsTest < Test::Unit::TestCase
@persisted_vm.expects(:package).never
Vagrant::Commands.package
end
should "package the vm with the default name and the current directory" do
@persisted_vm.expects(:package).with(Vagrant.config[:package][:name], FileUtils.pwd).once
Vagrant::Commands.package
end
should "package the vm with the specified name" do
should "package the vm with the specified name" do
@persisted_vm.expects(:package).with('foo', FileUtils.pwd).once
Vagrant::Commands.package('foo')
end

View File

@ -11,13 +11,88 @@ class VMTest < Test::Unit::TestCase
Net::SSH.stubs(:start)
end
context "vagrant up" do
should "create a Vagrant::VM instance and call create" do
inst = mock("instance")
inst.expects(:create).once
inst.expects(:from=).with(File.expand_path(Vagrant.config[:vm][:base]))
Vagrant::VM.expects(:new).returns(inst)
Vagrant::VM.up
context "callbacks" do
setup do
@vm = Vagrant::VM.new(@mock_vm)
end
should "not invoke callback on actions which don't respond to it" do
action = mock("action")
action.stubs(:respond_to?).with(:foo).returns(false)
action.expects(:foo).never
assert_nothing_raised do
@vm.actions << action
@vm.invoke_callback(:foo)
end
end
should "invoke callback on actions which do respond to the method" do
action = mock("action")
action.expects(:foo).once
@vm.actions << action
@vm.invoke_callback(:foo)
end
should "collect all the results and return them as an array" do
result = []
3.times do |i|
action = mock("action#{i}")
action.expects(:foo).returns("foo#{i}").once
@vm.actions << action
result << "foo#{i}"
end
assert_equal result, @vm.invoke_callback(:foo)
end
end
context "actions" do
setup do
@vm = Vagrant::VM.new(@mock_vm)
end
should "be empty initially" do
assert @vm.actions.empty?
end
should "initialize the action when added" do
action_klass = mock("action_class")
action_inst = mock("action_inst")
action_klass.expects(:new).once.returns(action_inst)
@vm.add_action(action_klass)
assert_equal 1, @vm.actions.length
end
should "run #prepare on all actions, then #execute!" do
action_seq = sequence("action_seq")
actions = []
5.times do |i|
action = mock("action#{i}")
@vm.actions << action
actions << action
end
[:prepare, :execute!].each do |method|
actions.each do |action|
action.expects(method).once.in_sequence(action_seq)
end
end
@vm.execute!
end
should "run actions on class method execute!" do
vm = mock("vm")
execute_seq = sequence("execute_seq")
Vagrant::VM.expects(:new).returns(vm).in_sequence(execute_seq)
vm.expects(:add_action).with("foo").in_sequence(execute_seq)
vm.expects(:execute!).once.in_sequence(execute_seq)
Vagrant::VM.execute!("foo")
end
end
@ -40,23 +115,6 @@ class VMTest < Test::Unit::TestCase
@vm = Vagrant::VM.new(@mock_vm)
end
context "creating" do
should "create the VM in the proper order" do
prov = mock("prov")
create_seq = sequence("create_seq")
Vagrant::Provisioning.expects(:new).with(@vm).in_sequence(create_seq).returns(prov)
@vm.expects(:import).in_sequence(create_seq)
@vm.expects(:persist).in_sequence(create_seq)
@vm.expects(:setup_mac_address).in_sequence(create_seq)
@vm.expects(:forward_ports).in_sequence(create_seq)
@vm.expects(:setup_shared_folders).in_sequence(create_seq)
@vm.expects(:start).in_sequence(create_seq)
@vm.expects(:mount_shared_folders).in_sequence(create_seq)
prov.expects(:run).in_sequence(create_seq)
@vm.create
end
end
context "destroying" do
setup do
@mock_vm.stubs(:running?).returns(false)
@ -75,127 +133,6 @@ class VMTest < Test::Unit::TestCase
end
end
context "starting" do
setup do
@mock_vm.stubs(:start)
end
should "start the VM in headless mode" do
@mock_vm.expects(:start).with(:headless, true).once
Vagrant::SSH.expects(:up?).once.returns(true)
@vm.start(0)
end
should "repeatedly ping the SSH port and return false with no response" do
seq = sequence('pings')
Vagrant::SSH.expects(:up?).times(Vagrant.config[:ssh][:max_tries].to_i - 1).returns(false).in_sequence(seq)
Vagrant::SSH.expects(:up?).once.returns(true).in_sequence(seq)
assert @vm.start(0)
end
should "ping the max number of times then just return" do
Vagrant::SSH.expects(:up?).times(Vagrant.config[:ssh][:max_tries].to_i).returns(false)
assert !@vm.start(0)
end
end
context "importing" do
should "call import on VirtualBox::VM with the proper base" do
VirtualBox::VM.expects(:import).once
@vm.import
end
should "return the VM object" do
VirtualBox::VM.expects(:import).returns(@mock_vm).once
assert_equal @mock_vm, @vm.import
end
end
context "persisting" do
should "persist the VM with Env" do
@mock_vm.stubs(:uuid)
Vagrant::Env.expects(:persist_vm).with(@mock_vm).once
@vm.persist
end
end
context "setting up MAC address" do
should "match the mac address with the base" do
nic = mock("nic")
nic.expects(:macaddress=).once
@mock_vm.expects(:nics).returns([nic]).once
@mock_vm.expects(:save).with(true).once
@vm.setup_mac_address
end
end
context "forwarding ports" do
should "create a port forwarding for the VM" do
# TODO: Test the actual port value to make sure it has the
# correct attributes
forwarded_ports = mock("forwarded_ports")
forwarded_ports.expects(:<<)
@mock_vm.expects(:forwarded_ports).returns(forwarded_ports)
@mock_vm.expects(:save).with(true).once
@vm.forward_ports
end
end
context "shared folders" do
setup do
@mock_vm = mock("mock_vm")
@vm = Vagrant::VM.new(@mock_vm)
end
should "not have any shared folders initially" do
assert @vm.shared_folders.empty?
end
should "be able to add shared folders" do
@vm.share_folder("foo", "from", "to")
assert_equal 1, @vm.shared_folders.length
end
should "be able to clear shared folders" do
@vm.share_folder("foo", "from", "to")
assert !@vm.shared_folders.empty?
@vm.shared_folders(true)
assert @vm.shared_folders.empty?
end
should "add all shared folders to the VM with 'setup_shared_folders'" do
@vm.share_folder("foo", "from", "to")
@vm.share_folder("bar", "bfrom", "bto")
share_seq = sequence("share_seq")
shared_folders = mock("shared_folders")
shared_folders.expects(:<<).in_sequence(share_seq).with() { |sf| sf.name == "foo" && sf.hostpath == "from" }
shared_folders.expects(:<<).in_sequence(share_seq).with() { |sf| sf.name == "bar" && sf.hostpath == "bfrom" }
@mock_vm.stubs(:shared_folders).returns(shared_folders)
@mock_vm.expects(:save).with(true).once
@vm.setup_shared_folders
end
should "mount all shared folders to the VM with `mount_shared_folders`" do
@vm.share_folder("foo", "from", "to")
@vm.share_folder("bar", "bfrom", "bto")
mount_seq = sequence("mount_seq")
ssh = mock("ssh")
@vm.shared_folders.each do |name, hostpath, guestpath|
ssh.expects(:exec!).with("sudo mkdir -p #{guestpath}").in_sequence(mount_seq)
ssh.expects(:exec!).with("sudo mount -t vboxsf #{name} #{guestpath}").in_sequence(mount_seq)
ssh.expects(:exec!).with("sudo chown #{Vagrant.config.ssh.username} #{guestpath}").in_sequence(mount_seq)
end
Vagrant::SSH.expects(:execute).yields(ssh)
@vm.mount_shared_folders
end
end
context "saving the state" do
should "check if a VM is saved" do
@mock_vm.expects(:saved?).returns("foo")
@ -207,40 +144,6 @@ class VMTest < Test::Unit::TestCase
@vm.save_state
end
end
context "creating a new vm with a specified disk storage location" do
should "error and exit of the vm is not powered off" do
# Exit does not prevent method from proceeding in test, so we must set expectations
vm = move_hd_expectations
@mock_vm.expects(:powered_off?).returns(false)
vm.expects(:error_and_exit)
vm.move_hd
end
should "create assign a new disk image, and delete the old one" do
vm = move_hd_expectations
@mock_vm.expects(:powered_off?).returns(true)
vm.move_hd
end
def move_hd_expectations
image, hd = mock('image'), mock('hd')
Vagrant.config[:vm].expects(:hd_location).at_least_once.returns('/locations/')
image.expects(:clone).with(Vagrant.config[:vm][:hd_location] + 'foo', Vagrant.config[:vm][:disk_image_format], true).returns(image)
image.expects(:filename).twice.returns('foo')
image.expects(:destroy)
hd.expects(:image).twice.returns(image)
hd.expects(:image=).with(image)
@mock_vm.expects(:save)
vm = Vagrant::VM.new(@mock_vm)
vm.expects(:hd).times(3).returns(hd)
vm
end
end
end
# TODO more comprehensive testing
@ -258,9 +161,9 @@ class VMTest < Test::Unit::TestCase
assert_equal Vagrant::VM.new(@mock_vm).package(name, location), "#{new_dir}.box"
end
end
context "unpackaging a vm" do
# TODO test actual decompression
should "call decompress with the path to the file and the directory to decompress to" do
working_dir = File.join FileUtils.pwd, 'something'