core: Basic MachineIndex class, starting point

This commit is contained in:
Mitchell Hashimoto 2014-03-02 22:08:20 +01:00
parent 7ccf7fadf2
commit 016afc7922
4 changed files with 295 additions and 0 deletions

View File

@ -316,6 +316,10 @@ module Vagrant
error_key(:copy_private_key_failed)
end
class CorruptMachineIndex < VagrantError
error_key(:corrupt_machine_index)
end
class DarwinNFSMountFailed < VagrantError
error_key(:darwin_nfs_mount_failed)
end

View File

@ -0,0 +1,169 @@
require "json"
require "pathname"
require "securerandom"
module Vagrant
# MachineIndex is able to manage the index of created Vagrant environments
# in a central location.
#
# The MachineIndex stores a mapping of UUIDs to basic information about
# a machine. The UUIDs are stored with the Vagrant environment and are
# looked up in the machine index.
#
# The MachineIndex stores information such as the name of a machine,
# the directory it was last seen at, its last known state, etc. Using
# this information, we can load the entire {Machine} object for a machine,
# or we can just display metadata if needed.
#
# The internal format of the data file is currently JSON in the following
# structure:
#
# {
# "version": 1,
# "machines": {
# "uuid": {
# "name": "foo",
# "provider": "vmware_fusion",
# "data_path": "/path/to/data/dir",
# "vagrantfile_path": "/path/to/Vagrantfile",
# "state": "running",
# "updated_at": "2014-03-02 11:11:44 +0100"
# }
# }
# }
#
class MachineIndex
# Initializes a MachineIndex at the given file location.
#
# @param [Pathname] data_file Path to the file that should be used
# to maintain the machine index. This file doesn't have to exist
# but this location must be writable.
def initialize(data_file)
@data_file = data_file
@machines = {}
if @data_file.file?
data = nil
begin
data = JSON.load(@data_file.read)
rescue JSON::ParserError
raise Errors::CorruptMachineIndex, path: data_file.to_s
end
if data
if !data["version"] || data["version"].to_i != 1
raise Errors::CorruptMachineIndex, path: data_file.to_s
end
@machines = data["machines"] || {}
end
end
end
# Accesses a machine by UUID and returns a {MachineIndex::Entry}
#
# @param [String] uuid UUID for the machine to access.
# @return [MachineIndex::Entry]
def [](uuid)
return nil if !@machines[uuid]
Entry.new(uuid, @machines[uuid].merge("id" => uuid))
end
# Creates/updates an entry object and returns the resulting entry.
#
# If the entry was new (no UUID), then the UUID will be set on the
# resulting entry and can be used.
#
# @param [Entry] entry
# @return [Entry]
def set(entry)
# Get the struct and update the updated_at attribute
struct = entry.to_json_struct
# Set an ID if there isn't one already set
id = entry.id
id ||= SecureRandom.uuid
# Store the data
@machines[id] = struct
save
Entry.new(id, struct)
end
# Saves the index.
#
# This doesn't usually need to be called because {#set} will
# automatically save as well.
def save
@data_file.open("w") do |f|
f.write(JSON.dump({
"version" => 1,
"machines" => @machines,
}))
end
end
# An entry in the MachineIndex.
class Entry
# The unique ID for this entry. This is _not_ the ID for the
# machine itself (which is provider-specific and in the data directory).
#
# @return [String]
attr_reader :id
# The name of the machine.
#
# @return [String]
attr_accessor :name
# The name of the provider.
#
# @return [String]
attr_accessor :provider
# The last known state of this machine.
#
# @return [String]
attr_accessor :state
# The path to the Vagrantfile that manages this machine.
#
# @return [Pathname]
attr_accessor :vagrantfile_path
# The last time this entry was updated.
#
# @return [DateTime]
attr_reader :updated_at
# Initializes an entry.
#
# The parameter given should be nil if this is being created
# publicly.
def initialize(id=nil, raw=nil)
# Do nothing if we aren't given a raw value. Otherwise, parse it.
return if !raw
@id = id
@name = raw["name"]
@provider = raw["provider"]
@state = raw["state"]
@vagrantfile_path = Pathname.new(raw["vagrantfile_path"])
# TODO(mitchellh): parse into a proper datetime
@updated_at = raw["updated_at"]
end
# Converts to the structure used by the JSON
def to_json_struct
{
"name" => @name,
"provider" => @provider,
"state" => @state,
"vagrantfile_path" => @vagrantfile_path,
"updated_at" => @updated_at,
}
end
end
end
end

View File

@ -598,6 +598,18 @@ en:
Source: %{source}
Destination: %{destination}
corrupt_machine_index: |-
The machine index which stores all required information about
running Vagrant environments has become corrupt. This is usually
caused by external tampering of the Vagrant data folder.
Vagrant cannot manage any Vagrant environments if the index is
corrupt. Please attempt to manually correct it. If you are unable
to manually correct it, then remove the data file at the path below.
This will leave all existing Vagrant environments "orphaned" and
they'll have to be destroyed manually.
Path: %{path}
destroy_requires_force: |-
Destroy doesn't have a TTY to ask for confirmation. Please pass the
`--force` flag to force a destroy, otherwise attach a TTY so that

View File

@ -0,0 +1,110 @@
require "json"
require "pathname"
require "tempfile"
require File.expand_path("../../base", __FILE__)
require "vagrant/machine_index"
describe Vagrant::MachineIndex do
include_context "unit"
let(:data_file) { temporary_file }
subject { described_class.new(data_file) }
it "raises an exception if the data file is corrupt" do
data_file.open("w") do |f|
f.write(JSON.dump({}))
end
expect { subject }.
to raise_error(Vagrant::Errors::CorruptMachineIndex)
end
it "raises an exception if the JSON is invalid" do
data_file.open("w") do |f|
f.write("foo")
end
expect { subject }.
to raise_error(Vagrant::Errors::CorruptMachineIndex)
end
describe "#[]" do
before do
data = {
"version" => 1,
"machines" => {
"bar" => {
"name" => "default",
"provider" => "vmware",
"vagrantfile_path" => "/foo/bar/baz",
"state" => "running",
"updated_at" => "foo",
}
}
}
data_file.open("w") do |f|
f.write(JSON.dump(data))
end
end
it "returns nil if the machine doesn't exist" do
expect(subject["foo"]).to be_nil
end
it "returns a valid entry if the machine exists" do
result = subject["bar"]
expect(result.id).to eq("bar")
expect(result.name).to eq("default")
expect(result.provider).to eq("vmware")
expect(result.vagrantfile_path).to eq(Pathname.new("/foo/bar/baz"))
expect(result.state).to eq("running")
expect(result.updated_at).to eq("foo")
end
end
describe "#set and #[]" do
let(:entry_klass) { Vagrant::MachineIndex::Entry }
it "adds a new entry" do
entry = entry_klass.new
entry.name = "foo"
entry.vagrantfile_path = "/bar"
result = subject.set(entry)
expect(result.id).to_not be_empty
# Get it froma new class and check the results
subject = described_class.new(data_file)
entry = subject[result.id]
expect(entry).to_not be_nil
expect(entry.name).to eq("foo")
# TODO: test that updated_at is set
end
it "updates an existing entry" do
entry = entry_klass.new
entry.name = "foo"
entry.vagrantfile_path = "/bar"
result = subject.set(entry)
expect(result.id).to_not be_empty
result.name = "bar"
nextresult = subject.set(result)
expect(nextresult.id).to eq(result.id)
# Get it froma new class and check the results
subject = described_class.new(data_file)
entry = subject[result.id]
expect(entry).to_not be_nil
expect(entry.name).to eq("bar")
end
end
end