From 03ac9cd0150bfb15d5cb4885d7be32e368101253 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Thu, 28 Jan 2021 17:03:48 -0800 Subject: [PATCH] Provide machine implementation based on server mode --- lib/vagrant/machine.rb | 795 +++-------------------------------------- 1 file changed, 41 insertions(+), 754 deletions(-) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index ea8ea165a..61257768b 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -1,744 +1,65 @@ -require_relative "util/ssh" -require_relative "action/builtin/mixin_synced_folders" - -require 'vagrant/proto/gen/plugin/core_services_pb' -require 'vagrant/proto/gen/plugin/core_pb' - -require 'grpc' -require "digest/md5" -require "thread" - -require "log4r" +require "vagrant/machine/thick" +require "vagrant/machine/thin" module Vagrant - # This is the client that does CRUD operations against - # the core Vagrant service - class MachineClient - - # @params [String] endpoint for the core service - def initialize(server_endpoint) - @client = Hashicorp::Vagrant::Sdk::MachineService::Stub.new(server_endpoint, :this_channel_is_insecure) - end - - def get_name(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::GetNameRequest.new( - machine: machine_ref - ) - resp_machine = @client.get_name(req) - resp_machine.name - end - - def set_name(id, name) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::SetNameRequest.new( - machine: machine_ref, - name: name - ) - @client.set_name(req) - end - - def get_id(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::GetIDRequest.new( - machine: machine_ref - ) - resp_machine = @client.get_id(req) - resp_machine.id - end - - def set_id(id, new_id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::SetNameRequest.new( - machine: machine_ref, - id: new_id - ) - @client.set_id(req) - end - - def get_box(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::BoxRequest.new( - machine: machine_ref - ) - resp = @client.box(req) - Vagrant::Box.new( - resp.box.name, - resp.box.provider.to_sym, - resp.box.version, - Pathname.new(resp.box.directory), - ) - end - - def get_data_dir(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::DatadirRequest.new( - machine: machine_ref - ) - resp = @client.datadir(req) - resp.datadir - end - - def get_local_data_path(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::LocalDataPathRequest.new( - machine: machine_ref - ) - resp = @client.localdatapath(req) - resp.path - end - - def get_provider(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::ProviderRequest.new( - machine: machine_ref - ) - resp = @client.provider(req) - resp - end - - def get_vagrantfile_name(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::VagrantfileNameRequest.new( - machine: machine_ref - ) - resp = @client.vagrantfile_name(req) - resp.name - end - - def get_vagrantfile_path(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::VagrantfilePathRequest.new( - machine: machine_ref - ) - resp = @client.vagrantfile_path(req) - Pathname.new(resp.path) - end - - def updated_at(id) - machine_ref = Hashicorp::Vagrant::Sdk::Ref::Machine.new(resource_id: id) - req = Hashicorp::Vagrant::Sdk::Machine::UpdatedAtRequest.new( - machine: machine_ref - ) - resp = @client.updated_at(req) - resp.updated_at - end - - # Get a machine by id - # - # @param [String] machine id - # @param [TerminalClient] - # @return [Machine] - def get_machine(id, ui_client) - provider_plugin = Vagrant.plugin("2").manager.providers[:virtualbox] - provider_cls = provider_plugin[0] - provider_options = provider_plugin[1] - - datadir = get_data_dir(id) - vagrantfile_name = get_vagrantfile_name(id) - vagrantfile_path = get_vagrantfile_path(id) - env = Vagrant::Environment.new( - cwd: vagrantfile_path, # TODO: this is probably not ideal - home_path: datadir.root_dir, - ui_class: Vagrant::UI::RemoteUI, - ui_opts: [ui_client], - vagrantfile_name: vagrantfile_name, - ) - - Machine.new( - "virtualbox", provider_cls, {}, provider_options, - nil, env, self, id - ) - end - end - # 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 :Client, "vagrant/machine/client" + autoload :Thick, "vagrant/machine/thick" + autoload :Thin, "vagrant/machine/thin" + extend Vagrant::Action::Builtin::MixinSyncedFolders - # Configuration for the machine. - # - # @return [Object] - attr_accessor :config + ATTR_READERS = [ + :data_dir, :env, :id, :name, :provider, :provider_name, + :provider_options, :triggers, :ui, :vagrantfile + ] + ATTR_ACCESSORS = [ + :box, :config, :provider_config + ] + METHODS = [ + :action, :action_raw, :communicate, :guest, :id=, :index_uuid, + :inspect, :reload, :ssh_info, :state, :recover_machine, :uid + ] - # The environment that this machine is a part of. - # - # @return [Environment] - attr_reader :env - - # TODO: not sure what to do about the provider bits yet - - # 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 - - # TODO: not sure what to do about the triggers bits yet - # 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 - - - # Initialize a new machine. - # - # @param [Object] config The configuration for this machine. - # @param [Environment] env The environment that this machine is a - # part of. - # @param [MachineClient] client - # @param [String] machine_id - def initialize( - provider_name, provider_cls, provider_config, provider_options, config, - env, client, machine_id - ) - @logger = Log4r::Logger.new("vagrant::machine") - - @client = client - @machine_id = machine_id - @config = config - @env = env - @guest = Guest.new( - self, - Vagrant.plugin("2").manager.guests, - Vagrant.plugin("2").manager.guest_capabilities - ) - - # TODO: Need to stream this back to core service - @ui = Vagrant::UI::Prefixed.new(@env.ui, @name) - @ui_mutex = Mutex.new - @state_mutex = Mutex.new - # TODO: reenable this once env stuff has been sorted - # @triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self, @ui) - - # Initializes the provider last so that it has access to all the - # state we setup on this machine. - # TODO: renable provider - @provider = provider_cls.new(self) - @provider._initialize(provider_name, self) - @provider_config = provider_config - @provider_name = provider_name - @provider_options = provider_options - - # If we're using WinRM, we eager load the plugin because of - # GH-3390 - # TODO: uncomment - # if @config.vm.communicator == :winrm - # @logger.debug("Eager loading WinRM communicator to avoid GH-3390") - # communicate - # end - - # Output a bunch of information about this machine in - # machine-readable format in case someone is listening. - @ui.machine("metadata", "provider", provider_name) + ATTR_READERS.each do |attr_name| + define_method(attr_name) { raise NotImplementedError, + "`#{self.class.name}##{attr_name}` has not been implemented" } end - # Name of the machine. This is assigned by the Vagrantfile. - # - # @return [Symbol] - def name - @client.get_name(@machine_id).to_sym + ATTR_ACCESSORS.each do |attr_name| + define_method(attr_name) { raise NotImplementedError, + "`#{self.class.name}##{attr_name}` has not been implemented" } + define_method("#{attr_name}=") { |_| raise NotImplementedError, + "`#{self.class.name}##{attr_name}=` has not been implemented" } end - # TODO: does this also need a set box? - # The box that is backing this machine. - # - # @return [Box] - def box - @client.get_box(@machine_id) + METHODS.each do |m_name| + define_method(m_name) { |*_| raise NotImplementedError, + "`#{self.class.name}##{m_name}` has not been implemented" } end - # 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] - def id - @client.get_id(@machine_id) - end + class << self - # Directory where machine-specific data can be stored. - # - # @return [Pathname] - def data_dir - dd = @client.get_data_dir(@machine_id) - Pathname.new(dd.data_dir) - end + alias_method :real_new, :new - # The Vagrantfile that this machine is attached to. - # - # @return [Vagrantfile] - def vagrantfile - # TODO - 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 - # An environment is required for triggers to function properly. This is - # passed in specifically for the `#Action::Warden` class triggers. We call it - # `:trigger_env` instead of `env` in case it collides with an existing environment - extra_env[:trigger_env] = @env - - 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 + # Creates a new machine instance based on the mode Vagrant is currently operating + def new(*args) + from_subclass = caller.detect do |line| + line.start_with?(__FILE__) 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.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 && 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 + if from_subclass + real_new(*args) + else + if Vagrant.server_mode? + Thin.new(*args) + else + Thick.new(*args) 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 - elsif info[:keys_only] - 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 && !@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, - "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 @@ -763,39 +84,5 @@ module Vagrant 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