Merge branch 'new-data-tiers'

This branch changes how local data is stored for Vagrant environments
from a single "dotfile" approach to having a directory of data, more
similar in style to git.

When a V1-style dotfile is encountered, it is automatically upgraded
to the new style directory format.
This commit is contained in:
Mitchell Hashimoto 2012-12-26 22:02:31 -08:00
commit 495479f480
12 changed files with 153 additions and 247 deletions

View File

@ -1,6 +1,5 @@
Vagrant.configure("2") do |config|
# default config goes here
config.vagrant.dotfile_name = ".vagrant"
config.vagrant.host = :detect
config.ssh.username = "vagrant"

View File

@ -67,7 +67,6 @@ module Vagrant
autoload :CLI, 'vagrant/cli'
autoload :Command, 'vagrant/command'
autoload :Config, 'vagrant/config'
autoload :DataStore, 'vagrant/data_store'
autoload :Downloaders, 'vagrant/downloaders'
autoload :Driver, 'vagrant/driver'
autoload :Easy, 'vagrant/easy'

View File

@ -1,92 +0,0 @@
require 'pathname'
require 'log4r'
module Vagrant
# The Vagrant data store is a key-value store which is persisted
# as JSON in a local file which is specified in the initializer.
# The data store itself is accessed via typical hash accessors: `[]`
# and `[]=`. If a key is set to `nil`, then it is removed from the
# datastore. The data store is only updated on disk when {#commit}
# is called on the data store itself.
#
# The data store is a hash with indifferent access, meaning that
# while all keys are persisted as strings in the JSON, you can access
# them back as either symbols or strings. Note that this is only true
# for the top-level data store. As soon as you set a hash inside the
# data store, unless you explicitly use a {Util::HashWithIndifferentAccess},
# it will be a regular hash.
class DataStore < Util::HashWithIndifferentAccess
attr_reader :file_path
def initialize(file_path)
@logger = Log4r::Logger.new("vagrant::datastore")
if file_path
@logger.info("Created: #{file_path}")
@file_path = Pathname.new(file_path)
if @file_path.exist?
raise Errors::DotfileIsDirectory if @file_path.directory?
begin
merge!(JSON.parse(@file_path.read))
rescue JSON::ParserError
# Ignore if the data is invalid in the file.
@logger.error("Data store contained invalid JSON. Ignoring.")
end
end
else
@logger.info("No file path. In-memory data store.")
@file_path = nil
end
end
# Commits any changes to the data to disk. Even if the data
# hasn't changed, it will be reserialized and written to disk.
def commit
if !@file_path
raise StandardError, "In-memory data stores can't be committed."
end
clean_nil_and_empties
if empty?
# Delete the file since an empty data store is not useful
@logger.info("Deleting data store since we're empty: #{@file_path}")
@file_path.delete if @file_path.exist?
else
@logger.info("Committing data to data store: #{@file_path}")
@file_path.open("w") do |f|
f.write(to_json)
f.fsync
end
end
end
protected
# Removes the "nil" and "empty?" values from the hash (children
# included) so that the final output JSON is cleaner.
def clean_nil_and_empties(hash=self)
# Clean depth first
hash.each do |k, v|
clean_nil_and_empties(v) if v.is_a?(Hash)
end
# Clean ourselves, knowing that any children have already been
# cleaned up
bad_keys = hash.inject([]) do |acc, data|
k,v = data
acc.push(k) if v.nil?
acc.push(k) if v.respond_to?(:empty?) && v.empty?
acc
end
bad_keys.each do |key|
hash.delete(key)
end
end
end
end

View File

@ -12,8 +12,8 @@ module Vagrant
# defined as basically a folder with a "Vagrantfile." This class allows
# access to the VMs, CLI, etc. all in the scope of this environment.
class Environment
HOME_SUBDIRS = ["tmp", "boxes", "gems"]
DEFAULT_HOME = "~/.vagrant.d"
DEFAULT_LOCAL_DATA = ".vagrant"
DEFAULT_RC = "~/.vagrantrc"
# This is the global config, comprised of loading configuration from
@ -35,6 +35,10 @@ module Vagrant
# global state.
attr_reader :home_path
# The directory to the directory where local, environment-specific
# data is stored.
attr_reader :local_data_path
# The directory where temporary files for Vagrant go.
attr_reader :tmp_path
@ -59,10 +63,11 @@ module Vagrant
def initialize(opts=nil)
opts = {
:cwd => nil,
:vagrantfile_name => nil,
:home_path => nil,
:local_data_path => nil,
:lock_path => nil,
:ui_class => nil,
:home_path => nil
:vagrantfile_name => nil
}.merge(opts || {})
# Set the default working directory to look for the vagrantfile
@ -71,6 +76,9 @@ module Vagrant
opts[:cwd] = Pathname.new(opts[:cwd])
raise Errors::EnvironmentNonExistentCWD if !opts[:cwd].directory?
# Set the default ui class
opts[:ui_class] ||= UI::Silent
# Set the Vagrantfile name up. We append "Vagrantfile" and "vagrantfile" so that
# those continue to work as well, but anything custom will take precedence.
opts[:vagrantfile_name] ||= []
@ -78,15 +86,12 @@ module Vagrant
opts[:vagrantfile_name] += ["Vagrantfile", "vagrantfile"]
# Set instance variables for all the configuration parameters.
@cwd = opts[:cwd]
@cwd = opts[:cwd]
@home_path = opts[:home_path]
@lock_path = opts[:lock_path]
@vagrantfile_name = opts[:vagrantfile_name]
@lock_path = opts[:lock_path]
@home_path = opts[:home_path]
@ui = opts[:ui_class].new("vagrant")
ui_class = opts[:ui_class] || UI::Silent
@ui = ui_class.new("vagrant")
@loaded = false
@lock_acquired = false
@logger = Log4r::Logger.new("vagrant::environment")
@ -95,10 +100,22 @@ module Vagrant
# Setup the home directory
setup_home_path
@tmp_path = @home_path.join("tmp")
@tmp_path = @home_path.join("tmp")
@boxes_path = @home_path.join("boxes")
@gems_path = @home_path.join("gems")
# Setup the local data directory. If a configuration path is given,
# then it is expanded relative to the working directory. Otherwise,
# we use the default which is expanded relative to the root path.
@local_data_path = nil
if opts[:local_data_path]
@local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd))
elsif !root_path.nil?
@local_data_path = root_path.join(DEFAULT_LOCAL_DATA)
end
setup_local_data_path
# Setup the default private key
@default_private_key_path = @home_path.join("insecure_private_key")
copy_insecure_private_key
@ -132,15 +149,6 @@ module Vagrant
:virtualbox
end
# The path to the `dotfile`, which contains the persisted UUID of
# the VM if it exists.
#
# @return [Pathname]
def dotfile_path
return nil if !root_path
root_path.join(config_global.vagrant.dotfile_name)
end
# Returns the collection of boxes for the environment.
#
# @return [BoxCollection]
@ -215,10 +223,15 @@ module Vagrant
# Get the provider configuration from the final loaded configuration
provider_config = config.vm.providers[provider].config
# Determine the machine data directory and pass it to the machine.
# XXX: Permissions error here.
machine_data_path = @local_data_path.join("machines/#{name}/#{provider}")
FileUtils.mkdir_p(machine_data_path)
# Create the machine and cache it for future calls. This will also
# return the machine from this method.
@machines[cache_key] = Machine.new(name, provider_cls, provider_config,
config, box, self)
config, machine_data_path, box, self)
end
# This returns a list of the configured machines for this environment.
@ -298,27 +311,6 @@ module Vagrant
end
end
# Loads on initial access and reads data from the global data store.
# The global data store is global to Vagrant everywhere (in every environment),
# so it can be used to store system-wide information. Note that "system-wide"
# typically means "for this user" since the location of the global data
# store is in the home directory.
#
# @return [DataStore]
def global_data
@global_data ||= DataStore.new(File.expand_path("global_data.json", home_path))
end
# Loads (on initial access) and reads data from the local data
# store. This file is always at the root path as the file "~/.vagrant"
# and contains a JSON dump of a hash. See {DataStore} for more
# information.
#
# @return [DataStore]
def local_data
@local_data ||= DataStore.new(dotfile_path)
end
# The root path is the path where the top-most (loaded last)
# Vagrantfile resides. It can be considered the project root for
# this environment.
@ -415,9 +407,11 @@ module Vagrant
DEFAULT_HOME))
@logger.info("Home path: #{@home_path}")
# Setup the array of necessary home directories
dirs = [@home_path]
dirs += HOME_SUBDIRS.collect { |subdir| @home_path.join(subdir) }
# Setup the list of child directories that need to be created if they
# don't already exist.
dirs = [@home_path]
subdirs = ["tmp", "boxes", "gems"]
dirs += subdirs.collect { |subdir| @home_path.join(subdir) }
# Go through each required directory, creating it if it doesn't exist
dirs.each do |dir|
@ -432,6 +426,32 @@ module Vagrant
end
end
# This creates the local data directory and show an error if it
# couldn't properly be created.
def setup_local_data_path
if @local_data_path.nil?
@logger.warn("No local data path is set. Local data cannot be stored.")
return
end
@logger.info("Local data path: #{@local_data_path}")
# If the local data path is a file, then we are probably seeing an
# old (V1) "dotfile." In this case, we upgrade it. The upgrade process
# will remove the old data file if it is successful.
if @local_data_path.file?
upgrade_v1_dotfile(@local_data_path)
end
begin
@logger.debug("Creating: #{@local_data_path}")
FileUtils.mkdir_p(@local_data_path)
rescue Errno::EACCES
raise Errors::LocalDataDirectoryNotAccessible,
:local_data_path => @local_data_path.to_s
end
end
protected
# This method copies the private key into the home directory if it
@ -488,5 +508,16 @@ module Vagrant
@logger.debug("RC file not found. Not loading: #{rc_path}")
end
end
# This upgrades a Vagrant 1.0.x "dotfile" to the new V2 format.
#
# This is a destructive process. Once the upgrade is complete, the
# old dotfile is removed, and the environment becomes incompatible for
# Vagrant 1.0 environments.
#
# @param [Pathname] path The path to the dotfile
def upgrade_v1_dotfile(path)
raise "V1 environment detected. An auto-upgrade process will be made soon."
end
end
end

View File

@ -230,6 +230,10 @@ module Vagrant
error_key(:port_collision_resume)
end
class LocalDataDirectoryNotAccessible < VagrantError
error_key(:local_data_dir_not_accessible)
end
class MachineGuestNotReady < VagrantError
error_key(:machine_guest_not_ready)
end

View File

@ -15,6 +15,11 @@ module Vagrant
# @return [Object]
attr_reader :config
# Directory where machine-specific data can be stored.
#
# @return [Pathname]
attr_reader :data_dir
# The environment that this machine is a part of.
#
# @return [Environment]
@ -50,19 +55,23 @@ module Vagrant
# @param [Object] provider_config The provider-specific configuration for
# this machine.
# @param [Object] config The configuration for this machine.
# @param [Pathname] data_dir The directory where machine-specific data
# can be stored. This directory is ensured to exist.
# @param [Box] box The box that is backing this virtual machine.
# @param [Environment] env The environment that this machine is a
# part of.
def initialize(name, provider_cls, provider_config, config, box, env, base=false)
def initialize(name, provider_cls, provider_config, config, data_dir, box, env, base=false)
@logger = Log4r::Logger.new("vagrant::machine")
@logger.info("Initializing machine: #{name}")
@logger.info(" - Provider: #{provider_cls}")
@logger.info(" - Box: #{box}")
@logger.info(" - Data dir: #{data_dir}")
@box = box
@config = config
@env = env
@name = name
@box = box
@config = config
@data_dir = data_dir
@env = env
@name = name
@provider_config = provider_config
# Read the ID, which is usually in local storage
@ -72,7 +81,10 @@ module Vagrant
if base
@id = name
else
@id = @env.local_data[:active][@name.to_s] if @env.local_data[:active]
# Read the id file from the data directory if it exists as the
# ID for the pre-existing physical representation of this machine.
id_file = @data_dir.join("id")
@id = id_file.read if id_file.file?
end
# Initializes the provider last so that it has access to all the
@ -176,20 +188,20 @@ module Vagrant
#
# @param [String] value The ID.
def id=(value)
@env.local_data[:active] ||= {}
# The file that will store the id if we have one. This allows the
# ID to persist across Vagrant runs.
id_file = @data_dir.join("id")
if value
# Set the value
@env.local_data[:active][@name] = value
# Write the "id" file with the id given.
id_file.open("w+") do |f|
f.write(value)
end
else
# Delete it from the active hash
@env.local_data[:active].delete(@name)
# Delete the file, since the machine is now destroyed
id_file.delete
end
# Commit the local data so that the next time Vagrant is initialized,
# it realizes the VM exists (or doesn't).
@env.local_data.commit
# Store the ID locally
@id = value

View File

@ -12,8 +12,9 @@ module VagrantPlugins
end
def upgrade(new)
new.vagrant.dotfile_name = @dotfile_name if @dotfile_name != UNSET_VALUE
new.vagrant.host = @host if @host != UNSET_VALUE
# TODO: Warn that "dotfile_name" is gone in V2
end
end
end

View File

@ -3,7 +3,6 @@ require "vagrant"
module VagrantPlugins
module Kernel_V2
class VagrantConfig < Vagrant.plugin("2", :config)
attr_accessor :dotfile_name
attr_accessor :host
end
end

View File

@ -85,6 +85,13 @@ en:
You specified: %{home_path}
interrupted: |-
Vagrant exited after cleanup due to external interrupt.
local_data_dir_not_accessible: |-
The directory Vagrant will use to store local environment-specific
state is not accessible. The directory specified as the local data
directory must be both readable and writable for the user that is
running Vagrant.
Local data directory: %{local_data_path}
machine_guest_not_ready: |-
Guest-specific operations were attempted on a machine that is not
ready for guest communication. This should not happen and a bug

View File

@ -1,79 +0,0 @@
require File.expand_path("../../base", __FILE__)
require 'pathname'
describe Vagrant::DataStore do
include_context "unit"
let(:db_file) do
# We create a tempfile and force an explicit close/unlink
# but save the path so that we can re-use it multiple times
temp = Tempfile.new("vagrant")
result = Pathname.new(temp.path)
temp.close
temp.unlink
result
end
let(:instance) { described_class.new(db_file) }
it "initializes a new DB file" do
instance[:data] = true
instance.commit
instance[:data].should == true
test = described_class.new(db_file)
test[:data].should == true
end
it "initializes empty if the file contains invalid data" do
db_file.open("w+") { |f| f.write("NOPE!") }
described_class.new(db_file).should be_empty
end
it "initializes empty if the file doesn't exist" do
described_class.new("NOPENOPENOPENOPENPEPEPEPE").should be_empty
end
it "raises an error if the path given is a directory" do
db_file.delete if db_file.exist?
db_file.mkdir
expect { described_class.new(db_file) }.
to raise_error(Vagrant::Errors::DotfileIsDirectory)
end
it "cleans nil and empties when committing" do
instance[:data] = { :bar => nil }
instance[:another] = {}
instance.commit
# The instance is now empty because the data was nil
instance.should be_empty
end
it "deletes the data file if the store is empty when saving" do
instance[:data] = true
instance.commit
another = described_class.new(db_file)
another[:data] = nil
another.commit
# The file should no longer exist
db_file.should_not be_exist
end
it "works if the DB file is nil" do
store = described_class.new(nil)
store[:foo] = "bar"
store[:foo].should == "bar"
end
it "throws an exception if attempting to commit a data store with no file" do
store = described_class.new(nil)
expect { store.commit }.
to raise_error(StandardError)
end
end

View File

@ -23,19 +23,22 @@ describe Vagrant::Environment do
describe "current working directory" do
it "is the cwd by default" do
with_temp_env("VAGRANT_CWD" => nil) do
described_class.new.cwd.should == Pathname.new(Dir.pwd)
temp_dir = Tempdir.new.path
Dir.chdir(temp_dir) do
with_temp_env("VAGRANT_CWD" => nil) do
described_class.new.cwd.should == Pathname.new(Dir.pwd)
end
end
end
it "is set to the cwd given" do
directory = File.dirname(__FILE__)
directory = Tempdir.new.path
instance = described_class.new(:cwd => directory)
instance.cwd.should == Pathname.new(directory)
end
it "is set to the environmental variable VAGRANT_CWD" do
directory = File.dirname(__FILE__)
directory = Tempdir.new.path
instance = with_temp_env("VAGRANT_CWD" => directory) do
described_class.new
end
@ -72,6 +75,24 @@ describe Vagrant::Environment do
end
end
describe "local data path" do
it "is set to the proper default" do
default = instance.root_path.join(described_class::DEFAULT_LOCAL_DATA)
instance.local_data_path.should == default
end
it "is expanded relative to the cwd" do
instance = described_class.new(:local_data_path => "foo")
instance.local_data_path.should == instance.cwd.join("foo")
end
it "is set to the given value" do
dir = Tempdir.new.path
instance = described_class.new(:local_data_path => dir)
instance.local_data_path.to_s.should == dir
end
end
describe "default provider" do
it "should return virtualbox" do
instance.default_provider.should == :virtualbox
@ -137,26 +158,26 @@ VF
environment = isolated_environment do |env|
env.vagrantfile(<<-VF)
Vagrant.configure("2") do |config|
config.vagrant.dotfile_name = "foo"
config.ssh.port = 200
end
VF
end
env = environment.create_vagrant_env
env.config_global.vagrant.dotfile_name.should == "foo"
env.config_global.ssh.port.should == 200
end
it "should load from a custom Vagrantfile" do
environment = isolated_environment do |env|
env.file("non_standard_name", <<-VF)
Vagrant.configure("2") do |config|
config.vagrant.dotfile_name = "custom"
config.ssh.port = 200
end
VF
end
env = environment.create_vagrant_env(:vagrantfile_name => "non_standard_name")
env.config_global.vagrant.dotfile_name.should == "custom"
env.config_global.ssh.port.should == 200
end
end
@ -302,7 +323,6 @@ VF
env.vagrantfile(<<-VF)
Vagrant.configure("2") do |config|
config.ssh.port = 1
config.vagrant.dotfile_name = "foo"
config.vm.box = "base"
config.vm.define "vm1" do |inner|
@ -317,7 +337,7 @@ VF
env = environment.create_vagrant_env
machine = env.machine(:vm1, :foo)
machine.config.ssh.port.should == 100
machine.config.vagrant.dotfile_name.should == "foo"
machine.config.vm.box.should == "base"
end
it "should load the box configuration for a V2 box" do

View File

@ -1,3 +1,5 @@
require "pathname"
require File.expand_path("../../base", __FILE__)
describe Vagrant::Machine do
@ -13,6 +15,7 @@ describe Vagrant::Machine do
let(:provider_config) { Object.new }
let(:box) { Object.new }
let(:config) { env.config_global }
let(:data_dir) { Pathname.new(Tempdir.new.path) }
let(:env) do
# We need to create a Vagrantfile so that this test environment
# has a proper root path
@ -28,7 +31,8 @@ describe Vagrant::Machine do
# Returns a new instance with the test data
def new_instance
described_class.new(name, provider_cls, provider_config, config, box, env)
described_class.new(name, provider_cls, provider_config,
config, data_dir, box, env)
end
describe "initialization" do
@ -57,7 +61,8 @@ describe Vagrant::Machine do
# Initialize a new machine and verify that we properly receive
# the machine we expect.
instance = described_class.new(name, provider_cls, provider_config, config, box, env)
instance = described_class.new(name, provider_cls, provider_config,
config, data_dir, box, env)
received_machine.should eql(instance)
end