class Vagrant::MachineIndex

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"
    }
  }
}

Public Class Methods

new(data_dir) click to toggle source

Initializes a MachineIndex at the given file location.

@param [Pathname] data_dir Path to the directory where data for the

index can be stored. This folder should exist and must be writable.
# File lib/vagrant/machine_index.rb, line 47
def initialize(data_dir)
  @data_dir   = data_dir
  @index_file = data_dir.join("index")
  @lock       = Monitor.new
  @machines  = {}
  @machine_locks = {}

  with_index_lock do
    unlocked_reload
  end
end

Public Instance Methods

delete(entry) click to toggle source

Deletes a machine by UUID.

The machine being deleted with this UUID must either be locked by this index or must be unlocked.

@param [Entry] entry The entry to delete. @return [Boolean] true if delete is successful

# File lib/vagrant/machine_index.rb, line 66
def delete(entry)
  return true if !entry.id

  @lock.synchronize do
    with_index_lock do
      return true if !@machines[entry.id]

      # If we don't have the lock, then we need to acquire it.
      if !@machine_locks[entry.id]
        raise "Unlocked delete on machine: #{entry.id}"
      end

      # Reload so we have the latest data, then delete and save
      unlocked_reload
      @machines.delete(entry.id)
      unlocked_save

      # Release access on this machine
      unlocked_release(entry.id)
    end
  end

  true
end
each(reload=false) { |entry(uuid, merge("id" => uuid))| ... } click to toggle source

Iterate over every machine in the index. The yielded {Entry} objects will NOT be locked, so you’ll have to call {#get} manually to acquire the lock on them.

# File lib/vagrant/machine_index.rb, line 94
def each(reload=false)
  if reload
    @lock.synchronize do
      with_index_lock do
        unlocked_reload
      end
    end
  end

  @machines.each do |uuid, data|
    yield Entry.new(uuid, data.merge("id" => uuid))
  end
end
get(uuid) click to toggle source

Accesses a machine by UUID and returns a {MachineIndex::Entry}

The entry returned is locked and can’t be read again or updated by this process or any other. To unlock the machine, call {#release} with the entry.

You can only {#set} an entry (update) when the lock is held.

@param [String] uuid UUID for the machine to access. @return [MachineIndex::Entry]

# File lib/vagrant/machine_index.rb, line 118
def get(uuid)
  entry = nil

  @lock.synchronize do
    with_index_lock do
      # Reload the data
      unlocked_reload

      data = find_by_prefix(uuid)
      return nil if !data
      uuid = data["id"]

      entry = Entry.new(uuid, data)

      # Lock this machine
      lock_file = lock_machine(uuid)
      if !lock_file
        raise Errors::MachineLocked,
          name: entry.name,
          provider: entry.provider
      end

      @machine_locks[uuid] = lock_file
    end
  end

  entry
end
include?(uuid) click to toggle source

Tests if the index has the given UUID.

@param [String] uuid @return [Boolean]

# File lib/vagrant/machine_index.rb, line 151
def include?(uuid)
  @lock.synchronize do
    with_index_lock do
      unlocked_reload
      return !!find_by_prefix(uuid)
    end
  end
end
recover(entry) click to toggle source

Reinsert a machine into the global index if it has a valid existing uuid but does not currently exist in the index.

@param [Entry] entry @return [Entry]

# File lib/vagrant/machine_index.rb, line 242
def recover(entry)
  @lock.synchronize do
    with_index_lock do
      # Reload the data
      unlocked_reload
      # Don't recover if entry already exists in the global
      return entry if find_by_prefix(entry.id)

      lock_file = lock_machine(entry.id)
      if !lock_file
        raise Errors::MachineLocked,
          name: entry.name,
          provider: entry.provider
      end
      @machine_locks[entry.id] = lock_file
    end
  end
  return set(entry)
end
release(entry) click to toggle source

Releases an entry, unlocking it.

This is an idempotent operation. It is safe to call this even if you’re unsure if an entry is locked or not.

After calling this, the previous entry should no longer be used.

@param [Entry] entry

# File lib/vagrant/machine_index.rb, line 168
def release(entry)
  @lock.synchronize do
    unlocked_release(entry.id)
  end
end
set(entry) click to toggle source

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. Additionally, the a lock will be created for the resulting entry, so you must {#release} it if you want others to be able to access it.

If the entry isn’t new (has a UUID). then this process must hold that entry’s lock or else this set will fail.

@param [Entry] entry @return [Entry]

# File lib/vagrant/machine_index.rb, line 186
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

  @lock.synchronize do
    with_index_lock do
      # Reload so we have the latest machine data. This allows other
      # processes to update their own machines without conflicting
      # with our own.
      unlocked_reload

      # If we don't have a machine ID, try to look one up
      if !id
        self.each do |other|
          if entry.name == other.name &&
            entry.provider == other.provider &&
            entry.vagrantfile_path.to_s == other.vagrantfile_path.to_s
            id = other.id
            break
          end
        end

        # If we still don't have an ID, generate a random one
        id = SecureRandom.uuid.gsub("-", "") if !id

        # Get a lock on this machine
        lock_file = lock_machine(id)
        if !lock_file
          raise "Failed to lock new machine: #{entry.name}"
        end

        @machine_locks[id] = lock_file
      end

      if !@machine_locks[id]
        raise "Unlocked write on machine: #{id}"
      end

      # Set our machine and save
      @machines[id] = struct
      unlocked_save
    end
  end

  Entry.new(id, struct)
end

Protected Instance Methods

find_by_prefix(prefix) click to toggle source

Finds a machine where the UUID is prefixed by the given string.

@return [Hash]

# File lib/vagrant/machine_index.rb, line 267
def find_by_prefix(prefix)
  return if !prefix || prefix == ""
  @machines.each do |uuid, data|
    return data.merge("id" => uuid) if uuid.start_with?(prefix)
  end

  nil
end
lock_machine(uuid) click to toggle source

Locks a machine exclusively to us, returning the file handle that holds the lock.

If the lock cannot be acquired, then nil is returned.

This should be called within an index lock.

@return [File]

# File lib/vagrant/machine_index.rb, line 284
def lock_machine(uuid)
  lock_path = @data_dir.join("#{uuid}.lock")
  lock_file = lock_path.open("w+")
  if lock_file.flock(File::LOCK_EX | File::LOCK_NB) === false
    lock_file.close
    lock_file = nil
  end

  lock_file
end
unlocked_release(id) click to toggle source

Releases a local lock on a machine. This does not acquire any locks so make sure to lock around it.

@param [String] id

# File lib/vagrant/machine_index.rb, line 299
def unlocked_release(id)
  lock_file = @machine_locks[id]
  if lock_file
    lock_file.close
    begin
      File.delete(lock_file.path)
    rescue Errno::EACCES
      # Another process is probably opened it, no problem.
    end

    @machine_locks.delete(id)
  end
end
unlocked_reload() click to toggle source

This will reload the data without locking the index. It is assumed the caller with lock the index outside of this call.

@param [File] f

# File lib/vagrant/machine_index.rb, line 317
def unlocked_reload
  return if !@index_file.file?

  data = nil
  begin
    data = JSON.load(@index_file.read)
  rescue JSON::ParserError
    raise Errors::CorruptMachineIndex, path: @index_file.to_s
  end

  if data
    if !data["version"] || data["version"].to_i != 1
      raise Errors::CorruptMachineIndex, path: @index_file.to_s
    end

    @machines = data["machines"] || {}
  end
end
unlocked_save() click to toggle source

Saves the index.

# File lib/vagrant/machine_index.rb, line 337
def unlocked_save
  @index_file.open("w") do |f|
    f.write(JSON.dump({
      "version"  => 1,
      "machines" => @machines,
    }))
  end
end
with_index_lock() { || ... } click to toggle source

This will hold a lock to the index so it can be read or updated.

# File lib/vagrant/machine_index.rb, line 348
def with_index_lock
  lock_path = "#{@index_file}.lock"
  File.open(lock_path, "w+") do |f|
    f.flock(File::LOCK_EX)
    yield
  end
end