454 lines
15 KiB
Ruby
454 lines
15 KiB
Ruby
require "digest/md5"
|
|
require "thread"
|
|
|
|
require "log4r"
|
|
|
|
module Vagrant
|
|
# This represents a machine that Vagrant manages. This provides a singular
|
|
# API for querying the state and making state changes to the machine, which
|
|
# is backed by any sort of provider (VirtualBox, VMWare, etc.).
|
|
class Machine
|
|
# The box that is backing this machine.
|
|
#
|
|
# @return [Box]
|
|
attr_accessor :box
|
|
|
|
# Configuration for the machine.
|
|
#
|
|
# @return [Object]
|
|
attr_accessor :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]
|
|
attr_reader :env
|
|
|
|
# ID of the machine. This ID comes from the provider and is not
|
|
# guaranteed to be of any particular format except that it is
|
|
# a string.
|
|
#
|
|
# @return [String]
|
|
attr_reader :id
|
|
|
|
# Name of the machine. This is assigned by the Vagrantfile.
|
|
#
|
|
# @return [Symbol]
|
|
attr_reader :name
|
|
|
|
# The provider backing this machine.
|
|
#
|
|
# @return [Object]
|
|
attr_reader :provider
|
|
|
|
# The provider-specific configuration for this machine.
|
|
#
|
|
# @return [Object]
|
|
attr_accessor :provider_config
|
|
|
|
# The name of the provider.
|
|
#
|
|
# @return [Symbol]
|
|
attr_reader :provider_name
|
|
|
|
# The options given to the provider when registering the plugin.
|
|
#
|
|
# @return [Hash]
|
|
attr_reader :provider_options
|
|
|
|
# The UI for outputting in the scope of this machine.
|
|
#
|
|
# @return [UI]
|
|
attr_reader :ui
|
|
|
|
# The Vagrantfile that this machine is attached to.
|
|
#
|
|
# @return [Vagrantfile]
|
|
attr_reader :vagrantfile
|
|
|
|
# Initialize a new machine.
|
|
#
|
|
# @param [String] name Name of the virtual machine.
|
|
# @param [Class] provider The provider backing this machine. This is
|
|
# currently expected to be a V1 `provider` plugin.
|
|
# @param [Object] provider_config The provider-specific configuration for
|
|
# this machine.
|
|
# @param [Hash] provider_options The provider-specific options from the
|
|
# plugin definition.
|
|
# @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_name, provider_cls, provider_config, provider_options, config, data_dir, box, env, vagrantfile, 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
|
|
@data_dir = data_dir
|
|
@env = env
|
|
@vagrantfile = vagrantfile
|
|
@guest = Guest.new(
|
|
self,
|
|
Vagrant.plugin("2").manager.guests,
|
|
Vagrant.plugin("2").manager.guest_capabilities)
|
|
@name = name
|
|
@provider_config = provider_config
|
|
@provider_name = provider_name
|
|
@provider_options = provider_options
|
|
@ui = Vagrant::UI::Prefixed.new(@env.ui, @name)
|
|
@ui_mutex = Mutex.new
|
|
|
|
# Read the ID, which is usually in local storage
|
|
@id = nil
|
|
|
|
# XXX: This is temporary. This will be removed very soon.
|
|
if base
|
|
@id = name
|
|
else
|
|
# 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.chomp if id_file.file?
|
|
end
|
|
|
|
# Keep track of where our UUID should be placed
|
|
@index_uuid_file = nil
|
|
@index_uuid_file = @data_dir.join("index_uuid") if @data_dir
|
|
|
|
# Initializes the provider last so that it has access to all the
|
|
# state we setup on this machine.
|
|
@provider = provider_cls.new(self)
|
|
@provider._initialize(@provider_name, self)
|
|
|
|
# If we're using WinRM, we eager load the plugin because of
|
|
# GH-3390
|
|
if @config.vm.communicator == :winrm
|
|
@logger.debug("Eager loading WinRM communicator to avoid GH-3390")
|
|
communicate
|
|
end
|
|
end
|
|
|
|
# This calls an action on the provider. The provider may or may not
|
|
# actually implement the action.
|
|
#
|
|
# @param [Symbol] name Name of the action to run.
|
|
# @param [Hash] extra_env This data will be passed into the action runner
|
|
# as extra data set on the environment hash for the middleware
|
|
# runner.
|
|
def action(name, extra_env=nil)
|
|
@logger.info("Calling action: #{name} on provider #{@provider}")
|
|
|
|
# Create a deterministic ID for this machine
|
|
id = Digest::MD5.hexdigest("#{@env.root_path}#{@name}")
|
|
|
|
# Lock this machine for the duration of this action
|
|
@env.lock("machine-action-#{id}") do
|
|
# Get the callable from the provider.
|
|
callable = @provider.action(name)
|
|
|
|
# If this action doesn't exist on the provider, then an exception
|
|
# must be raised.
|
|
if callable.nil?
|
|
raise Errors::UnimplementedProviderAction,
|
|
:action => name,
|
|
:provider => @provider.to_s
|
|
end
|
|
|
|
action_raw(name, callable, extra_env)
|
|
end
|
|
rescue Errors::EnvironmentLockedError
|
|
raise Errors::MachineActionLockedError,
|
|
action: name,
|
|
name: @name
|
|
end
|
|
|
|
# This calls a raw callable in the proper context of the machine using
|
|
# the middleware stack.
|
|
#
|
|
# @param [Symbol] name Name of the action
|
|
# @param [Proc] callable
|
|
# @param [Hash] extra_env Extra env for the action env.
|
|
# @return [Hash] The resulting env
|
|
def action_raw(name, callable, extra_env=nil)
|
|
# Run the action with the action runner on the environment
|
|
env = {
|
|
action_name: "machine_action_#{name}".to_sym,
|
|
machine: self,
|
|
machine_action: name,
|
|
ui: @ui,
|
|
}.merge(extra_env || {})
|
|
@env.action_runner.run(callable, env)
|
|
end
|
|
|
|
# Returns a communication object for executing commands on the remote
|
|
# machine. Note that the _exact_ semantics of this are up to the
|
|
# communication provider itself. Despite this, the semantics are expected
|
|
# to be consistent across operating systems. For example, all linux-based
|
|
# systems should have similar communication (usually a shell). All
|
|
# Windows systems should have similar communication as well. Therefore,
|
|
# prior to communicating with the machine, users of this method are
|
|
# expected to check the guest OS to determine their behavior.
|
|
#
|
|
# This method will _always_ return some valid communication object.
|
|
# The `ready?` API can be used on the object to check if communication
|
|
# is actually ready.
|
|
#
|
|
# @return [Object]
|
|
def communicate
|
|
if !@communicator
|
|
requested = @config.vm.communicator
|
|
requested ||= :ssh
|
|
klass = Vagrant.plugin("2").manager.communicators[requested]
|
|
raise Errors::CommunicatorNotFound, comm: requested.to_s if !klass
|
|
@communicator = klass.new(self)
|
|
end
|
|
|
|
@communicator
|
|
end
|
|
|
|
# Returns a guest implementation for this machine. The guest implementation
|
|
# knows how to do guest-OS specific tasks, such as configuring networks,
|
|
# mounting folders, etc.
|
|
#
|
|
# @return [Guest]
|
|
def guest
|
|
raise Errors::MachineGuestNotReady if !communicate.ready?
|
|
@guest.detect! if !@guest.ready?
|
|
@guest
|
|
end
|
|
|
|
# This sets the unique ID associated with this machine. This will
|
|
# persist this ID so that in the future Vagrant will be able to find
|
|
# this machine again. The unique ID must be absolutely unique to the
|
|
# virtual machine, and can be used by providers for finding the
|
|
# actual machine associated with this instance.
|
|
#
|
|
# **WARNING:** Only providers should ever use this method.
|
|
#
|
|
# @param [String] value The ID.
|
|
def id=(value)
|
|
@logger.info("New machine ID: #{value.inspect}")
|
|
|
|
# The file that will store the id if we have one. This allows the
|
|
# ID to persist across Vagrant runs. Also, store the UUID for the
|
|
# machine index.
|
|
id_file = @data_dir.join("id")
|
|
|
|
if value
|
|
# Write the "id" file with the id given.
|
|
id_file.open("w+") do |f|
|
|
f.write(value)
|
|
end
|
|
|
|
# If we don't have a UUID, then create one
|
|
if index_uuid.nil?
|
|
# Create the index entry and save it
|
|
entry = MachineIndex::Entry.new
|
|
entry.local_data_path = @env.local_data_path
|
|
entry.name = @name.to_s
|
|
entry.provider = @provider_name.to_s
|
|
entry.state = "preparing"
|
|
entry.vagrantfile_path = @env.root_path
|
|
entry.vagrantfile_name = @env.vagrantfile_name
|
|
|
|
if @box
|
|
entry.extra_data["box"] = {
|
|
"name" => @box.name,
|
|
"provider" => @box.provider.to_s,
|
|
"version" => @box.version.to_s,
|
|
}
|
|
end
|
|
|
|
entry = @env.machine_index.set(entry)
|
|
@env.machine_index.release(entry)
|
|
|
|
# Store our UUID so we can access it later
|
|
if @index_uuid_file
|
|
@index_uuid_file.open("w+") do |f|
|
|
f.write(entry.id)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
# Delete the file, since the machine is now destroyed
|
|
id_file.delete if id_file.file?
|
|
|
|
# If we have a UUID associated with the index, remove it
|
|
uuid = index_uuid
|
|
if uuid
|
|
entry = @env.machine_index.get(uuid)
|
|
@env.machine_index.delete(entry) if entry
|
|
end
|
|
|
|
# Delete the entire data directory contents since all state
|
|
# associated with the VM is now gone.
|
|
@data_dir.children.each do |child|
|
|
begin
|
|
child.rmtree
|
|
rescue Errno::EACCES
|
|
@logger.info("EACCESS deleting file: #{child}")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Store the ID locally
|
|
@id = value.nil? ? nil : value.to_s
|
|
|
|
# Notify the provider that the ID changed in case it needs to do
|
|
# any accounting from it.
|
|
@provider.machine_id_changed
|
|
end
|
|
|
|
# Returns the UUID associated with this machine in the machine
|
|
# index. We only have a UUID if an ID has been set.
|
|
#
|
|
# @return [String] UUID or nil if we don't have one yet.
|
|
def index_uuid
|
|
return nil if !@index_uuid_file
|
|
return @index_uuid_file.read.chomp if @index_uuid_file.file?
|
|
return nil
|
|
end
|
|
|
|
# This returns a clean inspect value so that printing the value via
|
|
# a pretty print (`p`) results in a readable value.
|
|
#
|
|
# @return [String]
|
|
def inspect
|
|
"#<#{self.class}: #{@name} (#{@provider.class})>"
|
|
end
|
|
|
|
# This returns the SSH info for accessing this machine. This SSH info
|
|
# is queried from the underlying provider. This method returns `nil` if
|
|
# the machine is not ready for SSH communication.
|
|
#
|
|
# The structure of the resulting hash is guaranteed to contain the
|
|
# following structure, although it may return other keys as well
|
|
# not documented here:
|
|
#
|
|
# {
|
|
# :host => "1.2.3.4",
|
|
# :port => "22",
|
|
# :username => "mitchellh",
|
|
# :private_key_path => "/path/to/my/key"
|
|
# }
|
|
#
|
|
# Note that Vagrant makes no guarantee that this info works or is
|
|
# correct. This is simply the data that the provider gives us or that
|
|
# is configured via a Vagrantfile. It is still possible after this
|
|
# point when attempting to connect via SSH to get authentication
|
|
# errors.
|
|
#
|
|
# @return [Hash] SSH information.
|
|
def ssh_info
|
|
# First, ask the provider for their information. If the provider
|
|
# returns nil, then the machine is simply not ready for SSH, and
|
|
# we return nil as well.
|
|
info = @provider.ssh_info
|
|
return nil if info.nil?
|
|
|
|
# Delete out the nil entries.
|
|
info.dup.each do |key, value|
|
|
info.delete(key) if value.nil?
|
|
end
|
|
|
|
# We set the defaults
|
|
info[:host] ||= @config.ssh.default.host
|
|
info[:port] ||= @config.ssh.default.port
|
|
info[:private_key_path] ||= @config.ssh.default.private_key_path
|
|
info[:username] ||= @config.ssh.default.username
|
|
|
|
# We set overrides if they are set. These take precedence over
|
|
# provider-returned data.
|
|
info[:host] = @config.ssh.host if @config.ssh.host
|
|
info[:port] = @config.ssh.port if @config.ssh.port
|
|
info[:username] = @config.ssh.username if @config.ssh.username
|
|
info[:password] = @config.ssh.password if @config.ssh.password
|
|
|
|
# We also set some fields that are purely controlled by Varant
|
|
info[:forward_agent] = @config.ssh.forward_agent
|
|
info[:forward_x11] = @config.ssh.forward_x11
|
|
|
|
# Add in provided proxy command config
|
|
info[:proxy_command] = @config.ssh.proxy_command if @config.ssh.proxy_command
|
|
|
|
# Set the private key path. If a specific private key is given in
|
|
# the Vagrantfile we set that. Otherwise, we use the default (insecure)
|
|
# private key, but only if the provider didn't give us one.
|
|
if !info[:private_key_path] && !info[:password]
|
|
if @config.ssh.private_key_path
|
|
info[:private_key_path] = @config.ssh.private_key_path
|
|
else
|
|
info[:private_key_path] = @env.default_private_key_path
|
|
end
|
|
end
|
|
|
|
# If we have a private key in our data dir, then use that
|
|
if @data_dir
|
|
data_private_key = @data_dir.join("private_key")
|
|
if data_private_key.file?
|
|
info[:private_key_path] = [data_private_key.to_s]
|
|
end
|
|
end
|
|
|
|
# Setup the keys
|
|
info[:private_key_path] ||= []
|
|
info[:private_key_path] = Array(info[:private_key_path])
|
|
|
|
# Expand the private key path relative to the root path
|
|
info[:private_key_path].map! do |path|
|
|
File.expand_path(path, @env.root_path)
|
|
end
|
|
|
|
# Return the final compiled SSH info data
|
|
info
|
|
end
|
|
|
|
# Returns the state of this machine. The state is queried from the
|
|
# backing provider, so it can be any arbitrary symbol.
|
|
#
|
|
# @return [MachineState]
|
|
def state
|
|
result = @provider.state
|
|
raise Errors::MachineStateInvalid if !result.is_a?(MachineState)
|
|
|
|
# Update our state cache if we have a UUID and an entry in the
|
|
# master index.
|
|
uuid = index_uuid
|
|
if uuid
|
|
entry = @env.machine_index.get(uuid)
|
|
if entry
|
|
entry.state = result.short_description
|
|
@env.machine_index.set(entry)
|
|
@env.machine_index.release(entry)
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
# Temporarily changes the machine UI. This is useful if you want
|
|
# to execute an {#action} with a different UI.
|
|
def with_ui(ui)
|
|
@ui_mutex.synchronize do
|
|
begin
|
|
old_ui = @ui
|
|
@ui = ui
|
|
yield
|
|
ensure
|
|
@ui = old_ui
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|