vaguerent/lib/vagrant/machine.rb
Chris Roberts 51adb12547 Add architecture support for boxes
Introduce support for handling box architecture. Adds a new
`box_architecture` setting that defaults to `:auto` which will perform
automatic detection of the host system, but can be overridden with a
custom value. Can also be set to `nil` which will result in it fetching
the box flagged with the default architecture within the metadata.

Box collection has been modified to allow existing boxes already
downloaded and unpacked to still function as expected when architecture
information is not available.
2023-09-14 16:15:03 -07:00

673 lines
22 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require_relative "./util/ssh"
require_relative "./action/builtin/mixin_synced_folders"
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
autoload :Remote, "vagrant/machine/remote"
extend Vagrant::Action::Builtin::MixinSyncedFolders
# 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 triggers with machine specific configuration applied
#
# @return [Vagrant::Plugin::V2::Trigger]
attr_reader :triggers
# 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
@state_mutex = Mutex.new
@triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self, @ui)
# 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
# For base setups, we don't want to insert the key
@config.ssh.insert_key = false
else
reload
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
# If the ID is the special not created ID, then set our ID to
# nil so that we destroy all our data.
if state.id == MachineState::NOT_CREATED_ID
self.id = nil
end
# Output a bunch of information about this machine in
# machine-readable format in case someone is listening.
@ui.machine("metadata", "provider", provider_name)
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, opts=nil)
@logger.info("Calling action: #{name} on provider #{@provider}")
opts ||= {}
# Determine whether we lock or not
lock = true
lock = opts.delete(:lock) if opts.key?(:lock)
# Extra env keys are the remaining opts
extra_env = opts.dup
check_cwd # Warns the UI if the machine was last used on a different dir
# Create a deterministic ID for this machine
vf = nil
vf = @env.vagrantfile_name[0] if @env.vagrantfile_name
id = Digest::MD5.hexdigest(
"#{@env.root_path}#{vf}#{@env.local_data_path}#{@name}")
# We only lock if we're not executing an SSH action. In the future
# we will want to do more fine-grained unlocking in actions themselves
# but for a 1.6.2 release this will work.
locker = Proc.new { |*args, &block| block.call }
locker = @env.method(:lock) if lock && !name.to_s.start_with?("ssh")
# Lock this machine for the duration of this action
return_env = locker.call("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
# Call the action
ui.machine("action", name.to_s, "start")
action_result = action_raw(name, callable, extra_env)
ui.machine("action", name.to_s, "end")
action_result
end
# preserve returning environment after machine action runs
return return_env
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={})
if !extra_env.is_a?(Hash)
extra_env = {}
end
# Run the action with the action runner on the environment
env = {ui: @ui}.merge(extra_env).merge(
raw_action_name: name,
action_name: "machine_action_#{name}".to_sym,
machine: self,
machine_action: name
)
@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}")
id_file = nil
if @data_dir
# 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")
end
if value
if id_file
# Write the "id" file with the id given.
id_file.open("w+") do |f|
f.write(value)
end
end
if uid_file
# Write the user id that created this machine
uid_file.open("w+") do |f|
f.write(Process.uid.to_s)
end
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.architecture = @architecture
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,
"architecture" => @box.architecture,
"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 && id_file.file?
uid_file.delete if uid_file && uid_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
if @data_dir
# 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
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 reloads the ID of the underlying machine.
def reload
old_id = @id
@id = nil
if @data_dir
# 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_content = id_file.read.strip if id_file.file?
if !id_content.to_s.empty?
@id = id_content
end
end
if @id != old_id && @provider
# It changed, notify the provider
@provider.machine_id_changed
end
@id
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[:keys_only] ||= @config.ssh.default.keys_only
info[:verify_host_key] ||= @config.ssh.default.verify_host_key
info[:username] ||= @config.ssh.default.username
info[:remote_user] ||= @config.ssh.default.remote_user
info[:compression] ||= @config.ssh.default.compression
info[:dsa_authentication] ||= @config.ssh.default.dsa_authentication
info[:extra_args] ||= @config.ssh.default.extra_args
info[:config] ||= @config.ssh.default.config
# 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[:keys_only] = @config.ssh.keys_only
info[:verify_host_key] = @config.ssh.verify_host_key
info[:compression] = @config.ssh.compression
info[:dsa_authentication] = @config.ssh.dsa_authentication
info[:username] = @config.ssh.username if @config.ssh.username
info[:password] = @config.ssh.password if @config.ssh.password
info[:remote_user] = @config.ssh.remote_user if @config.ssh.remote_user
info[:extra_args] = @config.ssh.extra_args if @config.ssh.extra_args
info[:config] = @config.ssh.config if @config.ssh.config
# We also set some fields that are purely controlled by Vagrant
info[:forward_agent] = @config.ssh.forward_agent
info[:forward_x11] = @config.ssh.forward_x11
info[:forward_env] = @config.ssh.forward_env
info[:connect_timeout] = @config.ssh.connect_timeout
info[:ssh_command] = @config.ssh.ssh_command if @config.ssh.ssh_command
# 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_paths
end
end
# If we have a private key in our data dir, then use that
if @data_dir && !@config.ssh.private_key_path
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
# Check that the private key permissions are valid
info[:private_key_path].each do |path|
key_path = Pathname.new(path)
if key_path.exist?
Vagrant::Util::SSH.check_key_permissions(key_path)
end
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
# active_machines provides access to query this info on each machine
# from a different thread, ensure multiple machines do not access
# the locked entry simultaneously as this triggers a locked machine
# exception.
@state_mutex.synchronize do
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
end
result
end
# Returns the state of this machine. The state is queried from the
# backing provider, so it can be any arbitrary symbol.
#
# @param [Symbol] state of machine
# @return [Entry] entry of recovered machine
def recover_machine(state)
entry = @env.machine_index.get(index_uuid)
if entry
@env.machine_index.release(entry)
return entry
end
entry = MachineIndex::Entry.new(id=index_uuid, {})
entry.local_data_path = @env.local_data_path
entry.name = @name.to_s
entry.provider = @provider_name.to_s
entry.state = state
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,
"architecture" => @box.architecture,
"version" => @box.version.to_s,
}
end
@state_mutex.synchronize do
entry = @env.machine_index.recover(entry)
@env.machine_index.release(entry)
end
return entry
end
# Returns the user ID that created this machine. This is specific to
# the host machine that this was created on.
#
# @return [String]
def uid
path = uid_file
return nil if !path
return nil if !path.file?
return uid_file.read.chomp
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
# This returns the set of shared folders that should be done for
# this machine. It returns the folders in a hash keyed by the
# implementation class for the synced folders.
#
# @return [Hash<Symbol, Hash<String, Hash>>]
def synced_folders
self.class.synced_folders(self)
end
protected
# Returns the path to the file that stores the UID.
def uid_file
return nil if !@data_dir
@data_dir.join("creator_uid")
end
# Checks the current directory for a given machine
# and displays a warning if that machine has moved
# from its previous location on disk. If the machine
# has moved, it prints a warning to the user.
def check_cwd
desired_encoding = @env.root_path.to_s.encoding
vagrant_cwd_filepath = @data_dir.join('vagrant_cwd')
vagrant_cwd = if File.exist?(vagrant_cwd_filepath)
File.read(vagrant_cwd_filepath,
external_encoding: desired_encoding
).chomp
end
if !File.identical?(vagrant_cwd.to_s, @env.root_path.to_s)
if vagrant_cwd
ui.warn(I18n.t(
'vagrant.moved_cwd',
old_wd: "#{vagrant_cwd}",
current_wd: "#{@env.root_path.to_s}"))
end
File.write(vagrant_cwd_filepath, @env.root_path.to_s,
external_encoding: desired_encoding
)
end
end
end
end