core: Basic MachineIndex class, starting point
This commit is contained in:
parent
7ccf7fadf2
commit
016afc7922
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue