diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 08120d930..3c17b4e00 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -1,64 +1,613 @@ -require "vagrant/machine/thick" -require "vagrant/machine/thin" +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 :Thick, "vagrant/machine/thick" - autoload :Thin, "vagrant/machine/thin" + autoload :Remote, "vagrant/machine/remote" extend Vagrant::Action::Builtin::MixinSyncedFolders - 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 box that is backing this machine. + # + # @return [Box] + attr_accessor :box - ATTR_READERS.each do |attr_name| - define_method(attr_name) { raise NotImplementedError, - "`#{self.class.name}##{attr_name}` has not been implemented" } + # 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 - 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 + # 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}") - METHODS.each do |m_name| - define_method(m_name) { |*_| raise NotImplementedError, - "`#{self.class.name}##{m_name}` has not been implemented" } - end + opts ||= {} - class << self + # Determine whether we lock or not + lock = true + lock = opts.delete(:lock) if opts.key?(:lock) - alias_method :real_new, :new + # 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 - # 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__) + 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 - if from_subclass - real_new(*args) - else - if Vagrant.server_mode? - Thin.new(*args) - else - Thick.new(*args) + + # 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 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 @@ -83,5 +632,39 @@ 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 diff --git a/lib/vagrant/machine/thin.rb b/lib/vagrant/machine/remote.rb similarity index 97% rename from lib/vagrant/machine/thin.rb rename to lib/vagrant/machine/remote.rb index 7b067b427..17435d4cf 100644 --- a/lib/vagrant/machine/thin.rb +++ b/lib/vagrant/machine/remote.rb @@ -1,15 +1,15 @@ module Vagrant class Machine - class Thin < Machine - extend Vagrant::Action::Builtin::MixinSyncedFolders + # This module enables the Machine for server mode + module Remote - attr_accessor :box, :config, :data_dir, :env, :name, :provider, :provider_config, - :provider_name, :provider_options, :triggers, :ui, :vagrantfile - - - attr_reader :client - # NOTE: The client is internal so don't make it publicly accessible -# protected :client + # Add an attribute reader for the client + # when applied to the Machine class + def self.included(klass) + klass.class_eval do + attr_reader :client + end + end 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") diff --git a/lib/vagrant/machine/thick.rb b/lib/vagrant/machine/thick.rb deleted file mode 100644 index c4c3bd8ee..000000000 --- a/lib/vagrant/machine/thick.rb +++ /dev/null @@ -1,671 +0,0 @@ -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 - # This is the pure ruby (legacy) implementation - class Thick < Machine - 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 - # 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 - 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 - 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 - # 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>] - 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 -end