diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 747214e85..c7fd558a3 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -66,7 +66,6 @@ module Vagrant autoload :BoxCollection, 'vagrant/box_collection' autoload :CLI, 'vagrant/cli' autoload :Command, 'vagrant/command' - autoload :Communication, 'vagrant/communication' autoload :Config, 'vagrant/config' autoload :DataStore, 'vagrant/data_store' autoload :Downloaders, 'vagrant/downloaders' @@ -76,27 +75,23 @@ module Vagrant autoload :Errors, 'vagrant/errors' autoload :Guest, 'vagrant/guest' autoload :Hosts, 'vagrant/hosts' + autoload :Machine, 'vagrant/machine' autoload :Plugin, 'vagrant/plugin' - autoload :SSH, 'vagrant/ssh' autoload :TestHelpers, 'vagrant/test_helpers' autoload :UI, 'vagrant/ui' autoload :Util, 'vagrant/util' - autoload :VM, 'vagrant/vm' # These are the various plugin versions and their components in # a lazy loaded Hash-like structure. - c = PLUGIN_COMPONENTS = Registry.new - c.register(:"1") { Plugin::V1::Plugin } - c.register([:"1", :command]) { Plugin::V1::Command } - c.register([:"1", :config]) { Plugin::V1::Config } - c.register([:"1", :guest]) { Plugin::V1::Guest } - c.register([:"1", :host]) { Plugin::V1::Host } - c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } - - # Returns a `Vagrant::Registry` object that contains all the built-in - # middleware stacks. - def self.actions - @actions ||= Vagrant::Action::Builtin.new + PLUGIN_COMPONENTS = Registry.new.tap do |c| + c.register(:"1") { Plugin::V1::Plugin } + c.register([:"1", :command]) { Plugin::V1::Command } + c.register([:"1", :communicator]) { Plugin::V1::Communicator } + c.register([:"1", :config]) { Plugin::V1::Config } + c.register([:"1", :guest]) { Plugin::V1::Guest } + c.register([:"1", :host]) { Plugin::V1::Host } + c.register([:"1", :provider]) { Plugin::V1::Provider } + c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } end # The source root is the path to the root directory of diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 8b816ae3d..0df155d44 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -2,60 +2,35 @@ require 'vagrant/action/builder' module Vagrant module Action - autoload :Builtin, 'vagrant/action/builtin' autoload :Environment, 'vagrant/action/environment' autoload :Runner, 'vagrant/action/runner' autoload :Warden, 'vagrant/action/warden' - module Box - autoload :Add, 'vagrant/action/box/add' - autoload :Download, 'vagrant/action/box/download' - autoload :Verify, 'vagrant/action/box/verify' - end - - module Env - autoload :Set, 'vagrant/action/env/set' + # Builtin contains middleware classes that are shipped with Vagrant-core + # and are thus available to all plugins as a "standard library" of sorts. + module Builtin + autoload :BoxAdd, "vagrant/action/builtin/box_add" + autoload :Call, "vagrant/action/builtin/call" + autoload :Confirm, "vagrant/action/builtin/confirm" + autoload :EnvSet, "vagrant/action/builtin/env_set" + autoload :SSHExec, "vagrant/action/builtin/ssh_exec" + autoload :SSHRun, "vagrant/action/builtin/ssh_run" end module General - autoload :CheckVirtualbox, 'vagrant/action/general/check_virtualbox' autoload :Package, 'vagrant/action/general/package' autoload :Validate, 'vagrant/action/general/validate' end - module VM - autoload :Boot, 'vagrant/action/vm/boot' - autoload :CheckAccessible, 'vagrant/action/vm/check_accessible' - autoload :CheckBox, 'vagrant/action/vm/check_box' - autoload :CheckGuestAdditions, 'vagrant/action/vm/check_guest_additions' - autoload :CheckPortCollisions, 'vagrant/action/vm/check_port_collisions' - autoload :CleanMachineFolder, 'vagrant/action/vm/clean_machine_folder' - autoload :ClearForwardedPorts, 'vagrant/action/vm/clear_forwarded_ports' - autoload :ClearNetworkInterfaces, 'vagrant/action/vm/clear_network_interfaces' - autoload :ClearSharedFolders, 'vagrant/action/vm/clear_shared_folders' - autoload :Customize, 'vagrant/action/vm/customize' - autoload :DefaultName, 'vagrant/action/vm/default_name' - autoload :Destroy, 'vagrant/action/vm/destroy' - autoload :DestroyUnusedNetworkInterfaces, 'vagrant/action/vm/destroy_unused_network_interfaces' - autoload :DiscardState, 'vagrant/action/vm/discard_state' - autoload :Export, 'vagrant/action/vm/export' - autoload :ForwardPorts, 'vagrant/action/vm/forward_ports' - autoload :Halt, 'vagrant/action/vm/halt' - autoload :HostName, 'vagrant/action/vm/host_name' - autoload :Import, 'vagrant/action/vm/import' - autoload :MatchMACAddress, 'vagrant/action/vm/match_mac_address' - autoload :Network, 'vagrant/action/vm/network' - autoload :NFS, 'vagrant/action/vm/nfs' - autoload :Package, 'vagrant/action/vm/package' - autoload :PackageVagrantfile, 'vagrant/action/vm/package_vagrantfile' - autoload :Provision, 'vagrant/action/vm/provision' - autoload :ProvisionerCleanup, 'vagrant/action/vm/provisioner_cleanup' - autoload :PruneNFSExports, 'vagrant/action/vm/prune_nfs_exports' - autoload :Resume, 'vagrant/action/vm/resume' - autoload :SaneDefaults, 'vagrant/action/vm/sane_defaults' - autoload :ShareFolders, 'vagrant/action/vm/share_folders' - autoload :SetupPackageFiles, 'vagrant/action/vm/setup_package_files' - autoload :Suspend, 'vagrant/action/vm/suspend' + # This is the action that will add a box from a URL. This middleware + # sequence is built-in to Vagrant. Plugins can hook into this like any + # other middleware sequence. This is particularly useful for provider + # plugins, which can hook in to do things like verification of boxes + # that are downloaded. + def self.action_box_add + Builder.new.tap do |b| + b.use Builtin::BoxAdd + end end end end diff --git a/lib/vagrant/action/box/add.rb b/lib/vagrant/action/box/add.rb deleted file mode 100644 index e80d52970..000000000 --- a/lib/vagrant/action/box/add.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Vagrant - module Action - module Box - # Adds a downloaded box file to the environment's box collection. - # This handles unpacking the box. See {BoxCollection#add} for more - # information. - class Add - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env[:ui].info I18n.t("vagrant.actions.box.add.adding", :name => env[:box_name]) - - begin - env[:box_collection].add(env[:box_download_temp_path], env[:box_name]) - rescue Vagrant::Errors::BoxUpgradeRequired - # Upgrade the box - env[:box_collection].upgrade(env[:box_name]) - - # Try adding it again - retry - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/box/download.rb b/lib/vagrant/action/box/download.rb deleted file mode 100644 index 144546f27..000000000 --- a/lib/vagrant/action/box/download.rb +++ /dev/null @@ -1,84 +0,0 @@ -module Vagrant - module Action - module Box - class Download - BASENAME = "box" - - include Util - - attr_reader :temp_path - - def initialize(app, env) - @app = app - @env = env - @env["download.classes"] ||= [] - @env["download.classes"] += [Downloaders::HTTP, Downloaders::File] - @downloader = nil - end - - def call(env) - @env = env - - download if instantiate_downloader - @app.call(@env) - - recover(env) # called in both cases to cleanup workspace - end - - def instantiate_downloader - # Assign to a temporary variable since this is easier to type out, - # since it is used so many times. - classes = @env["download.classes"] - - # Find the class to use. - classes.each_index do |i| - klass = classes[i] - - # Use the class if it matches the given URI or if this - # is the last class... - if classes.length == (i + 1) || klass.match?(@env[:box_url]) - @env[:ui].info I18n.t("vagrant.actions.box.download.with", :class => klass.to_s) - @downloader = klass.new(@env[:ui]) - break - end - end - - # This line should never be reached, but we'll keep this here - # just in case for now. - raise Errors::BoxDownloadUnknownType if !@downloader - - @downloader.prepare(@env[:box_url]) - true - end - - def download - with_tempfile do |tempfile| - download_to(tempfile) - @temp_path = @env[:box_download_temp_path] = tempfile.path - end - end - - def recover(env) - if temp_path && File.exist?(temp_path) - env[:ui].info I18n.t("vagrant.actions.box.download.cleaning") - File.unlink(temp_path) - end - end - - def with_tempfile - File.open(box_temp_path, Platform.tar_file_options) do |tempfile| - yield tempfile - end - end - - def box_temp_path - @env[:tmp_path].join(BASENAME + Time.now.to_i.to_s) - end - - def download_to(f) - @downloader.download!(@env[:box_url], f) - end - end - end - end -end diff --git a/lib/vagrant/action/box/verify.rb b/lib/vagrant/action/box/verify.rb deleted file mode 100644 index 082cf4fc0..000000000 --- a/lib/vagrant/action/box/verify.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Vagrant - module Action - module Box - class Verify - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - @env[:ui].info I18n.t("vagrant.actions.box.verify.verifying") - - box = env[:box_collection].find(env[:box_name], :virtualbox) - driver = Driver::VirtualBox.new(nil) - if !driver.verify_image(box.directory.join("box.ovf").to_s) - raise Errors::BoxVerificationFailed - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/builder.rb b/lib/vagrant/action/builder.rb index da95af792..a13912403 100644 --- a/lib/vagrant/action/builder.rb +++ b/lib/vagrant/action/builder.rb @@ -9,18 +9,21 @@ module Vagrant # # Building an action sequence is very easy: # - # app = Vagrant::Action::Builder.new do - # use MiddlewareA - # use MiddlewareB + # app = Vagrant::Action::Builder.new.tap do |b| + # b.use MiddlewareA + # b.use MiddlewareB # end # # Vagrant::Action.run(app) # class Builder - # Initializes the builder. An optional block can be passed which - # will be evaluated in the context of the instance. - def initialize(&block) - instance_eval(&block) if block_given? + # This is a shortcut for a middleware sequence with only one item + # in it. For a description of the arguments and the documentation, please + # see {#use} instead. + # + # @return [Builder] + def self.build(middleware, *args, &block) + new.use(middleware, *args, &block) end # Returns a mergeable version of the builder. If `use` is called with @@ -38,11 +41,6 @@ module Vagrant # # @param [Class] middleware The middleware class def use(middleware, *args, &block) - # Prepend with a environment setter if args are given - if !args.empty? && args.first.is_a?(Hash) && middleware != Env::Set - self.use(Env::Set, args.shift, &block) - end - if middleware.kind_of?(Builder) # Merge in the other builder's stack into our own self.stack.concat(middleware.stack) diff --git a/lib/vagrant/action/builtin.rb b/lib/vagrant/action/builtin.rb deleted file mode 100644 index b047ef43e..000000000 --- a/lib/vagrant/action/builtin.rb +++ /dev/null @@ -1,157 +0,0 @@ -module Vagrant - module Action - # A registry object containing the built-in middleware stacks. - class Builtin < Registry - def initialize - # Properly initialize the registry object - super - - # Register all the built-in stacks - register_builtin! - end - - protected - - def register_builtin! - # We do this so that the blocks below have a variable to access the - # outer registry. - registry = self - - # provision - Provisions a running VM - register(:provision) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::Provision - end - end - - # start - Starts a VM, assuming it already exists on the - # environment. - register(:start) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::CleanMachineFolder - use VM::ClearForwardedPorts - use VM::CheckPortCollisions, :port_collision_handler => :correct - use VM::ForwardPorts - use VM::Provision - use VM::PruneNFSExports - use VM::NFS - use VM::ClearSharedFolders - use VM::ShareFolders - use VM::ClearNetworkInterfaces - use VM::Network - use VM::HostName - use VM::SaneDefaults - use VM::Customize - use VM::Boot - end - end - - # halt - Halts the VM, attempting gracefully but then forcing - # a restart if fails. - register(:halt) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::DiscardState - use VM::Halt - end - end - - # suspend - Suspends the VM - register(:suspend) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::Suspend - end - end - - # resume - Resume a VM - register(:resume) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::CheckPortCollisions - use VM::Resume - end - end - - # reload - Halts then restarts the VM - register(:reload) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use registry.get(:halt) - use registry.get(:start) - end - end - - # up - Imports, prepares, then starts a fresh VM. - register(:up) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use VM::CheckBox - use VM::Import - use VM::CheckGuestAdditions - use VM::DefaultName - use VM::MatchMACAddress - use registry.get(:start) - end - end - - # destroy - Halts, cleans up, and destroys an existing VM - register(:destroy) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::CheckAccessible - use registry.get(:halt), :force => true - use VM::ProvisionerCleanup - use VM::PruneNFSExports - use VM::Destroy - use VM::CleanMachineFolder - use VM::DestroyUnusedNetworkInterfaces - end - end - - # package - Export and package the VM - register(:package) do - Builder.new do - use General::CheckVirtualbox - use General::Validate - use VM::SetupPackageFiles - use VM::CheckAccessible - use registry.get(:halt) - use VM::ClearForwardedPorts - use VM::ClearSharedFolders - use VM::Export - use VM::PackageVagrantfile - use VM::Package - end - end - - # box_add - Download and add a box. - register(:box_add) do - Builder.new do - use General::CheckVirtualbox - use Box::Download - use Box::Add - use Box::Verify - end - end - end - end - end -end diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb new file mode 100644 index 000000000..364cd27e6 --- /dev/null +++ b/lib/vagrant/action/builtin/box_add.rb @@ -0,0 +1,75 @@ +require "vagrant/util/platform" + +module Vagrant + module Action + module Builtin + # This middleware will download a remote box and add it to the + # given box collection. + class BoxAdd + def initialize(app, env) + @app = app + end + + def call(env) + # Instantiate the downloader + downloader = download_klass(env[:box_url]).new(env[:ui]) + env[:ui].info I18n.t("vagrant.actions.box.download.with", + :class => downloader.class.to_s) + + # Download the box to a temporary path. We store the temporary + # path as an instance variable so that the `#recover` method can + # access it. + @temp_path = env[:tmp_path].join("box" + Time.now.to_i.to_s) + File.open(@temp_path, Vagrant::Util::Platform.tar_file_options) do |f| + downloader.download!(env[:box_url], f) + end + + # Add the box + env[:ui].info I18n.t("vagrant.actions.box.add.adding", :name => env[:box_name]) + begin + env[:box_collection].add(@temp_path, env[:box_name]) + rescue Vagrant::Errors::BoxUpgradeRequired + # Upgrade the box + env[:box_collection].upgrade(env[:box_name]) + + # Try adding it again + retry + end + + # Call the 'recover' method in all cases to clean up the + # downloaded temporary file. + recover(env) + + # Carry on! + @app.call(env) + end + + def download_klass(url) + # This is hardcoded for now. In the future I'd like to make this + # pluginnable as well. + classes = [Downloaders::HTTP, Downloaders::File] + + # Find the class to use. + classes.each_index do |i| + klass = classes[i] + + # Use the class if it matches the given URI or if this + # is the last class... + return klass if classes.length == (i + 1) || klass.match?(url) + end + + # If no downloader knows how to download this file, then we + # raise an exception. + raise Errors::BoxDownloadUnknownType + end + + def recover(env) + if @temp_path && File.exist?(@temp_path) + env[:ui].info I18n.t("vagrant.actions.box.download.cleaning") + File.unlink(@temp_path) + end + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/call.rb b/lib/vagrant/action/builtin/call.rb new file mode 100644 index 000000000..2d29a68de --- /dev/null +++ b/lib/vagrant/action/builtin/call.rb @@ -0,0 +1,53 @@ +module Vagrant + module Action + module Builtin + # This middleware class allows a sort of "conditional" run within + # a single middlware sequence. It takes another middleware runnable, + # runs it with the same environment, then yields the resulting env to a block, + # allowing that block to determine the next course of action in the + # middleware sequence. + # + # The first argument to this middleware sequence is anywhere middleware + # runnable, whether it be a class, lambda, or something else that + # responds to `call`. This middleware runnable is run with the same + # environment as this class. + # + # After running, {Call} takes the environment and yields it to a block + # given to initialize the class, along with an instance of {Builder}. + # The result is used to build up a new sequence on the given builder. + # This builder is then run. + class Call + # For documentation, read the description of the {Call} class. + # + # @param [Object] callable A valid middleware runnable object. This + # can be a class, a lambda, or an object that responds to `call`. + # @yield [result, builder] This block is expected to build on `builder` + # which is the next middleware sequence that will be run. + def initialize(app, env, callable, &block) + raise ArgumentError, "A block must be given to Call" if !block + + @app = app + @callable = callable + @block = block + end + + def call(env) + runner = Runner.new + + # Run our callable with our environment + new_env = runner.run(@callable, env) + + # Build our new builder based on the result + builder = Builder.new + @block.call(new_env, builder) + + # Run the result with our new environment + final_env = runner.run(builder, new_env) + + # Call the next step using our final environment + @app.call(final_env) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/confirm.rb b/lib/vagrant/action/builtin/confirm.rb new file mode 100644 index 000000000..d877f9dea --- /dev/null +++ b/lib/vagrant/action/builtin/confirm.rb @@ -0,0 +1,28 @@ +module Vagrant + module Action + module Builtin + # This class asks the user to confirm some sort of question with + # a "Y/N" question. The only parameter is the text to ask the user. + # The result is placed in `env[:result]` so that it can be used + # with the {Call} class. + class Confirm + # For documentation, read the description of the {Confirm} class. + # + # @param [String] message The message to ask the user. + def initialize(app, env, message) + @app = app + @message = message + end + + def call(env) + # Ask the user the message and store the result + choice = nil + choice = env[:ui].ask(@message) + env[:result] = choice && choice.upcase == "Y" + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/env_set.rb b/lib/vagrant/action/builtin/env_set.rb new file mode 100644 index 000000000..bad9cb11d --- /dev/null +++ b/lib/vagrant/action/builtin/env_set.rb @@ -0,0 +1,24 @@ +module Vagrant + module Action + module Builtin + # This middleware class allows you to modify the environment hash + # in the middle of a middleware sequence. The new environmental data + # will take affect at this stage in the middleware and will persist + # through. + class EnvSet + def initialize(app, env, new_env=nil) + @app = app + @new_env = new_env || {} + end + + def call(env) + # Merge in the new data + env.merge!(@new_env) + + # Carry on + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/ssh_exec.rb b/lib/vagrant/action/builtin/ssh_exec.rb new file mode 100644 index 000000000..372707b07 --- /dev/null +++ b/lib/vagrant/action/builtin/ssh_exec.rb @@ -0,0 +1,38 @@ +require "vagrant/util/ssh" + +module Vagrant + module Action + module Builtin + # This class will exec into a full fledged SSH console into the + # remote machine. This middleware assumes that the VM is running and + # ready for SSH, and uses the {Machine#ssh_info} method to retrieve + # SSH information necessary to connect. + # + # Note: If there are any middleware after `SSHExec`, they will **not** + # run, since exec replaces the currently running process. + class SSHExec + # For quick access to the `SSH` class. + include Vagrant::Util + + def initialize(app, env) + @app = app + end + + def call(env) + # Grab the SSH info from the machine + info = env[:machine].ssh_info + + # If the result is nil, then the machine is telling us that it is + # not yet ready for SSH, so we raise this exception. + raise Errors::SSHNotReady if info.nil? + + # Check the SSH key permissions + SSH.check_key_permissions(info[:private_key_path]) + + # Exec! + SSH.exec(info, env[:ssh_opts]) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/ssh_run.rb b/lib/vagrant/action/builtin/ssh_run.rb new file mode 100644 index 000000000..1969600eb --- /dev/null +++ b/lib/vagrant/action/builtin/ssh_run.rb @@ -0,0 +1,43 @@ +require "log4r" + +module Vagrant + module Action + module Builtin + # This class will run a single command on the remote machine and will + # mirror the output to the UI. The resulting exit status of the command + # will exist in the `:ssh_run_exit_status` key in the environment. + class SSHRun + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::ssh_run") + end + + def call(env) + command = env[:ssh_run_command] + + @logger.debug("Executing command: #{command}") + exit_status = 0 + exit_status = env[:machine].communicate.execute(command, :error_check => false) do |type, data| + # Determine the proper channel to send the output onto depending + # on the type of data we are receiving. + channel = type == :stdout ? :out : :error + + # Print the output as it comes in, but don't prefix it and don't + # force a new line so that the output is properly preserved however + # it may be formatted. + env[:ui].info(data.to_s, + :prefix => false, + :new_line => false, + :channel => channel) + end + + # Set the exit status on a known environmental variable + env[:ssh_run_exit_status] = exit_status + + # Call the next middleware + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/env/set.rb b/lib/vagrant/action/env/set.rb deleted file mode 100644 index 38e23915f..000000000 --- a/lib/vagrant/action/env/set.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Vagrant - module Action - module Env - # A middleware which just sets up the environment with some - # options which are passed to it. - class Set - def initialize(app, env, options=nil) - @app = app - @options = options || {} - end - - def call(env) - # Merge the options that were given to us - env.merge!(@options) - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/general/check_virtualbox.rb b/lib/vagrant/action/general/check_virtualbox.rb deleted file mode 100644 index cbaecc8ec..000000000 --- a/lib/vagrant/action/general/check_virtualbox.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Vagrant - module Action - module General - # Checks that virtualbox is installed and ready to be used. - class CheckVirtualbox - def initialize(app, env) - @app = app - end - - def call(env) - # Certain actions may not actually have a VM, and thus no - # driver, so we have to be clever about obtaining an instance - # of the driver. - driver = nil - driver = env[:vm].driver if env[:vm] - driver = Driver::VirtualBox.new(nil) if !driver - - # Verify that it is ready to go! This will raise an exception - # if anything goes wrong. - driver.verify! - - # Carry on. - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/general/validate.rb b/lib/vagrant/action/general/validate.rb index 5045918dd..f566b793f 100644 --- a/lib/vagrant/action/general/validate.rb +++ b/lib/vagrant/action/general/validate.rb @@ -9,7 +9,10 @@ module Vagrant end def call(env) - env[:vm].config.validate!(env[:vm].env) if !env.has_key?("validate") || env["validate"] + if !env.has_key?(:validate) || env[:validate] + env[:machine].config.validate!(env[:machine].env) + end + @app.call(env) end end diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index a28551b44..b92d2726c 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -10,8 +10,7 @@ module Vagrant class Runner @@reported_interrupt = false - def initialize(registry, globals=nil, &block) - @registry = registry + def initialize(globals=nil, &block) @globals = globals || {} @lazy_globals = block @logger = Log4r::Logger.new("vagrant::action::runner") @@ -19,8 +18,7 @@ module Vagrant def run(callable_id, options=nil) callable = callable_id - callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class) - callable = registry_sequence(callable_id) if callable_id.kind_of?(Symbol) + callable = Builder.build(callable_id) if callable_id.kind_of?(Class) raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call) # Create the initial environment with the options given @@ -47,28 +45,10 @@ module Vagrant # We place a process lock around every action that is called @logger.info("Running action: #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } - end - protected - - def registry_sequence(id) - # Attempt to get the sequence - seq = @registry.get(id) - return nil if !seq - - # Go through all the registered plugins and get all the hooks - # for this sequence. - Vagrant.plugin("1").registered.each do |plugin| - hooks = plugin.action_hook(Vagrant::Plugin::V1::Plugin::ALL_ACTIONS) - hooks += plugin.action_hook(id) - - hooks.each do |hook| - hook.call(seq) - end - end - - # Return the sequence - seq + # Return the environment in case there are things in there that + # the caller wants to use. + environment end end end diff --git a/lib/vagrant/action/vm/destroy_unused_network_interfaces.rb b/lib/vagrant/action/vm/destroy_unused_network_interfaces.rb deleted file mode 100644 index a0fb94c8b..000000000 --- a/lib/vagrant/action/vm/destroy_unused_network_interfaces.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Vagrant - module Action - module VM - # Destroys the unused host only interfaces. This middleware cleans - # up any created host only networks. - class DestroyUnusedNetworkInterfaces - def initialize(app, env) - @app = app - end - - def call(env) - env[:vm].driver.delete_unused_host_only_networks - - # Continue along - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/discard_state.rb b/lib/vagrant/action/vm/discard_state.rb deleted file mode 100644 index 408b4580f..000000000 --- a/lib/vagrant/action/vm/discard_state.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Vagrant - module Action - module VM - # Discards the saved state of the VM if its saved. If its - # not saved, does nothing. - class DiscardState - def initialize(app, env) - @app = app - end - - def call(env) - if env[:vm].state == :saved - env[:ui].info I18n.t("vagrant.actions.vm.discard_state.discarding") - env[:vm].driver.discard_saved_state - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/vm/provisioner_cleanup.rb b/lib/vagrant/action/vm/provisioner_cleanup.rb deleted file mode 100644 index 1c5b2f0e6..000000000 --- a/lib/vagrant/action/vm/provisioner_cleanup.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Vagrant - module Action - module VM - class ProvisionerCleanup - def initialize(app, env) - @app = app - @env = env - end - - def call(env) - enabled_provisioners.each do |instance| - instance.cleanup - end - - @app.call(env) - end - - def enabled_provisioners - @env[:vm].config.vm.provisioners.map do |provisioner| - provisioner.provisioner.new(@env, provisioner.config) - end - end - end - end - end -end diff --git a/lib/vagrant/communication.rb b/lib/vagrant/communication.rb deleted file mode 100644 index cb9795b71..000000000 --- a/lib/vagrant/communication.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Vagrant - module Communication - autoload :Base, 'vagrant/communication/base' - - autoload :SSH, 'vagrant/communication/ssh' - end -end diff --git a/lib/vagrant/communication/base.rb b/lib/vagrant/communication/base.rb deleted file mode 100644 index b16d2626f..000000000 --- a/lib/vagrant/communication/base.rb +++ /dev/null @@ -1,63 +0,0 @@ -module Vagrant - module Communication - # The base class for any classes that provide an API for communicating - # with the virtual machine. - # - # There are various stages that require Vagrant to copy files or - # run commands on the target system, and communication classes provide - # the abstraction necessary to perform these tasks, via SSH or some - # other mechanism. - # - # Any subclasses of this class **must** implement all of the methods - # below. - class Base - # Checks if the target machine is ready for communication. - # - # @return [Boolean] - def ready? - end - - # Download a file from the virtual machine to the local machine. - # - # @param [String] from Path of the file on the virtual machine. - # @param [String] to Path to where to save the remote file. - def download(from, to) - end - - # Upload a file to the virtual machine. - # - # @param [String] from Path to a file to upload. - # @param [String] to Path to where to save this file. - def upload(from, to) - end - - # Execute a command on the remote machine. - # - # @param [String] command Command to execute. - # @yield [type, data] Realtime output of the command being executed. - # @yieldparam [String] type Type of the output, `:stdout`, `:stderr`, etc. - # @yieldparam [String] data Data for the given output. - # @return [Integer] Exit code of the command. - def execute(command, opts=nil) - end - - # Execute a comand with super user privileges. - # - # See #execute for parameter information. - def sudo(command, opts=nil) - end - - # Executes a command and returns a boolean statement if it was successful - # or not. - # - # This is implemented by default as expecting `execute` to return 0. - def test(command, opts=nil) - # Disable error checking no matter what - opts = (opts || {}).merge(:error_check => false) - - # Successful if the exit status is 0 - execute(command, opts) == 0 - end - end - end -end diff --git a/lib/vagrant/downloaders/base.rb b/lib/vagrant/downloaders/base.rb index 12fe8903d..5cc3cb980 100644 --- a/lib/vagrant/downloaders/base.rb +++ b/lib/vagrant/downloaders/base.rb @@ -14,9 +14,6 @@ module Vagrant # handle. def self.match?(url); false; end - # Called prior to execution so any error checks can be done - def prepare(source_url); end - # Downloads the source file to the destination file. It is up to # implementors of this class to handle the logic. def download!(source_url, destination_file); end diff --git a/lib/vagrant/downloaders/file.rb b/lib/vagrant/downloaders/file.rb index bbd87ce4d..63ca0ba56 100644 --- a/lib/vagrant/downloaders/file.rb +++ b/lib/vagrant/downloaders/file.rb @@ -9,11 +9,9 @@ module Vagrant ::File.file?(::File.expand_path(uri)) end - def prepare(source_url) - raise Errors::DownloaderFileDoesntExist if !::File.file?(::File.expand_path(source_url)) - end - def download!(source_url, destination_file) + raise Errors::DownloaderFileDoesntExist if !::File.file?(::File.expand_path(source_url)) + @ui.info I18n.t("vagrant.downloaders.file.download") FileUtils.cp(::File.expand_path(source_url), destination_file.path) end diff --git a/lib/vagrant/driver.rb b/lib/vagrant/driver.rb deleted file mode 100644 index 9f114f80e..000000000 --- a/lib/vagrant/driver.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Vagrant - module Driver - autoload :VirtualBox, 'vagrant/driver/virtualbox' - autoload :VirtualBox_4_0, 'vagrant/driver/virtualbox_4_0' - autoload :VirtualBox_4_1, 'vagrant/driver/virtualbox_4_1' - end -end diff --git a/lib/vagrant/driver/virtualbox.rb b/lib/vagrant/driver/virtualbox.rb deleted file mode 100644 index baf462b87..000000000 --- a/lib/vagrant/driver/virtualbox.rb +++ /dev/null @@ -1,140 +0,0 @@ -require 'forwardable' - -require 'log4r' - -require 'vagrant/driver/virtualbox_base' - -module Vagrant - module Driver - # This class contains the logic to drive VirtualBox. - # - # Read the VirtualBoxBase source for documentation on each method. - class VirtualBox < VirtualBoxBase - # This is raised if the VM is not found when initializing a driver - # with a UUID. - class VMNotFound < StandardError; end - - # We use forwardable to do all our driver forwarding - extend Forwardable - - # The UUID of the virtual machine we represent - attr_reader :uuid - - # The version of virtualbox that is running. - attr_reader :version - - def initialize(uuid=nil) - # Setup the base - super() - - @logger = Log4r::Logger.new("vagrant::driver::virtualbox") - @uuid = uuid - - # Read and assign the version of VirtualBox we know which - # specific driver to instantiate. - begin - @version = read_version || "" - rescue Subprocess::LaunchError - # This means that VirtualBox was not found, so we raise this - # error here. - raise Errors::VirtualBoxNotDetected - end - - # Instantiate the proper version driver for VirtualBox - @logger.debug("Finding driver for VirtualBox version: #{@version}") - driver_map = { - "4.0" => VirtualBox_4_0, - "4.1" => VirtualBox_4_1 - } - - driver_klass = nil - driver_map.each do |key, klass| - if @version.start_with?(key) - driver_klass = klass - break - end - end - - if !driver_klass - supported_versions = driver_map.keys.sort.join(", ") - raise Errors::VirtualBoxInvalidVersion, :supported_versions => supported_versions - end - - @logger.info("Using VirtualBox driver: #{driver_klass}") - @driver = driver_klass.new(@uuid) - - if @uuid - # Verify the VM exists, and if it doesn't, then don't worry - # about it (mark the UUID as nil) - raise VMNotFound if !@driver.vm_exists?(@uuid) - end - end - - def_delegators :@driver, :clear_forwarded_ports, - :clear_shared_folders, - :create_dhcp_server, - :create_host_only_network, - :delete, - :delete_unused_host_only_networks, - :discard_saved_state, - :enable_adapters, - :execute_command, - :export, - :forward_ports, - :halt, - :import, - :read_forwarded_ports, - :read_bridged_interfaces, - :read_guest_additions_version, - :read_host_only_interfaces, - :read_mac_address, - :read_mac_addresses, - :read_machine_folder, - :read_network_interfaces, - :read_state, - :read_used_ports, - :read_vms, - :set_mac_address, - :set_name, - :share_folders, - :ssh_port, - :start, - :suspend, - :verify!, - :verify_image, - :vm_exists? - - protected - - # This returns the version of VirtualBox that is running. - # - # @return [String] - def read_version - # The version string is usually in one of the following formats: - # - # * 4.1.8r1234 - # * 4.1.8r1234_OSE - # * 4.1.8_MacPortsr1234 - # - # Below accounts for all of these. - - # Note: We split this into multiple lines because apparently "".split("_") - # is [], so we have to check for an empty array in between. - output = execute("--version") - if output =~ /vboxdrv kernel module is not loaded/ - raise Errors::VirtualBoxKernelModuleNotLoaded - elsif output =~ /Please install/ - # Check for installation incomplete warnings, for example: - # "WARNING: The character device /dev/vboxdrv does not - # exist. Please install the virtualbox-ose-dkms package and - # the appropriate headers, most likely linux-headers-generic." - raise Errors::VirtualBoxInstallIncomplete - end - - parts = output.split("_") - return nil if parts.empty? - parts[0].split("r")[0] - end - end - end -end diff --git a/lib/vagrant/driver/virtualbox_4_0.rb b/lib/vagrant/driver/virtualbox_4_0.rb deleted file mode 100644 index 23546e2f0..000000000 --- a/lib/vagrant/driver/virtualbox_4_0.rb +++ /dev/null @@ -1,474 +0,0 @@ -require 'log4r' - -require 'vagrant/driver/virtualbox_base' - -module Vagrant - module Driver - # Driver for VirtualBox 4.0.x - class VirtualBox_4_0 < VirtualBoxBase - def initialize(uuid) - super() - - @logger = Log4r::Logger.new("vagrant::driver::virtualbox_4_0") - @uuid = uuid - end - - def clear_forwarded_ports - args = [] - read_forwarded_ports(@uuid).each do |nic, name, _, _| - args.concat(["--natpf#{nic}", "delete", name]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def clear_shared_folders - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if name = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] - execute("sharedfolder", "remove", @uuid, "--name", name) - end - end - end - - def create_dhcp_server(network, options) - execute("dhcpserver", "add", "--ifname", network, - "--ip", options[:dhcp_ip], - "--netmask", options[:netmask], - "--lowerip", options[:dhcp_lower], - "--upperip", options[:dhcp_upper], - "--enable") - end - - def create_host_only_network(options) - # Create the interface - interface = execute("hostonlyif", "create") - name = interface[/^Interface '(.+?)' was successfully created$/, 1] - - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) - - # Return the details - return { - :name => name, - :ip => options[:adapter_ip], - :netmask => options[:netmask], - :dhcp => nil - } - end - - def delete - execute("unregistervm", @uuid, "--delete") - end - - def delete_unused_host_only_networks - networks = [] - execute("list", "hostonlyifs").split("\n").each do |line| - if network_name = line[/^Name:\s+(.+?)$/, 1] - networks << network_name - end - end - - execute("list", "vms").split("\n").each do |line| - if vm_name = line[/^".+?"\s+\{(.+?)\}$/, 1] - info = execute("showvminfo", vm_name, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] - networks.delete(network_name) - end - end - end - end - - networks.each do |name| - # First try to remove any DHCP servers attached. We use `raw` because - # it is okay if this fails. It usually means that a DHCP server was - # never attached. - raw("dhcpserver", "remove", "--ifname", name) - - # Delete the actual host only network interface. - execute("hostonlyif", "remove", name) - end - end - - def discard_saved_state - execute("discardstate", @uuid) - end - - def enable_adapters(adapters) - args = [] - adapters.each do |adapter| - args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) - - if adapter[:bridge] - args.concat(["--bridgeadapter#{adapter[:adapter]}", - adapter[:bridge]]) - end - - if adapter[:hostonly] - args.concat(["--hostonlyadapter#{adapter[:adapter]}", - adapter[:hostonly]]) - end - - if adapter[:mac_address] - args.concat(["--macaddress#{adapter[:adapter]}", - adapter[:mac_address]]) - end - - if adapter[:nic_type] - args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) - end - end - - execute("modifyvm", @uuid, *args) - end - - def execute_command(command) - raw(*command) - end - - def export(path) - # TODO: Progress - execute("export", @uuid, "--output", path.to_s) - end - - def forward_ports(ports) - args = [] - ports.each do |options| - pf_builder = [options[:name], - options[:protocol] || "tcp", - "", - options[:hostport], - "", - options[:guestport]] - - args.concat(["--natpf#{options[:adapter] || 1}", - pf_builder.join(",")]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def halt - execute("controlvm", @uuid, "poweroff") - end - - def import(ovf) - output = "" - total = "" - last = 0 - execute("import", ovf) do |type, data| - if type == :stdout - # Keep track of the stdout so that we can get the VM name - output << data - elsif type == :stderr - # Append the data so we can see the full view - total << data - - # Break up the lines. We can't get the progress until we see an "OK" - lines = total.split("\n") - if lines.include?("OK.") - # The progress of the import will be in the last line. Do a greedy - # regular expression to find what we're looking for. - if current = lines.last[/.+(\d{2})%/, 1] - current = current.to_i - if current > last - last = current - yield current if block_given? - end - end - end - end - end - - # Find the name of the VM name - name = output[/Suggested VM name "(.+?)"/, 1] - if !name - @logger.error("Couldn't find VM name in the output.") - return nil - end - - output = execute("list", "vms") - if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] - return existing_vm - end - - nil - end - - def read_forwarded_ports(uuid=nil, active_only=false) - uuid ||= @uuid - - @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") - - results = [] - current_nic = nil - info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - # This is how we find the nic that a FP is attached to, - # since this comes first. - if nic = line[/^nic(\d+)=".+?"$/, 1] - current_nic = nic.to_i - end - - # If we care about active VMs only, then we check the state - # to verify the VM is running. - if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") - return [] - end - - # Parse out the forwarded port information - if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) - result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] - @logger.debug(" - #{result.inspect}") - results << result - end - end - - results - end - - def read_bridged_interfaces - execute("list", "bridgedifs").split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Return the info to build up the results - info - end - end - - def read_guest_additions_version - output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", - :retryable => true) - if value = output[/^Value: (.+?)$/, 1] - # Split the version by _ since some distro versions modify it - # to look like this: 4.1.2_ubuntu, and the distro part isn't - # too important. - return value.split("_").first - end - - return nil - end - - def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] - info[:network] = network - elsif ip = line[/^IP:\s+(.+?)$/, 1] - info[:ip] = ip - elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] - info[:lower] = lower - elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] - info[:upper] = upper - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - - execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - - info - end - end - - def read_mac_address - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if mac = line[/^macaddress1="(.+?)"$/, 1] - return mac - end - end - - nil - end - - def read_mac_addresses - macs = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - mac = matcher[2].to_s - macs[adapter] = mac - end - end - macs - end - - def read_machine_folder - execute("list", "systemproperties", :retryable => true).split("\n").each do |line| - if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] - return folder - end - end - - nil - end - - def read_network_interfaces - nics = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^nic(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - type = matcher[2].to_sym - - nics[adapter] ||= {} - nics[adapter][:type] = type - elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:hostonly] = network - elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:bridge] = network - end - end - - nics - end - - def read_state - output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - if output =~ /^name=""$/ - return :inaccessible - elsif state = output[/^VMState="(.+?)"$/, 1] - return state.to_sym - end - - nil - end - - def read_used_ports - ports = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if uuid = line[/^".+?" \{(.+?)\}$/, 1] - # Ignore our own used ports - next if uuid == @uuid - - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport - end - end - end - - ports - end - - def read_vms - results = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if vm = line[/^".+?" \{(.+?)\}$/, 1] - results << vm - end - end - - results - end - - def set_mac_address(mac) - execute("modifyvm", @uuid, "--macaddress1", mac) - end - - def set_name(name) - execute("modifyvm", @uuid, "--name", name) - end - - def share_folders(folders) - folders.each do |folder| - args = ["--name", - folder[:name], - "--hostpath", - folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - execute("sharedfolder", "add", @uuid, *args) - end - end - - def ssh_port(expected_port) - @logger.debug("Searching for SSH port: #{expected_port.inspect}") - - # Look for the forwarded port only by comparing the guest port - read_forwarded_ports.each do |_, _, hostport, guestport| - return hostport if guestport == expected_port - end - - nil - end - - def start(mode) - command = ["startvm", @uuid, "--type", mode.to_s] - r = raw(*command) - - if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ - # Some systems return an exit code 1 for some reason. For that - # we depend on the output. - return true - end - - # If we reached this point then it didn't work out. - raise Errors::VBoxManageError, :command => command.inspect - end - - def suspend - execute("controlvm", @uuid, "savestate") - end - - def verify! - # This command sometimes fails if kernel drivers aren't properly loaded - # so we just run the command and verify that it succeeded. - execute("list", "hostonlyifs") - end - - def verify_image(path) - r = raw("import", path.to_s, "--dry-run") - return r.exit_code == 0 - end - - def vm_exists?(uuid) - raw("showvminfo", uuid).exit_code == 0 - end - end - end -end diff --git a/lib/vagrant/driver/virtualbox_4_1.rb b/lib/vagrant/driver/virtualbox_4_1.rb deleted file mode 100644 index 3983a1112..000000000 --- a/lib/vagrant/driver/virtualbox_4_1.rb +++ /dev/null @@ -1,474 +0,0 @@ -require 'log4r' - -require 'vagrant/driver/virtualbox_base' - -module Vagrant - module Driver - # Driver for VirtualBox 4.1.x - class VirtualBox_4_1 < VirtualBoxBase - def initialize(uuid) - super() - - @logger = Log4r::Logger.new("vagrant::driver::virtualbox_4_1") - @uuid = uuid - end - - def clear_forwarded_ports - args = [] - read_forwarded_ports(@uuid).each do |nic, name, _, _| - args.concat(["--natpf#{nic}", "delete", name]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def clear_shared_folders - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if folder = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] - execute("sharedfolder", "remove", @uuid, "--name", folder) - end - end - end - - def create_dhcp_server(network, options) - execute("dhcpserver", "add", "--ifname", network, - "--ip", options[:dhcp_ip], - "--netmask", options[:netmask], - "--lowerip", options[:dhcp_lower], - "--upperip", options[:dhcp_upper], - "--enable") - end - - def create_host_only_network(options) - # Create the interface - interface = execute("hostonlyif", "create") - name = interface[/^Interface '(.+?)' was successfully created$/, 1] - - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) - - # Return the details - return { - :name => name, - :ip => options[:adapter_ip], - :netmask => options[:netmask], - :dhcp => nil - } - end - - def delete - execute("unregistervm", @uuid, "--delete") - end - - def delete_unused_host_only_networks - networks = [] - execute("list", "hostonlyifs").split("\n").each do |line| - if network = line[/^Name:\s+(.+?)$/, 1] - networks << network - end - end - - execute("list", "vms").split("\n").each do |line| - if vm = line[/^".+?"\s+\{(.+?)\}$/, 1] - info = execute("showvminfo", vm, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] - networks.delete(adapter) - end - end - end - end - - networks.each do |name| - # First try to remove any DHCP servers attached. We use `raw` because - # it is okay if this fails. It usually means that a DHCP server was - # never attached. - raw("dhcpserver", "remove", "--ifname", name) - - # Delete the actual host only network interface. - execute("hostonlyif", "remove", name) - end - end - - def discard_saved_state - execute("discardstate", @uuid) - end - - def enable_adapters(adapters) - args = [] - adapters.each do |adapter| - args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) - - if adapter[:bridge] - args.concat(["--bridgeadapter#{adapter[:adapter]}", - adapter[:bridge]]) - end - - if adapter[:hostonly] - args.concat(["--hostonlyadapter#{adapter[:adapter]}", - adapter[:hostonly]]) - end - - if adapter[:mac_address] - args.concat(["--macaddress#{adapter[:adapter]}", - adapter[:mac_address]]) - end - - if adapter[:nic_type] - args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) - end - end - - execute("modifyvm", @uuid, *args) - end - - def execute_command(command) - raw(*command) - end - - def export(path) - # TODO: Progress - execute("export", @uuid, "--output", path.to_s) - end - - def forward_ports(ports) - args = [] - ports.each do |options| - pf_builder = [options[:name], - options[:protocol] || "tcp", - "", - options[:hostport], - "", - options[:guestport]] - - args.concat(["--natpf#{options[:adapter] || 1}", - pf_builder.join(",")]) - end - - execute("modifyvm", @uuid, *args) if !args.empty? - end - - def halt - execute("controlvm", @uuid, "poweroff") - end - - def import(ovf) - output = "" - total = "" - last = 0 - execute("import", ovf) do |type, data| - if type == :stdout - # Keep track of the stdout so that we can get the VM name - output << data - elsif type == :stderr - # Append the data so we can see the full view - total << data - - # Break up the lines. We can't get the progress until we see an "OK" - lines = total.split("\n") - if lines.include?("OK.") - # The progress of the import will be in the last line. Do a greedy - # regular expression to find what we're looking for. - if current = lines.last[/.+(\d{2})%/, 1] - current = current.to_i - if current > last - last = current - yield current if block_given? - end - end - end - end - end - - # Find the name of the VM name - name = output[/Suggested VM name "(.+?)"/, 1] - if !name - @logger.error("Couldn't find VM name in the output.") - return nil - end - - output = execute("list", "vms") - if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] - return existing_vm - end - - nil - end - - def read_forwarded_ports(uuid=nil, active_only=false) - uuid ||= @uuid - - @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") - - results = [] - current_nic = nil - info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - # This is how we find the nic that a FP is attached to, - # since this comes first. - if nic = line[/^nic(\d+)=".+?"$/, 1] - current_nic = nic.to_i - end - - # If we care about active VMs only, then we check the state - # to verify the VM is running. - if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") - return [] - end - - # Parse out the forwarded port information - if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) - result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] - @logger.debug(" - #{result.inspect}") - results << result - end - end - - results - end - - def read_bridged_interfaces - execute("list", "bridgedifs").split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Return the info to build up the results - info - end - end - - def read_guest_additions_version - output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", - :retryable => true) - if value = output[/^Value: (.+?)$/, 1] - # Split the version by _ since some distro versions modify it - # to look like this: 4.1.2_ubuntu, and the distro part isn't - # too important. - return value.split("_").first - end - - return nil - end - - def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] - info[:network] = network - elsif ip = line[/^IP:\s+(.+?)$/, 1] - info[:ip] = ip - elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] - info[:lower] = lower - elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] - info[:upper] = upper - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - - execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| - info = {} - - block.split("\n").each do |line| - if name = line[/^Name:\s+(.+?)$/, 1] - info[:name] = name - elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] - info[:ip] = ip - elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] - info[:netmask] = netmask - elsif status = line[/^Status:\s+(.+?)$/, 1] - info[:status] = status - end - end - - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - - info - end - end - - def read_mac_address - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if mac = line[/^macaddress1="(.+?)"$/, 1] - return mac - end - end - - nil - end - - def read_mac_addresses - macs = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - mac = matcher[2].to_s - macs[adapter] = mac - end - end - macs - end - - def read_machine_folder - execute("list", "systemproperties", :retryable => true).split("\n").each do |line| - if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] - return folder - end - end - - nil - end - - def read_network_interfaces - nics = {} - info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if matcher = /^nic(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - type = matcher[2].to_sym - - nics[adapter] ||= {} - nics[adapter][:type] = type - elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:hostonly] = network - elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) - adapter = matcher[1].to_i - network = matcher[2].to_s - - nics[adapter] ||= {} - nics[adapter][:bridge] = network - end - end - - nics - end - - def read_state - output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) - if output =~ /^name=""$/ - return :inaccessible - elsif state = output[/^VMState="(.+?)"$/, 1] - return state.to_sym - end - - nil - end - - def read_used_ports - ports = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if uuid = line[/^".+?" \{(.+?)\}$/, 1] - # Ignore our own used ports - next if uuid == @uuid - - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport - end - end - end - - ports - end - - def read_vms - results = [] - execute("list", "vms", :retryable => true).split("\n").each do |line| - if vm = line[/^".+?" \{(.+?)\}$/, 1] - results << vm - end - end - - results - end - - def set_mac_address(mac) - execute("modifyvm", @uuid, "--macaddress1", mac) - end - - def set_name(name) - execute("modifyvm", @uuid, "--name", name) - end - - def share_folders(folders) - folders.each do |folder| - args = ["--name", - folder[:name], - "--hostpath", - folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - execute("sharedfolder", "add", @uuid, *args) - end - end - - def ssh_port(expected_port) - @logger.debug("Searching for SSH port: #{expected_port.inspect}") - - # Look for the forwarded port only by comparing the guest port - read_forwarded_ports.each do |_, _, hostport, guestport| - return hostport if guestport == expected_port - end - - nil - end - - def start(mode) - command = ["startvm", @uuid, "--type", mode.to_s] - r = raw(*command) - - if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ - # Some systems return an exit code 1 for some reason. For that - # we depend on the output. - return true - end - - # If we reached this point then it didn't work out. - raise Errors::VBoxManageError, :command => command.inspect - end - - def suspend - execute("controlvm", @uuid, "savestate") - end - - def verify! - # This command sometimes fails if kernel drivers aren't properly loaded - # so we just run the command and verify that it succeeded. - execute("list", "hostonlyifs") - end - - def verify_image(path) - r = raw("import", path.to_s, "--dry-run") - return r.exit_code == 0 - end - - def vm_exists?(uuid) - raw("showvminfo", uuid).exit_code == 0 - end - end - end -end diff --git a/lib/vagrant/driver/virtualbox_base.rb b/lib/vagrant/driver/virtualbox_base.rb deleted file mode 100644 index 9e7cd1868..000000000 --- a/lib/vagrant/driver/virtualbox_base.rb +++ /dev/null @@ -1,326 +0,0 @@ -require 'log4r' - -require 'vagrant/util/busy' -require 'vagrant/util/platform' -require 'vagrant/util/retryable' -require 'vagrant/util/subprocess' - -module Vagrant - module Driver - # Base class for all VirtualBox drivers. - # - # This class provides useful tools for things such as executing - # VBoxManage and handling SIGINTs and so on. - class VirtualBoxBase - # Include this so we can use `Subprocess` more easily. - include Vagrant::Util - include Vagrant::Util::Retryable - - def initialize - @logger = Log4r::Logger.new("vagrant::driver::virtualbox_base") - - # This flag is used to keep track of interrupted state (SIGINT) - @interrupted = false - - # Set the path to VBoxManage - @vboxmanage_path = "VBoxManage" - - if Util::Platform.windows? - @logger.debug("Windows. Trying VBOX_INSTALL_PATH for VBoxManage") - - # On Windows, we use the VBOX_INSTALL_PATH environmental - # variable to find VBoxManage. - if ENV.has_key?("VBOX_INSTALL_PATH") - # Get the path. - path = ENV["VBOX_INSTALL_PATH"] - @logger.debug("VBOX_INSTALL_PATH value: #{path}") - - # There can actually be multiple paths in here, so we need to - # split by the separator ";" and see which is a good one. - path.split(";").each do |single| - # Make sure it ends with a \ - single += "\\" if !single.end_with?("\\") - - # If the executable exists, then set it as the main path - # and break out - vboxmanage = "#{path}VBoxManage.exe" - if File.file?(vboxmanage) - @vboxmanage_path = vboxmanage - break - end - end - end - end - - @logger.info("VBoxManage path: #{@vboxmanage_path}") - end - - # Clears the forwarded ports that have been set on the virtual machine. - def clear_forwarded_ports - end - - # Clears the shared folders that have been set on the virtual machine. - def clear_shared_folders - end - - # Creates a DHCP server for a host only network. - # - # @param [String] network Name of the host-only network. - # @param [Hash] options Options for the DHCP server. - def create_dhcp_server(network, options) - end - - # Creates a host only network with the given options. - # - # @param [Hash] options Options to create the host only network. - # @return [Hash] The details of the host only network, including - # keys `:name`, `:ip`, and `:netmask` - def create_host_only_network(options) - end - - # Deletes the virtual machine references by this driver. - def delete - end - - # Deletes any host only networks that aren't being used for anything. - def delete_unused_host_only_networks - end - - # Discards any saved state associated with this VM. - def discard_saved_state - end - - # Enables network adapters on the VM. - # - # The format of each adapter specification should be like so: - # - # { - # :type => :hostonly, - # :hostonly => "vboxnet0", - # :mac_address => "tubes" - # } - # - # This must support setting up both host only and bridged networks. - # - # @param [Array] adapters Array of adapters to enable. - def enable_adapters(adapters) - end - - # Execute a raw command straight through to VBoxManage. - # - # @param [Array] command Command to execute. - def execute_command(command) - end - - # Exports the virtual machine to the given path. - # - # @param [String] path Path to the OVF file. - # @yield [progress] Yields the block with the progress of the export. - def export(path) - end - - # Forwards a set of ports for a VM. - # - # This will not affect any previously set forwarded ports, - # so be sure to delete those if you need to. - # - # The format of each port hash should be the following: - # - # { - # :name => "foo", - # :hostport => 8500, - # :guestport => 80, - # :adapter => 1, - # :protocol => "tcp" - # } - # - # Note that "adapter" and "protocol" are optional and will default - # to 1 and "tcp" respectively. - # - # @param [Array] ports An array of ports to set. See documentation - # for more information on the format. - def forward_ports(ports) - end - - # Halts the virtual machine (pulls the plug). - def halt - end - - # Imports the VM from an OVF file. - # - # @param [String] ovf Path to the OVF file. - # @return [String] UUID of the imported VM. - def import(ovf) - end - - # Returns a list of forwarded ports for a VM. - # - # @param [String] uuid UUID of the VM to read from, or `nil` if this - # VM. - # @param [Boolean] active_only If true, only VMs that are running will - # be checked. - # @return [Array] - def read_forwarded_ports(uuid=nil, active_only=false) - end - - # Returns a list of bridged interfaces. - # - # @return [Hash] - def read_bridged_interfaces - end - - # Returns the guest additions version that is installed on this VM. - # - # @return [String] - def read_guest_additions_version - end - - # Returns a list of available host only interfaces. - # - # @return [Hash] - def read_host_only_interfaces - end - - # Returns the MAC address of the first network interface. - # - # @return [String] - def read_mac_address - end - - # Returns the folder where VirtualBox places it's VMs. - # - # @return [String] - def read_machine_folder - end - - # Returns a list of network interfaces of the VM. - # - # @return [Hash] - def read_network_interfaces - end - - # Returns the current state of this VM. - # - # @return [Symbol] - def read_state - end - - # Returns a list of all forwarded ports in use by active - # virtual machines. - # - # @return [Array] - def read_used_ports - end - - # Returns a list of all UUIDs of virtual machines currently - # known by VirtualBox. - # - # @return [Array] - def read_vms - end - - # Sets the MAC address of the first network adapter. - # - # @param [String] mac MAC address without any spaces/hyphens. - def set_mac_address(mac) - end - - # Share a set of folders on this VM. - # - # @param [Array] folders - def share_folders(folders) - end - - # Reads the SSH port of this VM. - # - # @param [Integer] expected Expected guest port of SSH. - def ssh_port(expected) - end - - # Starts the virtual machine. - # - # @param [String] mode Mode to boot the VM. Either "headless" - # or "gui" - def start(mode) - end - - # Suspend the virtual machine. - def suspend - end - - # Verifies that the driver is ready to accept work. - # - # This should raise a VagrantError if things are not ready. - def verify! - end - - # Verifies that an image can be imported properly. - # - # @param [String] path Path to an OVF file. - # @return [Boolean] - def verify_image(path) - end - - # Checks if a VM with the given UUID exists. - # - # @return [Boolean] - def vm_exists?(uuid) - end - - # Execute the given subcommand for VBoxManage and return the output. - def execute(*command, &block) - # Get the options hash if it exists - opts = {} - opts = command.pop if command.last.is_a?(Hash) - - tries = 0 - tries = 3 if opts[:retryable] - - # Variable to store our execution result - r = nil - - retryable(:on => Errors::VBoxManageError, :tries => tries, :sleep => 1) do - # Execute the command - r = raw(*command, &block) - - # If the command was a failure, then raise an exception that is - # nicely handled by Vagrant. - if r.exit_code != 0 - if @interrupted - @logger.info("Exit code != 0, but interrupted. Ignoring.") - else - raise Errors::VBoxManageError, :command => command.inspect - end - else - # Sometimes, VBoxManage fails but doesn't actual return a non-zero - # exit code. For this we inspect the output and determine if an error - # occurred. - if r.stderr =~ /VBoxManage: error:/ - @logger.info("VBoxManage error text found, assuming error.") - raise Errors::VBoxManageError, :command => command.inspect - end - end - end - - # Return the output, making sure to replace any Windows-style - # newlines with Unix-style. - r.stdout.gsub("\r\n", "\n") - end - - # Executes a command and returns the raw result object. - def raw(*command, &block) - int_callback = lambda do - @interrupted = true - @logger.info("Interrupted.") - end - - # Append in the options for subprocess - command << { :notify => [:stdout, :stderr] } - - Util::Busy.busy(int_callback) do - Subprocess.execute(@vboxmanage_path, *command, &block) - end - end - end - end -end diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 90137cd2f..7be9e476e 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -104,6 +104,14 @@ module Vagrant activate_plugins end + # Return a human-friendly string for pretty printed or inspected + # instances. + # + # @return [String] + def inspect + "#<#{self.class}: #{@cwd}>" + end + #--------------------------------------------------------------- # Helpers #--------------------------------------------------------------- @@ -114,7 +122,7 @@ module Vagrant # @return [Pathname] def dotfile_path return nil if !root_path - root_path.join(File.expand_path(config.global.vagrant.dotfile_name)) + root_path.join(config.global.vagrant.dotfile_name) end # Returns the collection of boxes for the environment. @@ -205,7 +213,7 @@ module Vagrant # # @return [Action::Runner] def action_runner - @action_runner ||= Action::Runner.new(action_registry) do + @action_runner ||= Action::Runner.new do { :action_runner => action_runner, :box_collection => boxes, @@ -218,16 +226,6 @@ module Vagrant end end - # Action registry for registering new actions with this environment. - # - # @return [Registry] - def action_registry - # For now we return the global built-in actions registry. In the future - # we may want to create an isolated registry that inherits from this - # global one, but for now there isn't a use case that calls for it. - Vagrant.actions - end - # Loads on initial access and reads data from the global data store. # The global data store is global to Vagrant everywhere (in every environment), # so it can be used to store system-wide information. Note that "system-wide" @@ -445,11 +443,22 @@ module Vagrant # Loads the persisted VM (if it exists) for this environment. def load_vms! - result = {} + # This is hardcoded for now. + provider = nil + Vagrant.plugin("1").registered.each do |plugin| + provider = plugin.provider.get(:virtualbox) + break if provider + end + + raise "VirtualBox provider not found." if !provider # Load all the virtual machine instances. + result = {} config.vms.each do |name| - result[name] = Vagrant::VM.new(name, self, config.for_vm(name)) + vm_config = config.for_vm(name) + box = boxes.find(vm_config.vm.box, :virtualbox) + + result[name] = Vagrant::Machine.new(name, provider, vm_config, box, self) end result diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 0b57d7c45..69d6c5163 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -226,6 +226,10 @@ module Vagrant error_key(:port_collision_resume) end + class MachineGuestNotReady < VagrantError + error_key(:machine_guest_not_ready) + end + class MultiVMEnvironmentRequired < VagrantError status_code(5) error_key(:multi_vm_required) @@ -356,6 +360,10 @@ module Vagrant error_key(:ssh_key_type_not_supported) end + class SSHNotReady < VagrantError + error_key(:ssh_not_ready) + end + class SSHPortNotDetected < VagrantError status_code(50) error_key(:ssh_port_not_detected) @@ -376,6 +384,10 @@ module Vagrant error_key(:ui_expects_tty) end + class UnimplementedProviderAction < VagrantError + error_key(:unimplemented_provider_action) + end + class VagrantInterrupt < VagrantError status_code(40) error_key(:interrupted) diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb new file mode 100644 index 000000000..6dd8c95b7 --- /dev/null +++ b/lib/vagrant/machine.rb @@ -0,0 +1,296 @@ +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_reader :box + + # Configuration for the machine. + # + # @return [Object] + attr_reader :config + + # 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 [String] + attr_reader :name + + # The provider backing this machine. + # + # @return [Object] + attr_reader :provider + + # 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] config The configuration for this machine. + # @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_cls, config, box, env, base=false) + @logger = Log4r::Logger.new("vagrant::machine") + @logger.info("Initializing machine: #{name}") + @logger.info(" - Provider: #{provider_cls}") + @logger.info(" - Box: #{box}") + + @name = name + @box = box + @config = config + @env = env + + # 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 + @id = @env.local_data[:active][@name.to_s] if @env.local_data[:active] + end + + # Initializes the provider last so that it has access to all the + # state we setup on this machine. + @provider = provider_cls.new(self) + 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.debug("Calling action: #{name} on provider #{@provider}") + + # 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 + + # Run the action with the action runner on the environment + env = { :machine => self }.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 + # For now, we always return SSH. In the future, we'll abstract + # this and allow plugins to define new methods of communication. + Vagrant.plugin("1").registered.each do |plugin| + klass = plugin.communicator[:ssh] + if klass + # This plugin has the SSH communicator, use it. + @communicator = klass.new(self) + break + end + end + 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 [Object] + def guest + raise Errors::MachineGuestNotReady if !communicate.ready? + + # Load the initial guest. + last_guest = config.vm.guest + guest = load_guest(last_guest) + + # Loop and distro dispatch while there are distros. + while true + distro = guest.distro_dispatch + break if !distro + + # This is just some really basic loop detection and avoiding for + # guest classes. This is just here to help implementers a bit + # avoid a situation that is fairly easy, since if you subclass + # a parent which does `distro_dispatch`, you'll end up dispatching + # forever. + if distro == last_guest + @logger.warn("Distro dispatch loop in '#{distro}'. Exiting loop.") + break + end + + last_guest = distro + guest = load_guest(distro) + end + + # Return the result + 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) + @env.local_data[:active] ||= {} + + if value + # Set the value + @env.local_data[:active][@name] = value + else + # Delete it from the active hash + @env.local_data[:active].delete(@name) + end + + # Commit the local data so that the next time Vagrant is initialized, + # it realizes the VM exists (or doesn't). + @env.local_data.commit + + # Store the ID locally + @id = value + + # Notify the provider that the ID changed in case it needs to do + # any accounting from it. + @provider.machine_id_changed + 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 + + # Next, we default some fields if they weren't given to us by + # the provider. + 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 + + # 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 + + # 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. + info[:private_key_path] = @config.ssh.private_key_path if @config.ssh.private_key_path + info[:private_key_path] = @env.default_private_key_path if !info[:private_key_path] + + # 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 [Symbol] + def state + @provider.state + end + + protected + + # Given a guest name (such as `:windows`), this will load the associated + # guest implementation and return an instance. + # + # @param [Symbol] guest The name of the guest implementation. + # @return [Object] + def load_guest(guest) + @logger.info("Loading guest: #{guest}") + + klass = nil + Vagrant.plugin("1").registered.each do |plugin| + if plugin.guest.has_key?(guest) + klass = plugin.guest[guest] + break + end + end + + if klass.nil? + raise Errors::VMGuestError, + :_key => :unknown_type, + :guest => guest.to_s + end + + return klass.new(self) + end + end +end diff --git a/lib/vagrant/plugin/v1.rb b/lib/vagrant/plugin/v1.rb index 6c4c6d339..55b23e5eb 100644 --- a/lib/vagrant/plugin/v1.rb +++ b/lib/vagrant/plugin/v1.rb @@ -6,10 +6,12 @@ module Vagrant module Plugin module V1 autoload :Command, "vagrant/plugin/v1/command" + autoload :Communicator, "vagrant/plugin/v1/communicator" autoload :Config, "vagrant/plugin/v1/config" autoload :Guest, "vagrant/plugin/v1/guest" autoload :Host, "vagrant/plugin/v1/host" autoload :Plugin, "vagrant/plugin/v1/plugin" + autoload :Provider, "vagrant/plugin/v1/provider" autoload :Provisioner, "vagrant/plugin/v1/provisioner" end end diff --git a/lib/vagrant/plugin/v1/communicator.rb b/lib/vagrant/plugin/v1/communicator.rb new file mode 100644 index 000000000..7e4494dc3 --- /dev/null +++ b/lib/vagrant/plugin/v1/communicator.rb @@ -0,0 +1,98 @@ +module Vagrant + module Plugin + module V1 + # Base class for a communicator in Vagrant. A communicator is + # responsible for communicating with a machine in some way. There + # are various stages of Vagrant that require things such as uploading + # files to the machine, executing shell commands, etc. Implementors + # of this class are expected to provide this functionality in some + # way. + # + # Note that a communicator must provide **all** of the methods + # in this base class. There is currently no way for one communicator + # to provide say a more efficient way of uploading a file, but not + # provide shell execution. This sort of thing will come in a future + # version. + class Communicator + # This returns true/false depending on if the given machine + # can be communicated with using this communicator. If this returns + # `true`, then this class will be used as the primary communication + # method for the machine. + # + # @return [Boolean] + def self.match?(machine) + false + end + + # Initializes the communicator with the machine that we will be + # communicating with. This base method does nothing (it doesn't + # even store the machine in an instance variable for you), so you're + # expected to override this and do something with the machine if + # you care about it. + # + # @param [Machine] machine The machine this instance is expected to + # communicate with. + def initialize(machine) + end + + # Checks if the target machine is ready for communication. If this + # returns true, then all the other methods for communicating with + # the machine are expected to be functional. + # + # @return [Boolean] + def ready? + false + end + + # Download a file from the remote machine to the local machine. + # + # @param [String] from Path of the file on the remote machine. + # @param [String] to Path of where to save the file locally. + def download(from, to) + end + + # Upload a file to the remote machine. + # + # @param [String] from Path of the file locally to upload. + # @param [String] to Path of where to save the file on the remote + # machine. + def upload(from, to) + end + + # Execute a command on the remote machine. The exact semantics + # of this method are up to the implementor, but in general the + # users of this class will expect this to be a shell. + # + # This method gives you no way to write data back to the remote + # machine, so only execute commands that don't expect input. + # + # @param [String] command Command to execute. + # @yield [type, data] Realtime output of the command being executed. + # @yieldparam [String] type Type of the output. This can be + # `:stdout`, `:stderr`, etc. The exact types are up to the + # implementor. + # @yieldparam [String] data Data for the given output. + # @return [Integer] Exit code of the command. + def execute(command, opts=nil) + end + + # Executes a command on the remote machine with administrative + # privileges. See {#execute} for documentation, as the API is the + # same. + # + # @see #execute + def sudo(command, opts=nil) + end + + # Executes a command and returns true if the command succeeded, + # and false otherwise. By default, this executes as a normal user, + # and it is up to the communicator implementation if they expose an + # option for running tests as an administrator. + # + # @see #execute + def test(command, opts=nil) + end + end + end + end +end diff --git a/lib/vagrant/plugin/v1/guest.rb b/lib/vagrant/plugin/v1/guest.rb index 86e96c249..a62cbbce6 100644 --- a/lib/vagrant/plugin/v1/guest.rb +++ b/lib/vagrant/plugin/v1/guest.rb @@ -27,9 +27,8 @@ module Vagrant # Vagrant can successfully SSH into the machine) to give the system a chance # to determine the distro and return a distro-specific system. # - # **Warning:** If a return value which subclasses from {Base} is - # returned, Vagrant will use it as the new system instance for the - # class. + # If this method returns nil, then this instance is assumed to be + # the most specific guest implementation. def distro_dispatch end diff --git a/lib/vagrant/plugin/v1/plugin.rb b/lib/vagrant/plugin/v1/plugin.rb index decc7aaae..7d0e57b53 100644 --- a/lib/vagrant/plugin/v1/plugin.rb +++ b/lib/vagrant/plugin/v1/plugin.rb @@ -103,6 +103,24 @@ module Vagrant data[:command] end + # Defines additional communicators to be available. Communicators + # should be returned by a block passed to this method. This is done + # to ensure that the class is lazy loaded, so if your class inherits + # from or uses any Vagrant internals specific to Vagrant 1.0, then + # the plugin can still be defined without breaking anything in future + # versions of Vagrant. + # + # @param [String] name Communicator name. + def self.communicator(name=UNSET_VALUE, &block) + data[:communicator] ||= Registry.new + + # Register a new communicator class only if a name was given. + data[:communicator].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:communicator] + end + # Defines additional configuration keys to be available in the # Vagrantfile. The configuration class should be returned by a # block passed to this method. This is done to ensure that the class @@ -181,6 +199,19 @@ module Vagrant data[:hosts] end + # Registers additional providers to be available. + # + # @param [Symbol] name Name of the provider. + def self.provider(name=UNSET_VALUE, &block) + data[:providers] ||= Registry.new + + # Register a new provider class only if a name was given + data[:providers].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:providers] + end + # Registers additional provisioners to be available. # # @param [String] name Name of the provisioner. diff --git a/lib/vagrant/plugin/v1/provider.rb b/lib/vagrant/plugin/v1/provider.rb new file mode 100644 index 000000000..0a71e134c --- /dev/null +++ b/lib/vagrant/plugin/v1/provider.rb @@ -0,0 +1,68 @@ +module Vagrant + module Plugin + module V1 + # This is the base class for a provider for the V1 API. A provider + # is responsible for creating compute resources to match the needs + # of a Vagrant-configured system. + class Provider + # Initialize the provider to represent the given machine. + # + # @param [Vagrant::Machine] machine The machine that this provider + # is responsible for. + def initialize(machine) + end + + # This should return an action callable for the given name. + # + # @param [Symbol] name Name of the action. + # @return [Object] A callable action sequence object, whether it + # is a proc, object, etc. + def action(name) + nil + end + + # This method is called if the underying machine ID changes. Providers + # can use this method to load in new data for the actual backing + # machine or to realize that the machine is now gone (the ID can + # become `nil`). No parameters are given, since the underlying machine + # is simply the machine instance given to this object. And no + # return value is necessary. + def machine_id_changed + end + + # This should return a hash of information that explains how to + # SSH into the machine. If the machine is not at a point where + # SSH is even possible, then `nil` should be returned. + # + # The general structure of this returned hash should be the + # following: + # + # { + # :host => "1.2.3.4", + # :port => "22", + # :username => "mitchellh", + # :private_key_path => "/path/to/my/key" + # } + # + # **Note:** Vagrant only supports private key based authentication, + # mainly for the reason that there is no easy way to exec into an + # `ssh` prompt with a password, whereas we can pass a private key + # via commandline. + # + # @return [Hash] SSH information. For the structure of this hash + # read the accompanying documentation for this method. + def ssh_info + nil + end + + # This should return the state of the machine within this provider. + # The state can be any symbol. + # + # @return [Symbol] + def state + nil + end + end + end + end +end diff --git a/lib/vagrant/ssh.rb b/lib/vagrant/ssh.rb deleted file mode 100644 index f23b4d0f3..000000000 --- a/lib/vagrant/ssh.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'log4r' - -require 'vagrant/util/file_mode' -require 'vagrant/util/platform' -require 'vagrant/util/safe_exec' - -module Vagrant - # Manages SSH connection information as well as allows opening an - # SSH connection. - class SSH - include Util::SafeExec - - def initialize(vm) - @vm = vm - @logger = Log4r::Logger.new("vagrant::ssh") - end - - # Returns a hash of information necessary for accessing this - # virtual machine via SSH. - # - # @return [Hash] - def info - results = { - :host => @vm.config.ssh.host, - :port => @vm.config.ssh.port || @vm.driver.ssh_port(@vm.config.ssh.guest_port), - :username => @vm.config.ssh.username, - :forward_agent => @vm.config.ssh.forward_agent, - :forward_x11 => @vm.config.ssh.forward_x11 - } - - # This can happen if no port is set and for some reason Vagrant - # can't detect an SSH port. - raise Errors::SSHPortNotDetected if !results[:port] - - # Determine the private key path, which is either set by the - # configuration or uses just the built-in insecure key. - pk_path = @vm.config.ssh.private_key_path || @vm.env.default_private_key_path - results[:private_key_path] = File.expand_path(pk_path, @vm.env.root_path) - - # We need to check and fix the private key permissions - # to make sure that SSH gets a key with 0600 perms. - check_key_permissions(results[:private_key_path]) - - # Return the results - return results - end - - # Connects to the environment's virtual machine, replacing the ruby - # process with an SSH process. - # - # @param [Hash] opts Options hash - # @options opts [Boolean] :plain_mode If True, doesn't authenticate with - # the machine, only connects, allowing the user to connect. - def exec(opts={}) - # Get the SSH information and cache it here - ssh_info = info - - if Util::Platform.windows? - raise Errors::SSHUnavailableWindows, :host => ssh_info[:host], - :port => ssh_info[:port], - :username => ssh_info[:username], - :key_path => ssh_info[:private_key_path] - end - - raise Errors::SSHUnavailable if !Kernel.system("which ssh > /dev/null 2>&1") - - # If plain mode is enabled then we don't do any authentication (we don't - # set a user or an identity file) - plain_mode = opts[:plain_mode] - - options = {} - options[:host] = ssh_info[:host] - options[:port] = ssh_info[:port] - options[:username] = ssh_info[:username] - options[:private_key_path] = ssh_info[:private_key_path] - - # Command line options - command_options = ["-p", options[:port].to_s, "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=FATAL"] - - # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option - # (Also don't use it in plain mode, it'll skip user agents.) - command_options += ["-o", "IdentitiesOnly=yes"] if !(Util::Platform.solaris? || plain_mode) - - command_options += ["-i", options[:private_key_path]] if !plain_mode - command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent] - - # If there are extra options, then we append those - command_options.concat(opts[:extra_args]) if opts[:extra_args] - - if ssh_info[:forward_x11] - # Both are required so that no warnings are shown regarding X11 - command_options += ["-o", "ForwardX11=yes"] - command_options += ["-o", "ForwardX11Trusted=yes"] - end - - host_string = options[:host] - host_string = "#{options[:username]}@#{host_string}" if !plain_mode - command_options << host_string - @logger.info("Invoking SSH: #{command_options.inspect}") - safe_exec("ssh", *command_options) - end - - # Checks the file permissions for a private key, resetting them - # if needed. - def check_key_permissions(key_path) - # Windows systems don't have this issue - return if Util::Platform.windows? - - @logger.debug("Checking key permissions: #{key_path}") - stat = File.stat(key_path) - - if stat.owned? && Util::FileMode.from_octal(stat.mode) != "600" - @logger.info("Attempting to correct key permissions to 0600") - File.chmod(0600, key_path) - - stat = File.stat(key_path) - if Util::FileMode.from_octal(stat.mode) != "600" - raise Errors::SSHKeyBadPermissions, :key_path => key_path - end - end - rescue Errno::EPERM - # This shouldn't happen since we verified we own the file, but - # it is possible in theory, so we raise an error. - raise Errors::SSHKeyBadPermissions, :key_path => key_path - end - end -end diff --git a/lib/vagrant/util/safe_exec.rb b/lib/vagrant/util/safe_exec.rb index ccc214de6..d49a58a56 100644 --- a/lib/vagrant/util/safe_exec.rb +++ b/lib/vagrant/util/safe_exec.rb @@ -6,8 +6,8 @@ module Vagrant # This issue causes `exec` to fail if there is more than one system # thread. In that case, `safe_exec` automatically falls back to # forking. - module SafeExec - def safe_exec(command, *args) + class SafeExec + def self.exec(command, *args) # Create a list of things to rescue from. Since this is OS # specific, we need to do some defined? checks here to make # sure they exist. diff --git a/lib/vagrant/util/ssh.rb b/lib/vagrant/util/ssh.rb new file mode 100644 index 000000000..f7c3a859e --- /dev/null +++ b/lib/vagrant/util/ssh.rb @@ -0,0 +1,119 @@ +require "log4r" + +require "vagrant/util/file_mode" +require "vagrant/util/platform" +require "vagrant/util/safe_exec" + +module Vagrant + module Util + # This is a class that has helpers on it for dealing with SSH. These + # helpers don't depend on any part of Vagrant except what is given + # via the parameters. + class SSH + LOGGER = Log4r::Logger.new("vagrant::util::ssh") + + # Checks that the permissions for a private key are valid, and fixes + # them if possible. SSH requires that permissions on the private key + # are 0600 on POSIX based systems. This will make a best effort to + # fix these permissions if they are not properly set. + # + # @param [Pathname] key_path The path to the private key. + def self.check_key_permissions(key_path) + # Don't do anything if we're on Windows, since Windows doesn't worry + # about key permissions. + return if Platform.windows? + + LOGGER.debug("Checking key permissions: #{key_path}") + stat = key_path.stat + + if stat.owned? && FileMode.from_octal(stat.mode) != "600" + LOGGER.info("Attempting to correct key permissions to 0600") + key_path.chmod(0600) + + # Re-stat the file to get the new mode, and verify it worked + stat = key_path.stat + if FileMode.from_octal(stat.mode) != "600" + raise Errors::SSHKeyBadPermissions, :key_path => key_path + end + end + rescue Errno::EPERM + # This shouldn't happen since we verify we own the file, but + # it is possible in theory, so we raise an error. + raise Errors::SSHKeyBadPermissions, :key_path => key_path + end + + # Halts the running of this process and replaces it with a full-fledged + # SSH shell into a remote machine. + # + # Note: This method NEVER returns. The process ends after this. + # + # @param [Hash] ssh_info This is the SSH information. For the keys + # required please see the documentation of {Machine#ssh_info}. + # @param [Hash] opts These are additional options that are supported + # by exec. + def self.exec(ssh_info, opts={}) + # If we're running Windows, raise an exception since we currently + # still don't support exec-ing into SSH. In the future this should + # certainly be possible if we can detect we're in an environment that + # supports it. + if Platform.windows? + raise Errors::SSHUnavailableWindows, + :host => ssh_info[:host], + :port => ssh_info[:port], + :username => ssh_info[:username], + :key_path => ssh_info[:private_key_path] + end + + # Verify that we have SSH available on the system. + raise Errors::SSHUnavailable if !Kernel.system("which ssh > /dev/null 2>&1") + + # If plain mode is enabled then we don't do any authentication (we don't + # set a user or an identity file) + plain_mode = opts[:plain_mode] + + options = {} + options[:host] = ssh_info[:host] + options[:port] = ssh_info[:port] + options[:username] = ssh_info[:username] + options[:private_key_path] = ssh_info[:private_key_path] + + # Command line options + command_options = [ + "-p", options[:port].to_s, + "-o", "LogLevel=FATAL", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null"] + + # Configurables + command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent] + command_options.concat(opts[:extra_args]) if opts[:extra_args] + + # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the + # IdentitiesOnly option. Also, we don't enable it in plain mode so + # that SSH properly searches our identities and tries to do it itself. + if !Platform.solaris? && !plain_mode + command_options += ["-o", "IdentitiesOnly=yes"] + end + + # If we're not in plain mode, attach the private key path. + command_options += ["-i", options[:private_key_path].to_s] if !plain_mode + + if ssh_info[:forward_x11] + # Both are required so that no warnings are shown regarding X11 + command_options += [ + "-o", "ForwardX11=yes", + "-o", "ForwardX11Trusted=yes"] + end + + # Build up the host string for connecting + host_string = options[:host] + host_string = "#{options[:username]}@#{host_string}" if !plain_mode + command_options << host_string + + # Invoke SSH with all our options + LOGGER.info("Invoking SSH: #{command_options.inspect}") + SafeExec.exec("ssh", *command_options) + end + end + end +end diff --git a/lib/vagrant/vm.rb b/lib/vagrant/vm.rb deleted file mode 100644 index d35c25535..000000000 --- a/lib/vagrant/vm.rb +++ /dev/null @@ -1,205 +0,0 @@ -require 'log4r' - -module Vagrant - class VM - include Vagrant::Util - - attr_reader :uuid - attr_reader :env - attr_reader :name - attr_reader :vm - attr_reader :box - attr_reader :config - attr_reader :driver - - def initialize(name, env, config, opts=nil) - @logger = Log4r::Logger.new("vagrant::vm") - - @name = name - @vm = nil - @env = env - @config = config - @box = nil - @box = env.boxes.find(config.vm.box, :virtualbox) if config.vm.box - - opts ||= {} - if opts[:base] - # The name is the ID we use. - @uuid = name - else - # Load the UUID if its saved. - active = env.local_data[:active] || {} - @uuid = active[@name.to_s] - end - - # Reload ourselves to get the state - reload! - - # Load the associated guest. - load_guest! - @loaded_guest_distro = false - end - - # Loads the guest associated with the VM. The guest class is - # responsible for OS-specific functionality. More information - # can be found by reading the documentation on {Vagrant::Guest::Base}. - # - # **This method should never be called manually.** - def load_guest!(guest=nil) - guest ||= config.vm.guest - @logger.info("Loading guest: #{guest}") - - if guest.is_a?(Class) - raise Errors::VMGuestError, :_key => :invalid_class, :guest => guest.to_s if !(guest <= Vagrant.plugin("1", :guest)) - @guest = guest.new(self) - elsif guest.is_a?(Symbol) - # Look for the guest as a registered plugin - guest_klass = nil - Vagrant.plugin("1").registered.each do |plugin| - if plugin.guest.has_key?(guest) - guest_klass = plugin.guest[guest] - break - end - end - - raise Errors::VMGuestError, :_key => :unknown_type, :guest => guest.to_s if !guest_klass - @guest = guest_klass.new(self) - else - raise Errors::VMGuestError, :unspecified - end - end - - # Returns a channel object to communicate with the virtual - # machine. - def channel - @channel ||= Communication::SSH.new(self) - end - - # Returns the guest for this VM, loading the distro of the system if - # we can. - def guest - if !@loaded_guest_distro && state == :running - # Load the guest distro for the first time - result = @guest.distro_dispatch - load_guest!(result) - @loaded_guest_distro = true - @logger.info("Guest class: #{@guest.class}") - end - - @guest - end - - # Access the {Vagrant::SSH} object associated with this VM, which - # is used to get SSH credentials with the virtual machine. - def ssh - @ssh ||= SSH.new(self) - end - - # Returns the state of the VM as a symbol. - # - # @return [Symbol] - def state - return :not_created if !@uuid - state = @driver.read_state - return :not_created if !state - return state - end - - # Returns a boolean true if the VM has been created, otherwise - # returns false. - # - # @return [Boolean] - def created? - state != :not_created - end - - # Sets the currently active VM for this VM. If the VM is a valid, - # created virtual machine, then it will also update the local data - # to persist the VM. Otherwise, it will remove itself from the - # local data (if it exists). - def uuid=(value) - env.local_data[:active] ||= {} - if value - env.local_data[:active][name.to_s] = value - else - env.local_data[:active].delete(name.to_s) - end - - # Commit the local data so that the next time vagrant is initialized, - # it realizes the VM exists - env.local_data.commit - - # Store the uuid and reload the instance - @uuid = value - reload! - end - - def reload! - begin - @driver = Driver::VirtualBox.new(@uuid) - rescue Driver::VirtualBox::VMNotFound - # Clear the UUID since this VM doesn't exist. - @uuid = nil - - # Reset the driver. This shouldn't raise a VMNotFound since we won't - # feed it a UUID. - @driver = Driver::VirtualBox.new - end - end - - def package(options=nil) - run_action(:package, { "validate" => false }.merge(options || {})) - end - - def up(options=nil) - run_action(:up, options) - end - - def start(options=nil) - return if state == :running - return resume if state == :saved - - run_action(:start, options) - end - - def halt(options=nil) - run_action(:halt, options) - end - - def reload(options=nil) - run_action(:reload, options) - end - - def provision - run_action(:provision) - end - - def destroy - run_action(:destroy) - end - - def suspend - run_action(:suspend) - end - - def resume - run_action(:resume) - end - - def ui - return @_ui if defined?(@_ui) - @_ui = @env.ui.dup - @_ui.resource = @name - @_ui - end - - def run_action(name, options=nil) - options = { - :vm => self, - :ui => ui - }.merge(options || {}) - - env.action_runner.run(name, options) - end - end -end diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index 996074b26..f2af90389 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -7,11 +7,11 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant box add " - opts.separator "" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant box add " + o.separator "" - opts.on("-f", "--force", "Overwrite an existing box if it exists.") do |f| + o.on("-f", "--force", "Overwrite an existing box if it exists.") do |f| options[:force] = f end end @@ -28,8 +28,7 @@ module VagrantPlugins existing.destroy! if existing end - # Invoke the "box_add" middleware sequence. - @env.action_runner.run(:box_add, { + @env.action_runner.run(Vagrant::Action.action_box_add, { :box_name => argv[0], :box_provider => :virtualbox, :box_url => argv[1] diff --git a/plugins/commands/destroy/command.rb b/plugins/commands/destroy/command.rb index 9c18dafae..d0ef4b169 100644 --- a/plugins/commands/destroy/command.rb +++ b/plugins/commands/destroy/command.rb @@ -4,11 +4,11 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant destroy [vm-name]" - opts.separator "" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant destroy [vm-name]" + o.separator "" - opts.on("-f", "--force", "Destroy without confirmation.") do |f| + o.on("-f", "--force", "Destroy without confirmation.") do |f| options[:force] = f end end @@ -19,50 +19,12 @@ module VagrantPlugins @logger.debug("'Destroy' each target VM...") with_target_vms(argv, :reverse => true) do |vm| - if vm.created? - # Boolean whether we should actually go through with the destroy - # or not. This is true only if the "--force" flag is set or if the - # user confirms it. - do_destroy = false - - if options[:force] - do_destroy = true - else - choice = nil - - begin - choice = @env.ui.ask(I18n.t("vagrant.commands.destroy.confirmation", - :name => vm.name)) - rescue Interrupt - # Ctrl-C was pressed (or SIGINT). We just exit immediately - # with a non-zero exit status. - return 1 - rescue Vagrant::Errors::UIExpectsTTY - # We raise a more specific error but one which basically - # means the same thing. - raise Vagrant::Errors::DestroyRequiresForce - end - - do_destroy = choice.upcase == "Y" - end - - if do_destroy - @logger.info("Destroying: #{vm.name}") - vm.destroy - else - @logger.info("Not destroying #{vm.name} since confirmation was declined.") - @env.ui.success(I18n.t("vagrant.commands.destroy.will_not_destroy", - :name => vm.name), :prefix => false) - end - else - @logger.info("Not destroying #{vm.name}, since it isn't created.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + vm.action(:destroy) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/commands/halt/command.rb b/plugins/commands/halt/command.rb index 150b7eb37..1bda63eea 100644 --- a/plugins/commands/halt/command.rb +++ b/plugins/commands/halt/command.rb @@ -22,13 +22,8 @@ module VagrantPlugins @logger.debug("Halt command: #{argv.inspect} #{options.inspect}") with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Halting #{vm.name}") - vm.halt(:force => options[:force]) - else - @logger.info("Not halting #{vm.name}, since not created.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + # XXX: "force" + vm.action(:halt) end # Success, exit status 0 diff --git a/plugins/commands/package/command.rb b/plugins/commands/package/command.rb index d8b5e35fb..02338aa9d 100644 --- a/plugins/commands/package/command.rb +++ b/plugins/commands/package/command.rb @@ -6,25 +6,25 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant package [vm-name] [--base name] [--output name.box]" - opts.separator " [--include one,two,three] [--vagrantfile file]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant package [vm-name] [--base name] [--output name.box]" + o.separator " [--include one,two,three] [--vagrantfile file]" - opts.separator "" + o.separator "" - opts.on("--base NAME", "Name of a VM in virtualbox to package as a base box") do |b| + o.on("--base NAME", "Name of a VM in virtualbox to package as a base box") do |b| options[:base] = b end - opts.on("--output NAME", "Name of the file to output") do |o| - options[:output] = o + o.on("--output NAME", "Name of the file to output") do |output| + options[:output] = output end - opts.on("--include x,y,z", Array, "Additional files to package with the box.") do |i| + o.on("--include x,y,z", Array, "Additional files to package with the box.") do |i| options[:include] = i end - opts.on("--vagrantfile file", "Vagrantfile to package with the box.") do |v| + o.on("--vagrantfile file", "Vagrantfile to package with the box.") do |v| options[:vagrantfile] = v end end @@ -42,20 +42,28 @@ module VagrantPlugins # Success, exit status 0 0 - end + end protected def package_base(options) - vm = Vagrant::VM.new(options[:base], @env, @env.config.global, :base => true) - raise Vagrant::Errors::BaseVMNotFound, :name => options[:base] if !vm.created? + # XXX: This whole thing is hardcoded and very temporary. The whole + # `vagrant package --base` process is deprecated for something much + # better in the future. We just hardcode this to keep VirtualBox working + # for now. + provider = nil + Vagrant.plugin("1").registered.each do |plugin| + provider = plugin.provider.get(:virtualbox) + break if provider + end + + vm = Vagrant::Machine.new(options[:base], provider, @env.config.global, nil, @env, true) @logger.debug("Packaging base VM: #{vm.name}") package_vm(vm, options) end def package_target(name, options) with_target_vms(name, :single_target => true) do |vm| - raise Vagrant::Errors::VMNotCreatedError if !vm.created? @logger.debug("Packaging VM: #{vm.name}") package_vm(vm, options) end @@ -68,7 +76,7 @@ module VagrantPlugins acc end - vm.package(opts) + vm.action(:package, opts) end end end diff --git a/plugins/commands/provision/command.rb b/plugins/commands/provision/command.rb index d6bd66cbe..bbaa4044e 100644 --- a/plugins/commands/provision/command.rb +++ b/plugins/commands/provision/command.rb @@ -4,10 +4,8 @@ module VagrantPlugins module CommandProvision class Command < Vagrant.plugin("1", :command) def execute - options = {} - - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant provision [vm-name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant provision [vm-name]" end # Parse the options @@ -16,25 +14,13 @@ module VagrantPlugins # Go over each VM and provision! @logger.debug("'provision' each target VM...") - with_target_vms(argv) do |vm| - - if vm.created? - if vm.state == :running - @logger.info("Provisioning: #{vm.name}") - vm.provision - else - @logger.info("#{vm.name} not running. Not provisioning.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_running") - end - else - @logger.info("#{vm.name} not created. Not provisioning.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:provision) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/commands/reload/command.rb b/plugins/commands/reload/command.rb index 79f88eab0..718a45917 100644 --- a/plugins/commands/reload/command.rb +++ b/plugins/commands/reload/command.rb @@ -14,10 +14,10 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant reload [vm-name]" - opts.separator "" - build_start_options(opts, options) + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant reload [vm-name]" + o.separator "" + build_start_options(o, options) end # Parse the options @@ -25,19 +25,13 @@ module VagrantPlugins return if !argv @logger.debug("'reload' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Reloading: #{vm.name}") - vm.reload(options) - else - @logger.info("Not created: #{vm.name}. Not reloading.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:reload) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/commands/resume/command.rb b/plugins/commands/resume/command.rb index 256593d90..db942b48d 100644 --- a/plugins/commands/resume/command.rb +++ b/plugins/commands/resume/command.rb @@ -4,10 +4,8 @@ module VagrantPlugins module CommandResume class Command < Vagrant.plugin("1", :command) def execute - options = {} - - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant resume [vm-name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant resume [vm-name]" end # Parse the options @@ -15,19 +13,13 @@ module VagrantPlugins return if !argv @logger.debug("'resume' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Resume: #{vm.name}") - vm.resume - else - @logger.info("Not created: #{vm.name}. Not resuming.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:resume) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/commands/ssh/command.rb b/plugins/commands/ssh/command.rb index 6f5aa7b3f..a9b73dad8 100644 --- a/plugins/commands/ssh/command.rb +++ b/plugins/commands/ssh/command.rb @@ -37,53 +37,29 @@ module VagrantPlugins # Execute the actual SSH with_target_vms(argv, :single_target => true) do |vm| - # Basic checks that are required for proper SSH - raise Vagrant::Errors::VMNotCreatedError if !vm.created? - raise Vagrant::Errors::VMInaccessible if !vm.state == :inaccessible - raise Vagrant::Errors::VMNotRunningError if vm.state != :running - if options[:command] - ssh_execute(vm, options[:command]) + @logger.debug("Executing single command on remote machine: #{options[:command]}") + env = vm.action(:ssh_run, :ssh_run_command => options[:command]) + + # Exit with the exit status of the command or a 0 if we didn't + # get one. + exit_status = env[:ssh_run_exit_status] || 0 + return exit_status else opts = { :plain_mode => options[:plain_mode], :extra_args => options[:ssh_args] } - ssh_connect(vm, opts) + @logger.debug("Invoking `ssh` action on machine") + vm.action(:ssh, :ssh_opts => opts) + + # We should never reach this point, since the point of `ssh` + # is to exec into the proper SSH shell, but we'll just return + # an exit status of 0 anyways. + return 0 end end - - # Success, exit status 0 - 0 - end - - protected - - def ssh_execute(vm, command=nil) - exit_status = 0 - - @logger.debug("Executing command: #{command}") - exit_status = vm.channel.execute(command, :error_check => false) do |type, data| - # Determine the proper channel to send the output onto depending - # on the type of data we are receiving. - channel = type == :stdout ? :out : :error - - # Print the SSH output as it comes in, but don't prefix it and don't - # force a new line so that the output is properly preserved - vm.ui.info(data.to_s, - :prefix => false, - :new_line => false, - :channel => channel) - end - - # Exit with the exit status we got from executing the command - exit exit_status - end - - def ssh_connect(vm, opts) - @logger.debug("`exec` into ssh prompt") - vm.ssh.exec(opts) end end end diff --git a/plugins/commands/ssh_config/command.rb b/plugins/commands/ssh_config/command.rb index 60f88a9bb..05bb65f76 100644 --- a/plugins/commands/ssh_config/command.rb +++ b/plugins/commands/ssh_config/command.rb @@ -10,12 +10,11 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant ssh-config [vm-name] [--host name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant ssh-config [vm-name] [--host name]" + o.separator "" - opts.separator "" - - opts.on("--host NAME", "Name the host for the config..") do |h| + o.on("--host COMMAND", "Name the host for the config..") do |h| options[:host] = h end end @@ -23,13 +22,12 @@ module VagrantPlugins argv = parse_options(opts) return if !argv - with_target_vms(argv, :single_target => true) do |vm| - raise Vagrant::Errors::VMNotCreatedError if !vm.created? - raise Vagrant::Errors::VMInaccessible if !vm.state == :inaccessible + with_target_vms(argv, :single_target => true) do |machine| + ssh_info = machine.ssh_info + raise Vagrant::Errors::SSHNotReady if ssh_info.nil? - ssh_info = vm.ssh.info variables = { - :host_key => options[:host] || vm.name || "vagrant", + :host_key => options[:host] || machine.name || "vagrant", :ssh_host => ssh_info[:host], :ssh_port => ssh_info[:port], :ssh_user => ssh_info[:username], @@ -45,7 +43,7 @@ module VagrantPlugins # Success, exit status 0 0 - end + end end end end diff --git a/plugins/commands/suspend/command.rb b/plugins/commands/suspend/command.rb index 539be8ac5..6183b71d3 100644 --- a/plugins/commands/suspend/command.rb +++ b/plugins/commands/suspend/command.rb @@ -4,10 +4,8 @@ module VagrantPlugins module CommandSuspend class Command < Vagrant.plugin("1", :command) def execute - options = {} - - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant suspend [vm-name]" + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant suspend [vm-name]" end # Parse the options @@ -16,18 +14,12 @@ module VagrantPlugins @logger.debug("'suspend' each target VM...") with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Suspending: #{vm.name}") - vm.suspend - else - @logger.info("Not created: #{vm.name}. Not suspending.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + vm.action(:suspend) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/commands/up/command.rb b/plugins/commands/up/command.rb index 1336760ed..186f0c98e 100644 --- a/plugins/commands/up/command.rb +++ b/plugins/commands/up/command.rb @@ -11,10 +11,10 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant up [vm-name] [--[no-]provision] [-h]" - opts.separator "" - build_start_options(opts, options) + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant up [vm-name] [--[no-]provision] [-h]" + o.separator "" + build_start_options(o, options) end # Parse the options @@ -23,20 +23,13 @@ module VagrantPlugins # Go over each VM and bring it up @logger.debug("'Up' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Booting: #{vm.name}") - vm.ui.info I18n.t("vagrant.commands.up.vm_created") - vm.start(options) - else - @logger.info("Creating: #{vm.name}") - vm.up(options) - end + with_target_vms(argv) do |machine| + machine.action(:up) end # Success, exit status 0 0 - end + end end end end diff --git a/lib/vagrant/communication/ssh.rb b/plugins/communicators/ssh/communicator.rb similarity index 83% rename from lib/vagrant/communication/ssh.rb rename to plugins/communicators/ssh/communicator.rb index 353260547..c268e3b34 100644 --- a/lib/vagrant/communication/ssh.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -8,17 +8,23 @@ require 'vagrant/util/ansi_escape_code_remover' require 'vagrant/util/file_mode' require 'vagrant/util/platform' require 'vagrant/util/retryable' +require 'vagrant/util/ssh' -module Vagrant - module Communication - # Provides communication with the VM via SSH. - class SSH < Base - include Util::ANSIEscapeCodeRemover - include Util::Retryable +module VagrantPlugins + module CommunicatorSSH + # This class provides communication with the VM via SSH. + class Communicator < Vagrant.plugin("1", :communicator) + include Vagrant::Util::ANSIEscapeCodeRemover + include Vagrant::Util::Retryable - def initialize(vm) - @vm = vm - @logger = Log4r::Logger.new("vagrant::communication::ssh") + def self.match?(machine) + # All machines are currently expected to have SSH. + true + end + + def initialize(machine) + @machine = machine + @logger = Log4r::Logger.new("vagrant::communication::ssh") @connection = nil end @@ -31,7 +37,7 @@ module Vagrant # If we reached this point then we successfully connected @logger.info("SSH is ready!") true - rescue Errors::VagrantError => e + rescue Vagrant::Errors::VagrantError => e # We catch a `VagrantError` which would signal that something went # wrong expectedly in the `connect`, which means we didn't connect. @logger.info("SSH not up: #{e.inspect}") @@ -41,7 +47,7 @@ module Vagrant def execute(command, opts=nil, &block) opts = { :error_check => true, - :error_class => Errors::VagrantError, + :error_class => Vagrant::Errors::VagrantError, :error_key => :ssh_bad_exit_status, :command => command, :sudo => false @@ -79,6 +85,11 @@ module Vagrant end end + def test(command, opts=nil) + opts = { :error_check => false }.merge(opts || {}) + execute(command, opts) == 0 + end + def upload(from, to) @logger.debug("Uploading: #{from} to #{to}") @@ -93,7 +104,7 @@ module Vagrant # Otherwise, it is a permission denied, so let's raise a proper # exception - raise Errors::SCPPermissionDenied, :path => from.to_s + raise Vagrant::Errors::SCPPermissionDenied, :path => from.to_s end protected @@ -120,7 +131,8 @@ module Vagrant end end - ssh_info = @vm.ssh.info + # XXX: We need to raise some exception if SSH is not ready + ssh_info = @machine.ssh_info # Build the options we'll use to initiate the connection via Net::SSH opts = { @@ -134,38 +146,38 @@ module Vagrant } # Check that the private key permissions are valid - @vm.ssh.check_key_permissions(ssh_info[:private_key_path]) + Vagrant::Util::SSH.check_key_permissions(ssh_info[:private_key_path]) # Connect to SSH, giving it a few tries connection = nil begin exceptions = [Errno::ECONNREFUSED, Net::SSH::Disconnect, Timeout::Error] - connection = retryable(:tries => @vm.config.ssh.max_tries, :on => exceptions) do - Timeout.timeout(@vm.config.ssh.timeout) do + connection = retryable(:tries => @machine.config.ssh.max_tries, :on => exceptions) do + Timeout.timeout(@machine.config.ssh.timeout) do @logger.info("Attempting to connect to SSH: #{ssh_info[:host]}:#{ssh_info[:port]}") Net::SSH.start(ssh_info[:host], ssh_info[:username], opts) end end rescue Timeout::Error # This happens if we continued to timeout when attempting to connect. - raise Errors::SSHConnectionTimeout + raise Vagrant::Errors::SSHConnectionTimeout rescue Net::SSH::AuthenticationFailed # This happens if authentication failed. We wrap the error in our # own exception. - raise Errors::SSHAuthenticationFailed + raise Vagrant::Errors::SSHAuthenticationFailed rescue Net::SSH::Disconnect # This happens if the remote server unexpectedly closes the # connection. This is usually raised when SSH is running on the # other side but can't properly setup a connection. This is # usually a server-side issue. - raise Errors::SSHDisconnected + raise Vagrant::Errors::SSHDisconnected rescue Errno::ECONNREFUSED # This is raised if we failed to connect the max amount of times - raise Errors::SSHConnectionRefused + raise Vagrant::Errors::SSHConnectionRefused rescue NotImplementedError # This is raised if a private key type that Net-SSH doesn't support # is used. Show a nicer error. - raise Errors::SSHKeyTypeNotSupported + raise Vagrant::Errors::SSHKeyTypeNotSupported end @connection = connection @@ -178,7 +190,7 @@ module Vagrant # Yield the connection that is ready to be used and # return the value of the block return yield connection if block_given? - end + end # Executes the command on an SSH connection within a login shell. def shell_execute(connection, command, sudo=false) @@ -187,7 +199,7 @@ module Vagrant # Determine the shell to execute. If we are using `sudo` then we # need to wrap the shell in a `sudo` call. - shell = @vm.config.ssh.shell + shell = @machine.config.ssh.shell shell = "sudo -H #{shell}" if sudo # Open the channel so we can execute or command @@ -248,7 +260,7 @@ module Vagrant end rescue Net::SCP::Error => e # If we get the exit code of 127, then this means SCP is unavailable. - raise Errors::SCPUnavailable if e.message =~ /\(127\)/ + raise Vagrant::Errors::SCPUnavailable if e.message =~ /\(127\)/ # Otherwise, just raise the error up raise diff --git a/plugins/communicators/ssh/plugin.rb b/plugins/communicators/ssh/plugin.rb new file mode 100644 index 000000000..10de987a4 --- /dev/null +++ b/plugins/communicators/ssh/plugin.rb @@ -0,0 +1,19 @@ +require "vagrant" + +module VagrantPlugins + module CommunicatorSSH + class Plugin < Vagrant.plugin("1") + name "ssh communiator" + description <<-DESC + This plugin allows Vagrant to communicate with remote machines using + SSH as the underlying protocol, powered internally by Ruby's + net-ssh library. + DESC + + communicator("ssh") do + require File.expand_path("../communicator", __FILE__) + Communicator + end + end + end +end diff --git a/plugins/guests/linux/guest.rb b/plugins/guests/linux/guest.rb index 49d40be0c..82f512e8a 100644 --- a/plugins/guests/linux/guest.rb +++ b/plugins/guests/linux/guest.rb @@ -16,23 +16,25 @@ module VagrantPlugins end def distro_dispatch - if @vm.channel.test("cat /etc/debian_version") - return :debian if @vm.channel.test("cat /proc/version | grep 'Debian'") - return :ubuntu if @vm.channel.test("cat /proc/version | grep 'Ubuntu'") - end + @vm.communicate.tap do |comm| + if comm.test("cat /etc/debian_version") == 0 + return :debian if comm.test("cat /proc/version | grep 'Debian'") == 0 + return :ubuntu if comm.test("cat /proc/version | grep 'Ubuntu'") == 0 + end - return :gentoo if @vm.channel.test("cat /etc/gentoo-release") - return :fedora if @vm.channel.test("grep 'Fedora release 16' /etc/redhat-release") - return :redhat if @vm.channel.test("cat /etc/redhat-release") - return :suse if @vm.channel.test("cat /etc/SuSE-release") - return :arch if @vm.channel.test("cat /etc/arch-release") + return :gentoo if comm.test("cat /etc/gentoo-release") == 0 + return :fedora if comm.test("grep 'Fedora release 16' /etc/redhat-release") == 0 + return :redhat if comm.test("cat /etc/redhat-release") == 0 + return :suse if comm.test("cat /etc/SuSE-release") == 0 + return :arch if comm.test("cat /etc/arch-release") == 0 + end # Can't detect the distro, assume vanilla linux nil end def halt - @vm.channel.sudo("shutdown -h now") + @vm.communicate.sudo("shutdown -h now") # Wait until the VM's state is actually powered off. If this doesn't # occur within a reasonable amount of time (15 seconds by default), @@ -50,9 +52,9 @@ module VagrantPlugins real_guestpath = expanded_guest_path(guestpath) @logger.debug("Shell expanded guest path: #{real_guestpath}") - @vm.channel.sudo("mkdir -p #{real_guestpath}") + @vm.communicate.sudo("mkdir -p #{real_guestpath}") mount_folder(name, real_guestpath, options) - @vm.channel.sudo("chown `id -u #{options[:owner]}`:`id -g #{options[:group]}` #{real_guestpath}") + @vm.communicate.sudo("chown `id -u #{options[:owner]}`:`id -g #{options[:group]}` #{real_guestpath}") end def mount_nfs(ip, folders) @@ -63,8 +65,8 @@ module VagrantPlugins real_guestpath = expanded_guest_path(opts[:guestpath]) # Do the actual creating and mounting - @vm.channel.sudo("mkdir -p #{real_guestpath}") - @vm.channel.sudo("mount -o vers=#{opts[:nfs_version]} #{ip}:'#{opts[:hostpath]}' #{real_guestpath}", + @vm.communicate.sudo("mkdir -p #{real_guestpath}") + @vm.communicate.sudo("mount -o vers=#{opts[:nfs_version]} #{ip}:'#{opts[:hostpath]}' #{real_guestpath}", :error_class => LinuxError, :error_key => :mount_nfs_fail) end @@ -80,7 +82,7 @@ module VagrantPlugins # @return [String] The expanded guestpath def expanded_guest_path(guestpath) real_guestpath = nil - @vm.channel.execute("printf #{guestpath}") do |type, data| + @vm.communicate.execute("printf #{guestpath}") do |type, data| if type == :stdout real_guestpath ||= "" real_guestpath += data @@ -105,7 +107,7 @@ module VagrantPlugins attempts = 0 while true success = true - @vm.channel.sudo("mount -t vboxsf #{mount_options} #{name} #{guestpath}") do |type, data| + @vm.communicate.sudo("mount -t vboxsf #{mount_options} #{name} #{guestpath}") do |type, data| success = false if type == :stderr && data =~ /No such device/i end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb new file mode 100644 index 000000000..a775c72e3 --- /dev/null +++ b/plugins/providers/virtualbox/action.rb @@ -0,0 +1,289 @@ +require "vagrant/action/builder" + +module VagrantPlugins + module ProviderVirtualBox + module Action + autoload :Boot, File.expand_path("../action/boot", __FILE__) + autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) + autoload :CheckBox, File.expand_path("../action/check_box", __FILE__) + autoload :CheckCreated, File.expand_path("../action/check_created", __FILE__) + autoload :CheckGuestAdditions, File.expand_path("../action/check_guest_additions", __FILE__) + autoload :CheckPortCollisions, File.expand_path("../action/check_port_collisions", __FILE__) + autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) + autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) + autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) + autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) + autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) + autoload :ClearSharedFolders, File.expand_path("../action/clear_shared_folders", __FILE__) + autoload :Created, File.expand_path("../action/created", __FILE__) + autoload :Customize, File.expand_path("../action/customize", __FILE__) + autoload :DefaultName, File.expand_path("../action/default_name", __FILE__) + autoload :Destroy, File.expand_path("../action/destroy", __FILE__) + autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) + autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) + autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) + autoload :Export, File.expand_path("../action/export", __FILE__) + autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) + autoload :Halt, File.expand_path("../action/halt", __FILE__) + autoload :HostName, File.expand_path("../action/host_name", __FILE__) + autoload :Import, File.expand_path("../action/import", __FILE__) + autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) + autoload :IsSaved, File.expand_path("../action/is_saved", __FILE__) + autoload :MatchMACAddress, File.expand_path("../action/match_mac_address", __FILE__) + autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) + autoload :MessageNotRunning, File.expand_path("../action/message_not_running", __FILE__) + autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) + autoload :Network, File.expand_path("../action/network", __FILE__) + autoload :NFS, File.expand_path("../action/nfs", __FILE__) + autoload :Package, File.expand_path("../action/package", __FILE__) + autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) + autoload :Provision, File.expand_path("../action/provision", __FILE__) + autoload :ProvisionerCleanup, File.expand_path("../action/provisioner_cleanup", __FILE__) + autoload :PruneNFSExports, File.expand_path("../action/prune_nfs_exports", __FILE__) + autoload :Resume, File.expand_path("../action/resume", __FILE__) + autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) + autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) + autoload :ShareFolders, File.expand_path("../action/share_folders", __FILE__) + autoload :Suspend, File.expand_path("../action/suspend", __FILE__) + + # Include the built-in modules so that we can use them as top-level + # things. + include Vagrant::Action::Builtin + + # This action boots the VM, assuming the VM is in a state that requires + # a bootup (i.e. not saved). + def self.action_boot + Vagrant::Action::Builder.new.tap do |b| + b.use CheckAccessible + b.use CleanMachineFolder + b.use ClearForwardedPorts + b.use EnvSet, :port_collision_handler => :correct + b.use ForwardPorts + b.use Provision + b.use PruneNFSExports + b.use NFS + b.use ClearSharedFolders + b.use ShareFolders + b.use ClearNetworkInterfaces + b.use Network + b.use HostName + b.use SaneDefaults + b.use Customize + b.use Boot + end + end + + # This is the action that is primarily responsible for completely + # freeing the resources of the underlying virtual machine. + def self.action_destroy + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use Call, DestroyConfirm do |env2, b3| + if env2[:result] + b3.use Vagrant::Action::General::Validate + b3.use CheckAccessible + b3.use EnvSet, :force => true + b3.use action_halt + b3.use ProvisionerCleanup + b3.use PruneNFSExports + b3.use Destroy + b3.use CleanMachineFolder + b3.use DestroyUnusedNetworkInterfaces + else + b3.use MessageWillNotDestroy + end + end + end + end + end + + # This is the action that is primarily responsible for halting + # the virtual machine, gracefully or by force. + def self.action_halt + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use CheckAccessible + b2.use DiscardState + b2.use Halt + else + b2.use MessageNotCreated + end + end + end + end + + # This action packages the virtual machine into a single box file. + def self.action_package + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use SetupPackageFiles + b2.use CheckAccessible + b2.use action_halt + b2.use ClearForwardedPorts + b2.use ClearSharedFolders + b2.use Export + b2.use PackageVagrantfile + b2.use Package + end + end + end + + # This action just runs the provisioners on the machine. + def self.action_provision + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use Call, IsRunning do |env2, b3| + if !env2[:result] + b3.use MessageNotRunning + next + end + + b3.use CheckAccessible + b3.use Provision + end + end + end + end + + # This action is responsible for reloading the machine, which + # brings it down, sucks in new configuration, and brings the + # machine back up with the new configuration. + def self.action_reload + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use Vagrant::Action::General::Validate + b2.use action_halt + b2.use action_start + end + end + end + + # This is the action that is primarily responsible for resuming + # suspended machines. + def self.action_resume + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use CheckAccessible + b2.use CheckPortCollisions + b2.use Resume + else + b2.use MessageNotCreated + end + end + end + end + + # This is the action that will exec into an SSH shell. + def self.action_ssh + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use CheckCreated + b.use CheckAccessible + b.use CheckRunning + b.use SSHExec + end + end + + # This is the action that will run a single SSH command. + def self.action_ssh_run + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use CheckCreated + b.use CheckAccessible + b.use CheckRunning + b.use SSHRun + end + end + + # This action starts a VM, assuming it is already imported and exists. + # A precondition of this action is that the VM exists. + def self.action_start + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use Call, IsRunning do |env, b2| + # If the VM is running, then our work here is done, exit + next if env[:result] + + b2.use Call, IsSaved do |env2, b3| + if env2[:result] + # The VM is saved, so just resume it + b3.use action_resume + else + # The VM is not saved, so we must have to boot it up + # like normal. Boot! + b3.use action_boot + end + end + end + end + end + + # This is the action that is primarily responsible for suspending + # the virtual machine. + def self.action_suspend + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use CheckAccessible + b2.use Suspend + else + b2.use MessageNotCreated + end + end + end + end + + # This action brings the machine up from nothing, including importing + # the box, configuring metadata, and booting. + def self.action_up + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use Call, Created do |env, b2| + # If the VM is NOT created yet, then do the setup steps + if !env[:result] + b2.use CheckAccessible + b2.use CheckBox + b2.use Import + b2.use CheckGuestAdditions + b2.use DefaultName + b2.use MatchMACAddress + end + end + b.use action_start + end + end + end + end +end diff --git a/lib/vagrant/action/vm/boot.rb b/plugins/providers/virtualbox/action/boot.rb similarity index 72% rename from lib/vagrant/action/vm/boot.rb rename to plugins/providers/virtualbox/action/boot.rb index 6a2ce7d35..7c4c18f0e 100644 --- a/lib/vagrant/action/vm/boot.rb +++ b/plugins/providers/virtualbox/action/boot.rb @@ -1,32 +1,27 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Boot def initialize(app, env) @app = app - @env = env end def call(env) @env = env # Start up the VM and wait for it to boot. - boot + env[:ui].info I18n.t("vagrant.actions.vm.boot.booting") + env[:machine].provider.driver.start(@env[:machine].config.vm.boot_mode) raise Errors::VMFailedToBoot if !wait_for_boot @app.call(env) end - def boot - @env[:ui].info I18n.t("vagrant.actions.vm.boot.booting") - @env[:vm].driver.start(@env[:vm].config.vm.boot_mode) - end - def wait_for_boot @env[:ui].info I18n.t("vagrant.actions.vm.boot.waiting") - @env[:vm].config.ssh.max_tries.to_i.times do |i| - if @env[:vm].channel.ready? + @env[:machine].config.ssh.max_tries.to_i.times do |i| + if @env[:machine].communicate.ready? @env[:ui].info I18n.t("vagrant.actions.vm.boot.ready") return true end @@ -37,7 +32,7 @@ module Vagrant # If the VM is not starting or running, something went wrong # and we need to show a useful error. - state = @env[:vm].state + state = @env[:machine].provider.state raise Errors::VMFailedToRun if state != :starting && state != :running sleep 2 if !@env["vagrant.test"] @@ -46,8 +41,8 @@ module Vagrant @env[:ui].error I18n.t("vagrant.actions.vm.boot.failed") false end + end end end end - diff --git a/lib/vagrant/action/vm/check_accessible.rb b/plugins/providers/virtualbox/action/check_accessible.rb similarity index 73% rename from lib/vagrant/action/vm/check_accessible.rb rename to plugins/providers/virtualbox/action/check_accessible.rb index 69df47194..f000825b1 100644 --- a/lib/vagrant/action/vm/check_accessible.rb +++ b/plugins/providers/virtualbox/action/check_accessible.rb @@ -1,18 +1,18 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class CheckAccessible def initialize(app, env) @app = app end def call(env) - if env[:vm].state == :inaccessible + if env[:machine].state == :inaccessible # The VM we are attempting to manipulate is inaccessible. This # is a very bad situation and can only be fixed by the user. It # also prohibits us from actually doing anything with the virtual # machine, so we raise an error. - raise Errors::VMInaccessible + raise Vagrant::Errors::VMInaccessible end @app.call(env) diff --git a/lib/vagrant/action/vm/check_box.rb b/plugins/providers/virtualbox/action/check_box.rb similarity index 59% rename from lib/vagrant/action/vm/check_box.rb rename to plugins/providers/virtualbox/action/check_box.rb index 454f8b492..02c6fa866 100644 --- a/lib/vagrant/action/vm/check_box.rb +++ b/plugins/providers/virtualbox/action/check_box.rb @@ -1,18 +1,18 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class CheckBox def initialize(app, env) @app = app end def call(env) - box_name = env[:vm].config.vm.box - raise Errors::BoxNotSpecified if !box_name + box_name = env[:machine].config.vm.box + raise Vagrant::Errors::BoxNotSpecified if !box_name if !env[:box_collection].find(box_name, :virtualbox) - box_url = env[:vm].config.vm.box_url - raise Errors::BoxSpecifiedDoesntExist, :name => box_name if !box_url + box_url = env[:machine].config.vm.box_url + raise Vagrant::Errors::BoxSpecifiedDoesntExist, :name => box_name if !box_url # Add the box then reload the box collection so that it becomes # aware of it. @@ -21,8 +21,8 @@ module Vagrant env[:box_collection].reload! # Reload the environment and set the VM to be the new loaded VM. - env[:vm].env.reload! - env[:vm] = env[:vm].env.vms[env[:vm].name] + env[:machine].env.reload! + env[:machine] = env[:machine].env.vms[env[:machine].name] end @app.call(env) diff --git a/plugins/providers/virtualbox/action/check_created.rb b/plugins/providers/virtualbox/action/check_created.rb new file mode 100644 index 000000000..307cc1ed4 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_created.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + # This middleware checks that the VM is created, and raises an exception + # if it is not, notifying the user that creation is required. + class CheckCreated + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].state == :not_created + raise Vagrant::Errors::VMNotCreatedError + end + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/check_guest_additions.rb b/plugins/providers/virtualbox/action/check_guest_additions.rb similarity index 71% rename from lib/vagrant/action/vm/check_guest_additions.rb rename to plugins/providers/virtualbox/action/check_guest_additions.rb index fb96a5e65..2fd798814 100644 --- a/lib/vagrant/action/vm/check_guest_additions.rb +++ b/plugins/providers/virtualbox/action/check_guest_additions.rb @@ -1,9 +1,6 @@ -module Vagrant - module Action - module VM - # Middleware which verifies that the VM has the proper guest additions - # installed and prints a warning if they're not detected or if the - # version does not match the installed VirtualBox version. +module VagrantPlugins + module ProviderVirtualBox + module Action class CheckGuestAdditions def initialize(app, env) @app = app @@ -12,13 +9,13 @@ module Vagrant def call(env) # Use the raw interface for now, while the virtualbox gem # doesn't support guest properties (due to cross platform issues) - version = env[:vm].driver.read_guest_additions_version + version = env[:machine].provider.driver.read_guest_additions_version if !version env[:ui].warn I18n.t("vagrant.actions.vm.check_guest_additions.not_detected") else # Strip the -OSE/_OSE off from the guest additions and the virtual box # version since all the matters are that the version _numbers_ match up. - guest_version, vb_version = [version, env[:vm].driver.version].map do |v| + guest_version, vb_version = [version, env[:machine].provider.driver.version].map do |v| v.gsub(/[-_]ose/i, '') end @@ -32,6 +29,7 @@ module Vagrant # Continue @app.call(env) end + end end end diff --git a/lib/vagrant/action/vm/check_port_collisions.rb b/plugins/providers/virtualbox/action/check_port_collisions.rb similarity index 67% rename from lib/vagrant/action/vm/check_port_collisions.rb rename to plugins/providers/virtualbox/action/check_port_collisions.rb index 95f6d4f2f..75ed07132 100644 --- a/lib/vagrant/action/vm/check_port_collisions.rb +++ b/plugins/providers/virtualbox/action/check_port_collisions.rb @@ -1,12 +1,10 @@ require "vagrant/util/is_port_open" -module Vagrant - module Action - module VM - # Action that checks to make sure there are no forwarded port collisions, - # and raises an exception if there is. +module VagrantPlugins + module ProviderVirtualBox + module Action class CheckPortCollisions - include Util::IsPortOpen + include Vagrant::Util::IsPortOpen def initialize(app, env) @app = app @@ -17,17 +15,17 @@ module Vagrant @env = env # Figure out how we handle port collisions. By default we error. - handler = env[:port_collision_handler] || :error + handler = env[:port_collision_handler] || :error # Read our forwarded ports, if we have any, to override what # we have configured. current = {} - env[:vm].driver.read_forwarded_ports.each do |nic, name, hostport, guestport| + env[:machine].provider.driver.read_forwarded_ports.each do |nic, name, hostport, guestport| current[name] = hostport.to_i end - existing = env[:vm].driver.read_used_ports - env[:vm].config.vm.forwarded_ports.each do |options| + existing = env[:machine].provider.driver.read_used_ports + env[:machine].config.vm.forwarded_ports.each do |options| # Use the proper port, whether that be the configured port or the # port that is currently on use of the VM. hostport = options[:hostport].to_i @@ -44,7 +42,7 @@ module Vagrant # Handles a port collision by raising an exception. def handle_error(options, existing_ports) - raise Errors::ForwardPortCollisionResume + raise Vagrant::Errors::ForwardPortCollisionResume end # Handles a port collision by attempting to fix it. @@ -55,21 +53,23 @@ module Vagrant if !options[:auto] # Auto fixing is disabled for this port forward, so we # must throw an error so the user can fix it. - raise Errors::ForwardPortCollision, :host_port => options[:hostport].to_s, - :guest_port => options[:guestport].to_s + raise Vagrant::Errors::ForwardPortCollision, + :host_port => options[:hostport].to_s, + :guest_port => options[:guestport].to_s end # Get the auto port range and get rid of the used ports and # ports which are being used in other forwards so we're just # left with available ports. - range = @env[:vm].config.vm.auto_port_range.to_a - range -= @env[:vm].config.vm.forwarded_ports.collect { |opts| opts[:hostport].to_i } + range = @env[:machine].config.vm.auto_port_range.to_a + range -= @env[:machine].config.vm.forwarded_ports.collect { |opts| opts[:hostport].to_i } range -= existing_ports if range.empty? - raise Errors::ForwardPortAutolistEmpty, :vm_name => @env[:vm].name, - :host_port => options[:hostport].to_s, - :guest_port => options[:guestport].to_s + raise Vagrant::Errors::ForwardPortAutolistEmpty, + :vm_name => @env[:machine].name, + :host_port => options[:hostport].to_s, + :guest_port => options[:guestport].to_s end # Set the port up to be the first one and add that port to diff --git a/plugins/providers/virtualbox/action/check_running.rb b/plugins/providers/virtualbox/action/check_running.rb new file mode 100644 index 000000000..483493602 --- /dev/null +++ b/plugins/providers/virtualbox/action/check_running.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + # This middleware checks that the VM is running, and raises an exception + # if it is not, notifying the user that the VM must be running. + class CheckRunning + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].state != :running + raise Vagrant::Errors::VMNotRunningError + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/check_virtualbox.rb b/plugins/providers/virtualbox/action/check_virtualbox.rb new file mode 100644 index 000000000..1d590fe4f --- /dev/null +++ b/plugins/providers/virtualbox/action/check_virtualbox.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + # Checks that VirtualBox is installed and ready to be used. + class CheckVirtualbox + def initialize(app, env) + @app = app + end + + def call(env) + # This verifies that VirtualBox is installed and the driver is + # ready to function. If not, then an exception will be raised + # which will break us out of execution of the middleware sequence. + Driver::Meta.new.verify! + + # Carry on. + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/clean_machine_folder.rb b/plugins/providers/virtualbox/action/clean_machine_folder.rb similarity index 87% rename from lib/vagrant/action/vm/clean_machine_folder.rb rename to plugins/providers/virtualbox/action/clean_machine_folder.rb index 66c522342..3007cbd0f 100644 --- a/lib/vagrant/action/vm/clean_machine_folder.rb +++ b/plugins/providers/virtualbox/action/clean_machine_folder.rb @@ -1,8 +1,8 @@ -require 'fileutils' +require "fileutils" -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action # Cleans up the VirtualBox machine folder for any ".xml-prev" # files which VirtualBox may have left over. This is a bug in # VirtualBox. As soon as this is fixed, this middleware can and @@ -13,7 +13,7 @@ module Vagrant end def call(env) - clean_machine_folder(env[:vm].driver.read_machine_folder) + clean_machine_folder(env[:machine].provider.driver.read_machine_folder) @app.call(env) end diff --git a/lib/vagrant/action/vm/clear_forwarded_ports.rb b/plugins/providers/virtualbox/action/clear_forwarded_ports.rb similarity index 67% rename from lib/vagrant/action/vm/clear_forwarded_ports.rb rename to plugins/providers/virtualbox/action/clear_forwarded_ports.rb index cf25df861..c4e87610b 100644 --- a/lib/vagrant/action/vm/clear_forwarded_ports.rb +++ b/plugins/providers/virtualbox/action/clear_forwarded_ports.rb @@ -1,6 +1,6 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class ClearForwardedPorts def initialize(app, env) @app = app @@ -8,7 +8,7 @@ module Vagrant def call(env) env[:ui].info I18n.t("vagrant.actions.vm.clear_forward_ports.deleting") - env[:vm].driver.clear_forwarded_ports + env[:machine].provider.driver.clear_forwarded_ports @app.call(env) end diff --git a/lib/vagrant/action/vm/clear_network_interfaces.rb b/plugins/providers/virtualbox/action/clear_network_interfaces.rb similarity index 84% rename from lib/vagrant/action/vm/clear_network_interfaces.rb rename to plugins/providers/virtualbox/action/clear_network_interfaces.rb index 8d27a5d22..0098692f3 100644 --- a/lib/vagrant/action/vm/clear_network_interfaces.rb +++ b/plugins/providers/virtualbox/action/clear_network_interfaces.rb @@ -1,6 +1,6 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class ClearNetworkInterfaces def initialize(app, env) @app = app @@ -21,7 +21,7 @@ module Vagrant # "Enable" all the adapters we setup. env[:ui].info I18n.t("vagrant.actions.vm.clear_network_interfaces.deleting") - env[:vm].driver.enable_adapters(adapters) + env[:machine].provider.driver.enable_adapters(adapters) @app.call(env) end diff --git a/lib/vagrant/action/vm/clear_shared_folders.rb b/plugins/providers/virtualbox/action/clear_shared_folders.rb similarity index 59% rename from lib/vagrant/action/vm/clear_shared_folders.rb rename to plugins/providers/virtualbox/action/clear_shared_folders.rb index 7ebeee543..1aa617ff1 100644 --- a/lib/vagrant/action/vm/clear_shared_folders.rb +++ b/plugins/providers/virtualbox/action/clear_shared_folders.rb @@ -1,14 +1,13 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class ClearSharedFolders def initialize(app, env) @app = app - @env = env end def call(env) - env[:vm].driver.clear_shared_folders + env[:machine].provider.driver.clear_shared_folders @app.call(env) end diff --git a/plugins/providers/virtualbox/action/created.rb b/plugins/providers/virtualbox/action/created.rb new file mode 100644 index 000000000..804801459 --- /dev/null +++ b/plugins/providers/virtualbox/action/created.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Created + def initialize(app, env) + @app = app + end + + def call(env) + # Set the result to be true if the machine is created. + env[:result] = env[:machine].state != :not_created + + # Call the next if we have one (but we shouldn't, since this + # middleware is built to run with the Call-type middlewares) + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/customize.rb b/plugins/providers/virtualbox/action/customize.rb similarity index 73% rename from lib/vagrant/action/vm/customize.rb rename to plugins/providers/virtualbox/action/customize.rb index a8696ec91..ea699ac33 100644 --- a/lib/vagrant/action/vm/customize.rb +++ b/plugins/providers/virtualbox/action/customize.rb @@ -1,24 +1,24 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Customize def initialize(app, env) @app = app end def call(env) - customizations = env[:vm].config.vm.customizations + customizations = env[:machine].config.vm.customizations if !customizations.empty? env[:ui].info I18n.t("vagrant.actions.vm.customize.running") # Execute each customization command. customizations.each do |command| processed_command = command.collect do |arg| - arg = env[:vm].uuid if arg == :id + arg = env[:machine].id if arg == :id arg.to_s end - result = env[:vm].driver.execute_command(processed_command) + result = env[:machine].provider.driver.execute_command(processed_command) if result.exit_code != 0 raise Errors::VMCustomizationFailed, { :command => processed_command.inspect, diff --git a/lib/vagrant/action/vm/default_name.rb b/plugins/providers/virtualbox/action/default_name.rb similarity index 73% rename from lib/vagrant/action/vm/default_name.rb rename to plugins/providers/virtualbox/action/default_name.rb index 3ae69851d..9ef89cf70 100644 --- a/lib/vagrant/action/vm/default_name.rb +++ b/plugins/providers/virtualbox/action/default_name.rb @@ -1,8 +1,8 @@ -require 'log4r' +require "log4r" -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class DefaultName def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::defaultname") @@ -12,7 +12,7 @@ module Vagrant def call(env) @logger.info("Setting the default name of the VM") name = env[:root_path].basename.to_s + "_#{Time.now.to_i}" - env[:vm].driver.set_name(name) + env[:machine].provider.driver.set_name(name) @app.call(env) end diff --git a/lib/vagrant/action/vm/destroy.rb b/plugins/providers/virtualbox/action/destroy.rb similarity index 62% rename from lib/vagrant/action/vm/destroy.rb rename to plugins/providers/virtualbox/action/destroy.rb index 2af9b0a18..dcf8a8201 100644 --- a/lib/vagrant/action/vm/destroy.rb +++ b/plugins/providers/virtualbox/action/destroy.rb @@ -1,6 +1,6 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Destroy def initialize(app, env) @app = app @@ -8,8 +8,8 @@ module Vagrant def call(env) env[:ui].info I18n.t("vagrant.actions.vm.destroy.destroying") - env[:vm].driver.delete - env[:vm].uuid = nil + env[:machine].provider.driver.delete + env[:machine].id = nil @app.call(env) end diff --git a/plugins/providers/virtualbox/action/destroy_confirm.rb b/plugins/providers/virtualbox/action/destroy_confirm.rb new file mode 100644 index 000000000..797f9b737 --- /dev/null +++ b/plugins/providers/virtualbox/action/destroy_confirm.rb @@ -0,0 +1,16 @@ +require "vagrant/action/builtin/confirm" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class DestroyConfirm < Vagrant::Action::Builtin::Confirm + def initialize(app, env) + message = I18n.t("vagrant.commands.destroy.confirmation", + :name => env[:machine].name) + + super(app, env, message) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb b/plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb new file mode 100644 index 000000000..0897ef6dd --- /dev/null +++ b/plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class DestroyUnusedNetworkInterfaces + def initialize(app, env) + @app = app + end + + def call(env) + env[:machine].provider.driver.delete_unused_host_only_networks + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/discard_state.rb b/plugins/providers/virtualbox/action/discard_state.rb new file mode 100644 index 000000000..b19ccbfea --- /dev/null +++ b/plugins/providers/virtualbox/action/discard_state.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class DiscardState + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].provider.state == :saved + env[:ui].info I18n.t("vagrant.actions.vm.discard_state.discarding") + env[:machine].provider.driver.discard_saved_state + end + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/export.rb b/plugins/providers/virtualbox/action/export.rb similarity index 81% rename from lib/vagrant/action/vm/export.rb rename to plugins/providers/virtualbox/action/export.rb index 8228f86df..2bf9f3353 100644 --- a/lib/vagrant/action/vm/export.rb +++ b/plugins/providers/virtualbox/action/export.rb @@ -1,20 +1,19 @@ -require 'fileutils' +require "fileutils" -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Export attr_reader :temp_dir def initialize(app, env) @app = app - @env = env end def call(env) @env = env - raise Errors::VMPowerOffToPackage if @env["vm"].state != :poweroff + raise Vagrant::Errors::VMPowerOffToPackage if @env[:machine].provider.state != :poweroff setup_temp_dir export @@ -38,7 +37,7 @@ module Vagrant def export @env[:ui].info I18n.t("vagrant.actions.vm.export.exporting") - @env[:vm].driver.export(ovf_path) do |progress| + @env[:machine].provider.driver.export(ovf_path) do |progress| @env[:ui].clear_line @env[:ui].report_progress(progress.percent, 100, false) end diff --git a/lib/vagrant/action/vm/forward_ports.rb b/plugins/providers/virtualbox/action/forward_ports.rb similarity index 90% rename from lib/vagrant/action/vm/forward_ports.rb rename to plugins/providers/virtualbox/action/forward_ports.rb index 59c5d2a3f..0afc71115 100644 --- a/lib/vagrant/action/vm/forward_ports.rb +++ b/plugins/providers/virtualbox/action/forward_ports.rb @@ -1,8 +1,8 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class ForwardPorts - def initialize(app,env) + def initialize(app, env) @app = app end @@ -31,7 +31,7 @@ module Vagrant # organize them by their guestport, taking the "last one wins" # approach. guest_port_mapping = {} - @env[:vm].config.vm.forwarded_ports.each do |options| + @env[:machine].config.vm.forwarded_ports.each do |options| guest_port_mapping[options[:guestport]] = options end @@ -53,7 +53,7 @@ module Vagrant def forward_ports(mappings) ports = [] - interfaces = @env[:vm].driver.read_network_interfaces + interfaces = @env[:machine].provider.driver.read_network_interfaces mappings.each do |options| message_attributes = { @@ -83,7 +83,7 @@ module Vagrant if !ports.empty? # We only need to forward ports if there are any to forward - @env[:vm].driver.forward_ports(ports) + @env[:machine].provider.driver.forward_ports(ports) end end end diff --git a/lib/vagrant/action/vm/halt.rb b/plugins/providers/virtualbox/action/halt.rb similarity index 66% rename from lib/vagrant/action/vm/halt.rb rename to plugins/providers/virtualbox/action/halt.rb index 65bc06d0d..42b749acd 100644 --- a/lib/vagrant/action/vm/halt.rb +++ b/plugins/providers/virtualbox/action/halt.rb @@ -1,26 +1,25 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Halt - def initialize(app, env, options=nil) + def initialize(app, env) @app = app - env.merge!(options || {}) end def call(env) - current_state = env[:vm].state + current_state = env[:machine].provider.state if current_state == :running || current_state == :gurumeditation # If the VM is running and we're not forcing, we can # attempt a graceful shutdown - if current_state == :running && !env["force"] + if current_state == :running && !env[:force] env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") - env[:vm].guest.halt + env[:machine].guest.halt end # If we're not powered off now, then force it - if env[:vm].state != :poweroff + if env[:machine].provider.state != :poweroff env[:ui].info I18n.t("vagrant.actions.vm.halt.force") - env[:vm].driver.halt + env[:machine].provider.driver.halt end # Sleep for a second to verify that the VM properly diff --git a/lib/vagrant/action/vm/host_name.rb b/plugins/providers/virtualbox/action/host_name.rb similarity index 61% rename from lib/vagrant/action/vm/host_name.rb rename to plugins/providers/virtualbox/action/host_name.rb index 8df7a9fee..40cc934fe 100644 --- a/lib/vagrant/action/vm/host_name.rb +++ b/plugins/providers/virtualbox/action/host_name.rb @@ -1,6 +1,6 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class HostName def initialize(app, env) @app = app @@ -9,10 +9,10 @@ module Vagrant def call(env) @app.call(env) - host_name = env[:vm].config.vm.host_name + host_name = env[:machine].config.vm.host_name if !host_name.nil? env[:ui].info I18n.t("vagrant.actions.vm.host_name.setting") - env[:vm].guest.change_host_name(host_name) + env[:machine].guest.change_host_name(host_name) end end end diff --git a/lib/vagrant/action/vm/import.rb b/plugins/providers/virtualbox/action/import.rb similarity index 64% rename from lib/vagrant/action/vm/import.rb rename to plugins/providers/virtualbox/action/import.rb index 6e690d24d..64e2507ab 100644 --- a/lib/vagrant/action/vm/import.rb +++ b/plugins/providers/virtualbox/action/import.rb @@ -1,22 +1,18 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Import - # The name for easy reference - def self.name - :import - end - def initialize(app, env) @app = app end def call(env) - env[:ui].info I18n.t("vagrant.actions.vm.import.importing", :name => env[:vm].box.name) + env[:ui].info I18n.t("vagrant.actions.vm.import.importing", + :name => env[:machine].box.name) # Import the virtual machine - ovf_file = env[:vm].box.directory.join("box.ovf").to_s - env[:vm].uuid = env[:vm].driver.import(ovf_file) do |progress| + ovf_file = env[:machine].box.directory.join("box.ovf").to_s + env[:machine].id = env[:machine].provider.driver.import(ovf_file) do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end @@ -30,21 +26,21 @@ module Vagrant return if env[:interrupted] # Flag as erroneous and return if import failed - raise Errors::VMImportFailure if !env[:vm].uuid + raise Vagrant::Errors::VMImportFailure if !env[:machine].id # Import completed successfully. Continue the chain @app.call(env) end def recover(env) - if env[:vm].created? - return if env["vagrant.error"].is_a?(Errors::VagrantError) + if env[:machine].provider.state != :not_created + return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) # Interrupted, destroy the VM. We note that we don't want to # validate the configuration here. destroy_env = env.clone destroy_env[:validate] = false - env[:action_runner].run(:destroy, destroy_env) + env[:action_runner].run(Action.action_destroy, destroy_env) end end end diff --git a/plugins/providers/virtualbox/action/is_running.rb b/plugins/providers/virtualbox/action/is_running.rb new file mode 100644 index 000000000..565b3a735 --- /dev/null +++ b/plugins/providers/virtualbox/action/is_running.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class IsRunning + def initialize(app, env) + @app = app + end + + def call(env) + # Set the result to be true if the machine is running. + env[:result] = env[:machine].state == :running + + # Call the next if we have one (but we shouldn't, since this + # middleware is built to run with the Call-type middlewares) + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/is_saved.rb b/plugins/providers/virtualbox/action/is_saved.rb new file mode 100644 index 000000000..749f64964 --- /dev/null +++ b/plugins/providers/virtualbox/action/is_saved.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class IsSaved + def initialize(app, env) + @app = app + end + + def call(env) + # Set the result to be true if the machine is saved. + env[:result] = env[:machine].state == :saved + + # Call the next if we have one (but we shouldn't, since this + # middleware is built to run with the Call-type middlewares) + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/match_mac_address.rb b/plugins/providers/virtualbox/action/match_mac_address.rb similarity index 57% rename from lib/vagrant/action/vm/match_mac_address.rb rename to plugins/providers/virtualbox/action/match_mac_address.rb index 70da6b2a4..a66f40bad 100644 --- a/lib/vagrant/action/vm/match_mac_address.rb +++ b/plugins/providers/virtualbox/action/match_mac_address.rb @@ -1,17 +1,17 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class MatchMACAddress def initialize(app, env) @app = app end def call(env) - raise Errors::VMBaseMacNotSpecified if !env[:vm].config.vm.base_mac + raise Vagrant::Errors::VMBaseMacNotSpecified if !env[:machine].config.vm.base_mac # Create the proc which we want to use to modify the virtual machine env[:ui].info I18n.t("vagrant.actions.vm.match_mac.matching") - env[:vm].driver.set_mac_address(env[:vm].config.vm.base_mac) + env[:machine].provider.driver.set_mac_address(env[:machine].config.vm.base_mac) @app.call(env) end diff --git a/plugins/providers/virtualbox/action/message_not_created.rb b/plugins/providers/virtualbox/action/message_not_created.rb new file mode 100644 index 000000000..1bd792c9e --- /dev/null +++ b/plugins/providers/virtualbox/action/message_not_created.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MessageNotCreated + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.commands.common.vm_not_created") + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/message_not_running.rb b/plugins/providers/virtualbox/action/message_not_running.rb new file mode 100644 index 000000000..fdfff199e --- /dev/null +++ b/plugins/providers/virtualbox/action/message_not_running.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MessageNotRunning + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.commands.common.vm_not_running") + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/message_will_not_destroy.rb b/plugins/providers/virtualbox/action/message_will_not_destroy.rb new file mode 100644 index 000000000..a80bea3c9 --- /dev/null +++ b/plugins/providers/virtualbox/action/message_will_not_destroy.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class MessageWillNotDestroy + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", + :name => env[:machine].name) + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/network.rb b/plugins/providers/virtualbox/action/network.rb similarity index 91% rename from lib/vagrant/action/vm/network.rb rename to plugins/providers/virtualbox/action/network.rb index f10d5d957..31a6fa38d 100644 --- a/lib/vagrant/action/vm/network.rb +++ b/plugins/providers/virtualbox/action/network.rb @@ -1,17 +1,15 @@ -require 'set' +require "set" -require 'log4r' +require "log4r" -require 'vagrant/util/network_ip' +require "vagrant/util/network_ip" -module Vagrant - module Action - module VM - # This action handles all `config.vm.network` configurations by - # setting up the VM properly and enabling the networks afterword. +module VagrantPlugins + module ProviderVirtualBox + module Action class Network # Utilities to deal with network addresses - include Util::NetworkIP + include Vagrant::Util::NetworkIP def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::network") @@ -28,7 +26,7 @@ module Vagrant @logger.debug("Determining adapters and networks...") adapters = [] networks = [] - env[:vm].config.vm.networks.each do |type, args| + env[:machine].config.vm.networks.each do |type, args| # Get the normalized configuration we'll use around config = send("#{type}_config", args) @@ -54,7 +52,7 @@ module Vagrant # Create all the network interfaces @logger.info("Enabling adapters...") env[:ui].info I18n.t("vagrant.actions.vm.network.preparing") - env[:vm].driver.enable_adapters(adapters) + env[:machine].provider.driver.enable_adapters(adapters) end # Continue the middleware chain. We're done with our VM @@ -69,7 +67,7 @@ module Vagrant # want to configure the networks that have `auto_config` setup. networks_to_configure = networks.select { |n| n[:_auto_config] } env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") - env[:vm].guest.configure_networks(networks_to_configure) + env[:machine].guest.configure_networks(networks_to_configure) end end @@ -86,7 +84,7 @@ module Vagrant available = Set.new(1..8) # Determine which NICs are actually available. - interfaces = @env[:vm].driver.read_network_interfaces + interfaces = @env[:machine].provider.driver.read_network_interfaces interfaces.each do |number, nic| # Remove the number from the available NICs if the # NIC is in use. @@ -102,7 +100,7 @@ module Vagrant if !adapter[:adapter] # If we have no available adapters, then that is an exceptional # event. - raise Errors::NetworkNoAdapters if available.empty? + raise Vagrant::Errors::NetworkNoAdapters if available.empty? # Otherwise, assign as the adapter the next available item adapter[:adapter] = available.shift @@ -116,7 +114,7 @@ module Vagrant # Verify that there are no collisions in the adapters being used. used = Set.new adapters.each do |adapter| - raise Errors::NetworkAdapterCollision if used.include?(adapter[:adapter]) + raise Vagrant::Errors::NetworkAdapterCollision if used.include?(adapter[:adapter]) used.add(adapter[:adapter]) end end @@ -134,7 +132,7 @@ module Vagrant adapter_to_interface = {} # Make a first pass to assign interface numbers by adapter location - vm_adapters = @env[:vm].driver.read_network_interfaces + vm_adapters = @env[:machine].provider.driver.read_network_interfaces vm_adapters.sort.each do |number, adapter| if adapter[:type] != :none # Not used, so assign the interface number and increment @@ -220,7 +218,7 @@ module Vagrant # It is an error case if a specific name was given but the network # doesn't exist. if config[:name] - raise Errors::NetworkNotFound, :name => config[:name] + raise Vagrant::Errors::NetworkNotFound, :name => config[:name] end # Otherwise, we create a new network and put the net network @@ -238,13 +236,13 @@ module Vagrant interface[:dhcp][:lower] == config[:dhcp_lower] && interface[:dhcp][:upper] == config[:dhcp_upper] - raise Errors::NetworkDHCPAlreadyAttached if !valid + raise Vagrant::Errors::NetworkDHCPAlreadyAttached if !valid @logger.debug("DHCP server already properly configured") else # Configure the DHCP server for the network. @logger.debug("Creating a DHCP server...") - @env[:vm].driver.create_dhcp_server(interface[:name], config) + @env[:machine].provider.driver.create_dhcp_server(interface[:name], config) end end @@ -274,7 +272,7 @@ module Vagrant options = config.dup options[:ip] = options[:adapter_ip] - @env[:vm].driver.create_host_only_network(options) + @env[:machine].provider.driver.create_host_only_network(options) end # Finds a host only network that matches our configuration on VirtualBox. @@ -282,7 +280,7 @@ module Vagrant def find_matching_hostonly_network(config) this_netaddr = network_address(config[:ip], config[:netmask]) - @env[:vm].driver.read_host_only_interfaces.each do |interface| + @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| if config[:name] && config[:name] == interface[:name] return interface elsif this_netaddr == network_address(interface[:ip], interface[:netmask]) @@ -302,9 +300,9 @@ module Vagrant def verify_no_bridge_collision(options) this_netaddr = network_address(options[:ip], options[:netmask]) - @env[:vm].driver.read_bridged_interfaces.each do |interface| + @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| that_netaddr = network_address(interface[:ip], interface[:netmask]) - raise Errors::NetworkCollision if this_netaddr == that_netaddr && interface[:status] != "Down" + raise Vagrant::Errors::NetworkCollision if this_netaddr == that_netaddr && interface[:status] != "Down" end end @@ -323,7 +321,7 @@ module Vagrant def bridged_adapter(config) # Find the bridged interfaces that are available - bridgedifs = @env[:vm].driver.read_bridged_interfaces + bridgedifs = @env[:machine].provider.driver.read_bridged_interfaces bridgedifs.delete_if { |interface| interface[:status] == "Down" } # The name of the chosen bridge interface will be assigned to this diff --git a/lib/vagrant/action/vm/nfs.rb b/plugins/providers/virtualbox/action/nfs.rb similarity index 78% rename from lib/vagrant/action/vm/nfs.rb rename to plugins/providers/virtualbox/action/nfs.rb index 8aa81d31b..90eaae320 100644 --- a/lib/vagrant/action/vm/nfs.rb +++ b/plugins/providers/virtualbox/action/nfs.rb @@ -4,21 +4,9 @@ require 'pathname' require 'log4r' -module Vagrant - module Action - module VM - # Enables NFS based shared folders. `nfsd` must already be installed - # on the host machine, and NFS client must already be installed on - # the guest machine. - # - # This is a two part process: - # - # 1. Adds an entry to `/etc/exports` on the host machine using - # the host class to export the proper folder to the proper - # machine. - # 2. After boot, runs `mount` on the guest to mount the shared - # folder. - # +module VagrantPlugins + module ProviderVirtualBox + module Action class NFS def initialize(app,env) @logger = Log4r::Logger.new("vagrant::action::vm::nfs") @@ -54,7 +42,7 @@ module Vagrant def extract_folders # Load the NFS enabled shared folders @folders = {} - @env[:vm].config.vm.shared_folders.each do |key, opts| + @env[:machine].config.vm.shared_folders.each do |key, opts| if opts[:nfs] # Duplicate the options, set the hostpath, and set disabled on the original # options so the ShareFolders middleware doesn't try to mount it. @@ -68,7 +56,8 @@ module Vagrant begin FileUtils.mkpath(hostpath) rescue Errno::EACCES - raise Errors::SharedFolderCreateFailed, :path => hostpath.to_s + raise Vagrant::Errors::SharedFolderCreateFailed, + :path => hostpath.to_s end end @@ -112,7 +101,7 @@ module Vagrant # The options on the hash get priority, then the default # values - value = opts.has_key?(key) ? opts[key] : @env[:vm].config.nfs.send(key) + value = opts.has_key?(key) ? opts[key] : @env[:machine].config.nfs.send(key) return value if value != :auto # Get UID/GID from folder if we've made it this far @@ -126,7 +115,7 @@ module Vagrant # up to the host class to define the specific behavior. def export_folders @env[:ui].info I18n.t("vagrant.actions.vm.nfs.exporting") - @env[:host].nfs_export(@env[:vm].uuid, guest_ip, folders) + @env[:host].nfs_export(@env[:machine].id, guest_ip, folders) end # Uses the system class to mount the NFS folders. @@ -141,16 +130,16 @@ module Vagrant end end - @env[:vm].guest.mount_nfs(host_ip, mount_folders) + @env[:machine].guest.mount_nfs(host_ip, mount_folders) end # Returns the IP address of the first host only network adapter # # @return [String] def host_ip - @env[:vm].driver.read_network_interfaces.each do |adapter, opts| + @env[:machine].provider.driver.read_network_interfaces.each do |adapter, opts| if opts[:type] == :hostonly - @env[:vm].driver.read_host_only_interfaces.each do |interface| + @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| if interface[:name] == opts[:hostonly] return interface[:ip] end @@ -166,7 +155,7 @@ module Vagrant # # @return [String] def guest_ip - @env[:vm].config.vm.networks.each do |type, args| + @env[:machine].config.vm.networks.each do |type, args| if type == :hostonly && args[0].is_a?(String) return args[0] end @@ -177,7 +166,7 @@ module Vagrant # Checks if there are any NFS enabled shared folders. def nfs_enabled? - @env[:vm].config.vm.shared_folders.each do |key, opts| + @env[:machine].config.vm.shared_folders.each do |key, opts| return true if opts[:nfs] end @@ -186,9 +175,9 @@ module Vagrant # Verifies that the host is set and supports NFS. def verify_settings - raise Errors::NFSHostRequired if @env[:host].nil? - raise Errors::NFSNotSupported if !@env[:host].nfs? - raise Errors::NFSNoHostNetwork if !guest_ip + raise Vagrant::Errors::NFSHostRequired if @env[:host].nil? + raise Vagrant::Errors::NFSNotSupported if !@env[:host].nfs? + raise Vagrant::Errors::NFSNoHostNetwork if !guest_ip end end end diff --git a/lib/vagrant/action/vm/package.rb b/plugins/providers/virtualbox/action/package.rb similarity index 66% rename from lib/vagrant/action/vm/package.rb rename to plugins/providers/virtualbox/action/package.rb index d392d6514..5a6c581ce 100644 --- a/lib/vagrant/action/vm/package.rb +++ b/plugins/providers/virtualbox/action/package.rb @@ -1,12 +1,9 @@ require 'vagrant/action/general/package' -module Vagrant - module Action - module VM - # A subclass of {General::Package} which simply makes sure that - # the package directory is set to the directory which the VM - # was exported to. - class Package < General::Package +module VagrantPlugins + module ProviderVirtualBox + module Action + class Package < Vagrant::Action::General::Package # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call diff --git a/lib/vagrant/action/vm/package_vagrantfile.rb b/plugins/providers/virtualbox/action/package_vagrantfile.rb similarity index 73% rename from lib/vagrant/action/vm/package_vagrantfile.rb rename to plugins/providers/virtualbox/action/package_vagrantfile.rb index 17869091f..0517718ee 100644 --- a/lib/vagrant/action/vm/package_vagrantfile.rb +++ b/plugins/providers/virtualbox/action/package_vagrantfile.rb @@ -1,17 +1,14 @@ require 'vagrant/util/template_renderer' -module Vagrant - module Action - module VM - # Puts a generated Vagrantfile into the package directory so that - # it can be included in the package. +module VagrantPlugins + module ProviderVirtualBox + module Action class PackageVagrantfile # For TemplateRenderer - include Util + include Vagrant::Util def initialize(app, env) @app = app - @env = env end def call(env) @@ -26,7 +23,7 @@ module Vagrant def create_vagrantfile File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| f.write(TemplateRenderer.render("package_Vagrantfile", { - :base_mac => @env["vm"].driver.read_mac_address + :base_mac => @env[:machine].provider.driver.read_mac_address })) end end diff --git a/lib/vagrant/action/vm/provision.rb b/plugins/providers/virtualbox/action/provision.rb similarity index 92% rename from lib/vagrant/action/vm/provision.rb rename to plugins/providers/virtualbox/action/provision.rb index 56be8a8e6..d2304c609 100644 --- a/lib/vagrant/action/vm/provision.rb +++ b/plugins/providers/virtualbox/action/provision.rb @@ -1,8 +1,8 @@ require "log4r" -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Provision def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::provision") @@ -19,6 +19,7 @@ module Vagrant # We set this here so that even if this value is changed in the future, # it stays constant to what we expect here in this moment. enabled = env["provision.enabled"] + # Instantiate and prepare the provisioners. Preparation must happen here # so that shared folders and such can properly take effect. provisioners = enabled_provisioners @@ -38,7 +39,7 @@ module Vagrant def enabled_provisioners enabled = [] - @env[:vm].config.vm.provisioners.each do |provisioner| + @env[:machine].config.vm.provisioners.each do |provisioner| if @env["provision.types"] # If we've specified types of provisioners to enable, then we # only use those provisioners, and skip any that we haven't diff --git a/plugins/providers/virtualbox/action/provisioner_cleanup.rb b/plugins/providers/virtualbox/action/provisioner_cleanup.rb new file mode 100644 index 000000000..c97e3334e --- /dev/null +++ b/plugins/providers/virtualbox/action/provisioner_cleanup.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ProvisionerCleanup + def initialize(app, env) + @app = app + end + + def call(env) + # Instantiate all the enabled provisioners + provisioners = env[:machine].config.vm.provisioners.map do |provisioner| + provisioner.provisioner.new(env, provisioner.config) + end + + # Call cleanup on each + provisioners.each do |instance| + instance.cleanup + end + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/vm/prune_nfs_exports.rb b/plugins/providers/virtualbox/action/prune_nfs_exports.rb similarity index 66% rename from lib/vagrant/action/vm/prune_nfs_exports.rb rename to plugins/providers/virtualbox/action/prune_nfs_exports.rb index b8b3b3061..e74f176fc 100644 --- a/lib/vagrant/action/vm/prune_nfs_exports.rb +++ b/plugins/providers/virtualbox/action/prune_nfs_exports.rb @@ -1,6 +1,6 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class PruneNFSExports def initialize(app, env) @app = app @@ -8,7 +8,7 @@ module Vagrant def call(env) if env[:host] - valid_ids = env[:vm].driver.read_vms + valid_ids = env[:machine].provider.driver.read_vms env[:host].nfs_prune(valid_ids) end diff --git a/lib/vagrant/action/vm/resume.rb b/plugins/providers/virtualbox/action/resume.rb similarity index 71% rename from lib/vagrant/action/vm/resume.rb rename to plugins/providers/virtualbox/action/resume.rb index bf62c8b3e..9967b91fe 100644 --- a/lib/vagrant/action/vm/resume.rb +++ b/plugins/providers/virtualbox/action/resume.rb @@ -1,13 +1,13 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Resume def initialize(app, env) @app = app end def call(env) - if env[:vm].state == :saved + if env[:machine].provider.state == :saved env[:ui].info I18n.t("vagrant.actions.vm.resume.resuming") env[:action_runner].run(Boot, env) end diff --git a/lib/vagrant/action/vm/sane_defaults.rb b/plugins/providers/virtualbox/action/sane_defaults.rb similarity index 82% rename from lib/vagrant/action/vm/sane_defaults.rb rename to plugins/providers/virtualbox/action/sane_defaults.rb index f2dd31ab9..21d8e9055 100644 --- a/lib/vagrant/action/vm/sane_defaults.rb +++ b/plugins/providers/virtualbox/action/sane_defaults.rb @@ -1,11 +1,8 @@ require "log4r" -module Vagrant - module Action - module VM - # This middleware enforces some sane defaults on the virtualbox - # VM which help with performance, stability, and in some cases - # behavior. +module VagrantPlugins + module ProviderVirtualBox + module Action class SaneDefaults def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::sanedefaults") @@ -22,7 +19,7 @@ module Vagrant # errors. The Host IO cache vastly improves disk IO performance # for VMs. command = [ - "storagectl", env[:vm].uuid, + "storagectl", env[:machine].id, "--name", "SATA Controller", "--hostiocache", "on" ] @@ -46,14 +43,14 @@ module Vagrant # Enable/disable the NAT DNS proxy as necessary if enable_dns_proxy command = [ - "modifyvm", env[:vm].uuid, + "modifyvm", env[:machine].id, "--natdnsproxy1", "on" ] attempt_and_log(command, "Enable the NAT DNS proxy on adapter 1...") else - command = [ "modifyvm", env[:vm].uuid, "--natdnsproxy1", "off" ] + command = [ "modifyvm", env[:machine].id, "--natdnsproxy1", "off" ] attempt_and_log(command, "Disable the NAT DNS proxy on adapter 1...") - command = [ "modifyvm", env[:vm].uuid, "--natdnshostresolver1", "off" ] + command = [ "modifyvm", env[:machine].id, "--natdnshostresolver1", "off" ] attempt_and_log(command, "Disable the NAT DNS resolver on adapter 1...") end @@ -69,7 +66,7 @@ module Vagrant # @param [Array] command Command to run # @param [String] log Log message to write. def attempt_and_log(command, log) - result = @env[:vm].driver.execute_command(command) + result = @env[:machine].provider.driver.execute_command(command) @logger.info("#{log} (exit status = #{result.exit_code})") end end diff --git a/lib/vagrant/action/vm/setup_package_files.rb b/plugins/providers/virtualbox/action/setup_package_files.rb similarity index 82% rename from lib/vagrant/action/vm/setup_package_files.rb rename to plugins/providers/virtualbox/action/setup_package_files.rb index f45210707..93e1fdd89 100644 --- a/lib/vagrant/action/vm/setup_package_files.rb +++ b/plugins/providers/virtualbox/action/setup_package_files.rb @@ -1,10 +1,6 @@ -require 'pathname' - -module Vagrant - module Action - module VM - # Sets up the mapping of files to copy into the package and verifies - # that the files can be properly copied. +module VagrantPlugins + module ProviderVirtualBox + module Action class SetupPackageFiles def initialize(app, env) @app = app @@ -40,7 +36,8 @@ module Vagrant # Verify the mapping files.each do |from, _| - raise Errors::PackageIncludeMissing, :file => from if !File.exist?(from) + raise Vagrant::Errors::PackageIncludeMissing, + :file => from if !File.exist?(from) end # Save the mapping diff --git a/lib/vagrant/action/vm/share_folders.rb b/plugins/providers/virtualbox/action/share_folders.rb similarity index 84% rename from lib/vagrant/action/vm/share_folders.rb rename to plugins/providers/virtualbox/action/share_folders.rb index 29dc4ccf8..a68beb926 100644 --- a/lib/vagrant/action/vm/share_folders.rb +++ b/plugins/providers/virtualbox/action/share_folders.rb @@ -1,10 +1,10 @@ -require 'pathname' +require "pathname" -require 'log4r' +require "log4r" -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class ShareFolders def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::share_folders") @@ -25,7 +25,7 @@ module Vagrant # This method returns an actual list of VirtualBox shared # folders to create and their proper path. def shared_folders - @env[:vm].config.vm.shared_folders.inject({}) do |acc, data| + @env[:machine].config.vm.shared_folders.inject({}) do |acc, data| key, value = data next acc if value[:disabled] @@ -50,7 +50,8 @@ module Vagrant begin hostpath.mkpath rescue Errno::EACCES - raise Errors::SharedFolderCreateFailed, :path => hostpath.to_s + raise Vagrant::Errors::SharedFolderCreateFailed, + :path => hostpath.to_s end end end @@ -68,7 +69,7 @@ module Vagrant } end - @env[:vm].driver.share_folders(folders) + @env[:machine].provider.driver.share_folders(folders) end def mount_shared_folders @@ -96,11 +97,11 @@ module Vagrant data = data.dup # Calculate the owner and group - data[:owner] ||= @env[:vm].config.ssh.username - data[:group] ||= @env[:vm].config.ssh.username + data[:owner] ||= @env[:machine].config.ssh.username + data[:group] ||= @env[:machine].config.ssh.username # Mount the actual folder - @env[:vm].guest.mount_shared_folder(name, data[:guestpath], data) + @env[:machine].guest.mount_shared_folder(name, data[:guestpath], data) else # If no guest path is specified, then automounting is disabled @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", diff --git a/lib/vagrant/action/vm/suspend.rb b/plugins/providers/virtualbox/action/suspend.rb similarity index 60% rename from lib/vagrant/action/vm/suspend.rb rename to plugins/providers/virtualbox/action/suspend.rb index 8f39293eb..9a9b7a94b 100644 --- a/lib/vagrant/action/vm/suspend.rb +++ b/plugins/providers/virtualbox/action/suspend.rb @@ -1,15 +1,15 @@ -module Vagrant - module Action - module VM +module VagrantPlugins + module ProviderVirtualBox + module Action class Suspend def initialize(app, env) @app = app end def call(env) - if env[:vm].state == :running + if env[:machine].provider.state == :running env[:ui].info I18n.t("vagrant.actions.vm.suspend.suspending") - env[:vm].driver.suspend + env[:machine].provider.driver.suspend end @app.call(env) diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb new file mode 100644 index 000000000..1b503e950 --- /dev/null +++ b/plugins/providers/virtualbox/driver/base.rb @@ -0,0 +1,327 @@ +require 'log4r' + +require 'vagrant/util/busy' +require 'vagrant/util/platform' +require 'vagrant/util/retryable' +require 'vagrant/util/subprocess' + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Base class for all VirtualBox drivers. + # + # This class provides useful tools for things such as executing + # VBoxManage and handling SIGINTs and so on. + class Base + # Include this so we can use `Subprocess` more easily. + include Vagrant::Util::Retryable + + def initialize + @logger = Log4r::Logger.new("vagrant::provider::virtualbox::base") + + # This flag is used to keep track of interrupted state (SIGINT) + @interrupted = false + + # Set the path to VBoxManage + @vboxmanage_path = "VBoxManage" + + if Vagrant::Util::Platform.windows? + @logger.debug("Windows. Trying VBOX_INSTALL_PATH for VBoxManage") + + # On Windows, we use the VBOX_INSTALL_PATH environmental + # variable to find VBoxManage. + if ENV.has_key?("VBOX_INSTALL_PATH") + # Get the path. + path = ENV["VBOX_INSTALL_PATH"] + @logger.debug("VBOX_INSTALL_PATH value: #{path}") + + # There can actually be multiple paths in here, so we need to + # split by the separator ";" and see which is a good one. + path.split(";").each do |single| + # Make sure it ends with a \ + single += "\\" if !single.end_with?("\\") + + # If the executable exists, then set it as the main path + # and break out + vboxmanage = "#{path}VBoxManage.exe" + if File.file?(vboxmanage) + @vboxmanage_path = vboxmanage + break + end + end + end + end + + @logger.info("VBoxManage path: #{@vboxmanage_path}") + end + + # Clears the forwarded ports that have been set on the virtual machine. + def clear_forwarded_ports + end + + # Clears the shared folders that have been set on the virtual machine. + def clear_shared_folders + end + + # Creates a DHCP server for a host only network. + # + # @param [String] network Name of the host-only network. + # @param [Hash] options Options for the DHCP server. + def create_dhcp_server(network, options) + end + + # Creates a host only network with the given options. + # + # @param [Hash] options Options to create the host only network. + # @return [Hash] The details of the host only network, including + # keys `:name`, `:ip`, and `:netmask` + def create_host_only_network(options) + end + + # Deletes the virtual machine references by this driver. + def delete + end + + # Deletes any host only networks that aren't being used for anything. + def delete_unused_host_only_networks + end + + # Discards any saved state associated with this VM. + def discard_saved_state + end + + # Enables network adapters on the VM. + # + # The format of each adapter specification should be like so: + # + # { + # :type => :hostonly, + # :hostonly => "vboxnet0", + # :mac_address => "tubes" + # } + # + # This must support setting up both host only and bridged networks. + # + # @param [Array] adapters Array of adapters to enable. + def enable_adapters(adapters) + end + + # Execute a raw command straight through to VBoxManage. + # + # @param [Array] command Command to execute. + def execute_command(command) + end + + # Exports the virtual machine to the given path. + # + # @param [String] path Path to the OVF file. + # @yield [progress] Yields the block with the progress of the export. + def export(path) + end + + # Forwards a set of ports for a VM. + # + # This will not affect any previously set forwarded ports, + # so be sure to delete those if you need to. + # + # The format of each port hash should be the following: + # + # { + # :name => "foo", + # :hostport => 8500, + # :guestport => 80, + # :adapter => 1, + # :protocol => "tcp" + # } + # + # Note that "adapter" and "protocol" are optional and will default + # to 1 and "tcp" respectively. + # + # @param [Array] ports An array of ports to set. See documentation + # for more information on the format. + def forward_ports(ports) + end + + # Halts the virtual machine (pulls the plug). + def halt + end + + # Imports the VM from an OVF file. + # + # @param [String] ovf Path to the OVF file. + # @return [String] UUID of the imported VM. + def import(ovf) + end + + # Returns a list of forwarded ports for a VM. + # + # @param [String] uuid UUID of the VM to read from, or `nil` if this + # VM. + # @param [Boolean] active_only If true, only VMs that are running will + # be checked. + # @return [Array] + def read_forwarded_ports(uuid=nil, active_only=false) + end + + # Returns a list of bridged interfaces. + # + # @return [Hash] + def read_bridged_interfaces + end + + # Returns the guest additions version that is installed on this VM. + # + # @return [String] + def read_guest_additions_version + end + + # Returns a list of available host only interfaces. + # + # @return [Hash] + def read_host_only_interfaces + end + + # Returns the MAC address of the first network interface. + # + # @return [String] + def read_mac_address + end + + # Returns the folder where VirtualBox places it's VMs. + # + # @return [String] + def read_machine_folder + end + + # Returns a list of network interfaces of the VM. + # + # @return [Hash] + def read_network_interfaces + end + + # Returns the current state of this VM. + # + # @return [Symbol] + def read_state + end + + # Returns a list of all forwarded ports in use by active + # virtual machines. + # + # @return [Array] + def read_used_ports + end + + # Returns a list of all UUIDs of virtual machines currently + # known by VirtualBox. + # + # @return [Array] + def read_vms + end + + # Sets the MAC address of the first network adapter. + # + # @param [String] mac MAC address without any spaces/hyphens. + def set_mac_address(mac) + end + + # Share a set of folders on this VM. + # + # @param [Array] folders + def share_folders(folders) + end + + # Reads the SSH port of this VM. + # + # @param [Integer] expected Expected guest port of SSH. + def ssh_port(expected) + end + + # Starts the virtual machine. + # + # @param [String] mode Mode to boot the VM. Either "headless" + # or "gui" + def start(mode) + end + + # Suspend the virtual machine. + def suspend + end + + # Verifies that the driver is ready to accept work. + # + # This should raise a VagrantError if things are not ready. + def verify! + end + + # Verifies that an image can be imported properly. + # + # @param [String] path Path to an OVF file. + # @return [Boolean] + def verify_image(path) + end + + # Checks if a VM with the given UUID exists. + # + # @return [Boolean] + def vm_exists?(uuid) + end + + # Execute the given subcommand for VBoxManage and return the output. + def execute(*command, &block) + # Get the options hash if it exists + opts = {} + opts = command.pop if command.last.is_a?(Hash) + + tries = 0 + tries = 3 if opts[:retryable] + + # Variable to store our execution result + r = nil + + retryable(:on => Vagrant::Errors::VBoxManageError, :tries => tries, :sleep => 1) do + # Execute the command + r = raw(*command, &block) + + # If the command was a failure, then raise an exception that is + # nicely handled by Vagrant. + if r.exit_code != 0 + if @interrupted + @logger.info("Exit code != 0, but interrupted. Ignoring.") + else + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + else + # Sometimes, VBoxManage fails but doesn't actual return a non-zero + # exit code. For this we inspect the output and determine if an error + # occurred. + if r.stderr =~ /VBoxManage: error:/ + @logger.info("VBoxManage error text found, assuming error.") + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + end + end + + # Return the output, making sure to replace any Windows-style + # newlines with Unix-style. + r.stdout.gsub("\r\n", "\n") + end + + # Executes a command and returns the raw result object. + def raw(*command, &block) + int_callback = lambda do + @interrupted = true + @logger.info("Interrupted.") + end + + # Append in the options for subprocess + command << { :notify => [:stdout, :stderr] } + + Vagrant::Util::Busy.busy(int_callback) do + Vagrant::Util::Subprocess.execute(@vboxmanage_path, *command, &block) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb new file mode 100644 index 000000000..1cf76436e --- /dev/null +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -0,0 +1,139 @@ +require "forwardable" + +require "log4r" + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + class Meta < Base + # This is raised if the VM is not found when initializing a driver + # with a UUID. + class VMNotFound < StandardError; end + + # We use forwardable to do all our driver forwarding + extend Forwardable + + # The UUID of the virtual machine we represent + attr_reader :uuid + + # The version of virtualbox that is running. + attr_reader :version + + def initialize(uuid=nil) + # Setup the base + super() + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox::meta") + @uuid = uuid + + # Read and assign the version of VirtualBox we know which + # specific driver to instantiate. + begin + @version = read_version || "" + rescue Vagrant::Util::Subprocess::LaunchError + # This means that VirtualBox was not found, so we raise this + # error here. + raise Vagrant::Errors::VirtualBoxNotDetected + end + + # Instantiate the proper version driver for VirtualBox + @logger.debug("Finding driver for VirtualBox version: #{@version}") + driver_map = { + "4.0" => Version_4_0, + "4.1" => Version_4_1 + } + + driver_klass = nil + driver_map.each do |key, klass| + if @version.start_with?(key) + driver_klass = klass + break + end + end + + if !driver_klass + supported_versions = driver_map.keys.sort.join(", ") + raise Vagrant::Errors::VirtualBoxInvalidVersion, :supported_versions => supported_versions + end + + @logger.info("Using VirtualBox driver: #{driver_klass}") + @driver = driver_klass.new(@uuid) + + if @uuid + # Verify the VM exists, and if it doesn't, then don't worry + # about it (mark the UUID as nil) + raise VMNotFound if !@driver.vm_exists?(@uuid) + end + end + + def_delegators :@driver, :clear_forwarded_ports, + :clear_shared_folders, + :create_dhcp_server, + :create_host_only_network, + :delete, + :delete_unused_host_only_networks, + :discard_saved_state, + :enable_adapters, + :execute_command, + :export, + :forward_ports, + :halt, + :import, + :read_forwarded_ports, + :read_bridged_interfaces, + :read_guest_additions_version, + :read_host_only_interfaces, + :read_mac_address, + :read_mac_addresses, + :read_machine_folder, + :read_network_interfaces, + :read_state, + :read_used_ports, + :read_vms, + :set_mac_address, + :set_name, + :share_folders, + :ssh_port, + :start, + :suspend, + :verify!, + :verify_image, + :vm_exists? + + protected + + # This returns the version of VirtualBox that is running. + # + # @return [String] + def read_version + # The version string is usually in one of the following formats: + # + # * 4.1.8r1234 + # * 4.1.8r1234_OSE + # * 4.1.8_MacPortsr1234 + # + # Below accounts for all of these. + + # Note: We split this into multiple lines because apparently "".split("_") + # is [], so we have to check for an empty array in between. + output = execute("--version") + if output =~ /vboxdrv kernel module is not loaded/ + raise Vagrant::Errors::VirtualBoxKernelModuleNotLoaded + elsif output =~ /Please install/ + # Check for installation incomplete warnings, for example: + # "WARNING: The character device /dev/vboxdrv does not + # exist. Please install the virtualbox-ose-dkms package and + # the appropriate headers, most likely linux-headers-generic." + raise Vagrant::Errors::VirtualBoxInstallIncomplete + end + + parts = output.split("_") + return nil if parts.empty? + parts[0].split("r")[0] + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/version_4_0.rb b/plugins/providers/virtualbox/driver/version_4_0.rb new file mode 100644 index 000000000..bbf0cde83 --- /dev/null +++ b/plugins/providers/virtualbox/driver/version_4_0.rb @@ -0,0 +1,476 @@ +require 'log4r' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Driver for VirtualBox 4.0.x + class Version_4_0 < Base + def initialize(uuid) + super() + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_0") + @uuid = uuid + end + + def clear_forwarded_ports + args = [] + read_forwarded_ports(@uuid).each do |nic, name, _, _| + args.concat(["--natpf#{nic}", "delete", name]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def clear_shared_folders + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if name = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] + execute("sharedfolder", "remove", @uuid, "--name", name) + end + end + end + + def create_dhcp_server(network, options) + execute("dhcpserver", "add", "--ifname", network, + "--ip", options[:dhcp_ip], + "--netmask", options[:netmask], + "--lowerip", options[:dhcp_lower], + "--upperip", options[:dhcp_upper], + "--enable") + end + + def create_host_only_network(options) + # Create the interface + interface = execute("hostonlyif", "create") + name = interface[/^Interface '(.+?)' was successfully created$/, 1] + + # Configure it + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + + # Return the details + return { + :name => name, + :ip => options[:adapter_ip], + :netmask => options[:netmask], + :dhcp => nil + } + end + + def delete + execute("unregistervm", @uuid, "--delete") + end + + def delete_unused_host_only_networks + networks = [] + execute("list", "hostonlyifs").split("\n").each do |line| + if network_name = line[/^Name:\s+(.+?)$/, 1] + networks << network_name + end + end + + execute("list", "vms").split("\n").each do |line| + if vm_name = line[/^".+?"\s+\{(.+?)\}$/, 1] + info = execute("showvminfo", vm_name, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] + networks.delete(network_name) + end + end + end + end + + networks.each do |name| + # First try to remove any DHCP servers attached. We use `raw` because + # it is okay if this fails. It usually means that a DHCP server was + # never attached. + raw("dhcpserver", "remove", "--ifname", name) + + # Delete the actual host only network interface. + execute("hostonlyif", "remove", name) + end + end + + def discard_saved_state + execute("discardstate", @uuid) + end + + def enable_adapters(adapters) + args = [] + adapters.each do |adapter| + args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) + + if adapter[:bridge] + args.concat(["--bridgeadapter#{adapter[:adapter]}", + adapter[:bridge]]) + end + + if adapter[:hostonly] + args.concat(["--hostonlyadapter#{adapter[:adapter]}", + adapter[:hostonly]]) + end + + if adapter[:mac_address] + args.concat(["--macaddress#{adapter[:adapter]}", + adapter[:mac_address]]) + end + + if adapter[:nic_type] + args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) + end + end + + execute("modifyvm", @uuid, *args) + end + + def execute_command(command) + raw(*command) + end + + def export(path) + # TODO: Progress + execute("export", @uuid, "--output", path.to_s) + end + + def forward_ports(ports) + args = [] + ports.each do |options| + pf_builder = [options[:name], + options[:protocol] || "tcp", + "", + options[:hostport], + "", + options[:guestport]] + + args.concat(["--natpf#{options[:adapter] || 1}", + pf_builder.join(",")]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def halt + execute("controlvm", @uuid, "poweroff") + end + + def import(ovf) + output = "" + total = "" + last = 0 + execute("import", ovf) do |type, data| + if type == :stdout + # Keep track of the stdout so that we can get the VM name + output << data + elsif type == :stderr + # Append the data so we can see the full view + total << data + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + if lines.include?("OK.") + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if current = lines.last[/.+(\d{2})%/, 1] + current = current.to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + # Find the name of the VM name + name = output[/Suggested VM name "(.+?)"/, 1] + if !name + @logger.error("Couldn't find VM name in the output.") + return nil + end + + output = execute("list", "vms") + if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] + return existing_vm + end + + nil + end + + def read_forwarded_ports(uuid=nil, active_only=false) + uuid ||= @uuid + + @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") + + results = [] + current_nic = nil + info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + # This is how we find the nic that a FP is attached to, + # since this comes first. + if nic = line[/^nic(\d+)=".+?"$/, 1] + current_nic = nic.to_i + end + + # If we care about active VMs only, then we check the state + # to verify the VM is running. + if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") + return [] + end + + # Parse out the forwarded port information + if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) + result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] + @logger.debug(" - #{result.inspect}") + results << result + end + end + + results + end + + def read_bridged_interfaces + execute("list", "bridgedifs").split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Return the info to build up the results + info + end + end + + def read_guest_additions_version + output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", + :retryable => true) + if value = output[/^Value: (.+?)$/, 1] + # Split the version by _ since some distro versions modify it + # to look like this: 4.1.2_ubuntu, and the distro part isn't + # too important. + return value.split("_").first + end + + return nil + end + + def read_host_only_interfaces + dhcp = {} + execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| + info = {} + + block.split("\n").each do |line| + if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] + info[:network] = network + elsif ip = line[/^IP:\s+(.+?)$/, 1] + info[:ip] = ip + elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] + info[:lower] = lower + elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] + info[:upper] = upper + end + end + + # Set the DHCP info + dhcp[info[:network]] = info + end + + execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Set the DHCP info if it exists + info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] + + info + end + end + + def read_mac_address + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if mac = line[/^macaddress1="(.+?)"$/, 1] + return mac + end + end + + nil + end + + def read_mac_addresses + macs = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + mac = matcher[2].to_s + macs[adapter] = mac + end + end + macs + end + + def read_machine_folder + execute("list", "systemproperties", :retryable => true).split("\n").each do |line| + if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] + return folder + end + end + + nil + end + + def read_network_interfaces + nics = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^nic(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + type = matcher[2].to_sym + + nics[adapter] ||= {} + nics[adapter][:type] = type + elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:hostonly] = network + elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:bridge] = network + end + end + + nics + end + + def read_state + output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + if output =~ /^name=""$/ + return :inaccessible + elsif state = output[/^VMState="(.+?)"$/, 1] + return state.to_sym + end + + nil + end + + def read_used_ports + ports = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if uuid = line[/^".+?" \{(.+?)\}$/, 1] + # Ignore our own used ports + next if uuid == @uuid + + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + end + end + + ports + end + + def read_vms + results = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if vm = line[/^".+?" \{(.+?)\}$/, 1] + results << vm + end + end + + results + end + + def set_mac_address(mac) + execute("modifyvm", @uuid, "--macaddress1", mac) + end + + def set_name(name) + execute("modifyvm", @uuid, "--name", name) + end + + def share_folders(folders) + folders.each do |folder| + args = ["--name", + folder[:name], + "--hostpath", + folder[:hostpath]] + args << "--transient" if folder.has_key?(:transient) && folder[:transient] + execute("sharedfolder", "add", @uuid, *args) + end + end + + def ssh_port(expected_port) + @logger.debug("Searching for SSH port: #{expected_port.inspect}") + + # Look for the forwarded port only by comparing the guest port + read_forwarded_ports.each do |_, _, hostport, guestport| + return hostport if guestport == expected_port + end + + nil + end + + def start(mode) + command = ["startvm", @uuid, "--type", mode.to_s] + r = raw(*command) + + if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ + # Some systems return an exit code 1 for some reason. For that + # we depend on the output. + return true + end + + # If we reached this point then it didn't work out. + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + + def suspend + execute("controlvm", @uuid, "savestate") + end + + def verify! + # This command sometimes fails if kernel drivers aren't properly loaded + # so we just run the command and verify that it succeeded. + execute("list", "hostonlyifs") + end + + def verify_image(path) + r = raw("import", path.to_s, "--dry-run") + return r.exit_code == 0 + end + + def vm_exists?(uuid) + raw("showvminfo", uuid).exit_code == 0 + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/version_4_1.rb b/plugins/providers/virtualbox/driver/version_4_1.rb new file mode 100644 index 000000000..e4a1268a8 --- /dev/null +++ b/plugins/providers/virtualbox/driver/version_4_1.rb @@ -0,0 +1,476 @@ +require 'log4r' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Driver for VirtualBox 4.1.x + class Version_4_1 < Base + def initialize(uuid) + super() + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_1") + @uuid = uuid + end + + def clear_forwarded_ports + args = [] + read_forwarded_ports(@uuid).each do |nic, name, _, _| + args.concat(["--natpf#{nic}", "delete", name]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def clear_shared_folders + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if folder = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] + execute("sharedfolder", "remove", @uuid, "--name", folder) + end + end + end + + def create_dhcp_server(network, options) + execute("dhcpserver", "add", "--ifname", network, + "--ip", options[:dhcp_ip], + "--netmask", options[:netmask], + "--lowerip", options[:dhcp_lower], + "--upperip", options[:dhcp_upper], + "--enable") + end + + def create_host_only_network(options) + # Create the interface + interface = execute("hostonlyif", "create") + name = interface[/^Interface '(.+?)' was successfully created$/, 1] + + # Configure it + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + + # Return the details + return { + :name => name, + :ip => options[:adapter_ip], + :netmask => options[:netmask], + :dhcp => nil + } + end + + def delete + execute("unregistervm", @uuid, "--delete") + end + + def delete_unused_host_only_networks + networks = [] + execute("list", "hostonlyifs").split("\n").each do |line| + if network = line[/^Name:\s+(.+?)$/, 1] + networks << network + end + end + + execute("list", "vms").split("\n").each do |line| + if vm = line[/^".+?"\s+\{(.+?)\}$/, 1] + info = execute("showvminfo", vm, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] + networks.delete(adapter) + end + end + end + end + + networks.each do |name| + # First try to remove any DHCP servers attached. We use `raw` because + # it is okay if this fails. It usually means that a DHCP server was + # never attached. + raw("dhcpserver", "remove", "--ifname", name) + + # Delete the actual host only network interface. + execute("hostonlyif", "remove", name) + end + end + + def discard_saved_state + execute("discardstate", @uuid) + end + + def enable_adapters(adapters) + args = [] + adapters.each do |adapter| + args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) + + if adapter[:bridge] + args.concat(["--bridgeadapter#{adapter[:adapter]}", + adapter[:bridge]]) + end + + if adapter[:hostonly] + args.concat(["--hostonlyadapter#{adapter[:adapter]}", + adapter[:hostonly]]) + end + + if adapter[:mac_address] + args.concat(["--macaddress#{adapter[:adapter]}", + adapter[:mac_address]]) + end + + if adapter[:nic_type] + args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) + end + end + + execute("modifyvm", @uuid, *args) + end + + def execute_command(command) + raw(*command) + end + + def export(path) + # TODO: Progress + execute("export", @uuid, "--output", path.to_s) + end + + def forward_ports(ports) + args = [] + ports.each do |options| + pf_builder = [options[:name], + options[:protocol] || "tcp", + "", + options[:hostport], + "", + options[:guestport]] + + args.concat(["--natpf#{options[:adapter] || 1}", + pf_builder.join(",")]) + end + + execute("modifyvm", @uuid, *args) if !args.empty? + end + + def halt + execute("controlvm", @uuid, "poweroff") + end + + def import(ovf) + output = "" + total = "" + last = 0 + execute("import", ovf) do |type, data| + if type == :stdout + # Keep track of the stdout so that we can get the VM name + output << data + elsif type == :stderr + # Append the data so we can see the full view + total << data + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + if lines.include?("OK.") + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if current = lines.last[/.+(\d{2})%/, 1] + current = current.to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + # Find the name of the VM name + name = output[/Suggested VM name "(.+?)"/, 1] + if !name + @logger.error("Couldn't find VM name in the output.") + return nil + end + + output = execute("list", "vms") + if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] + return existing_vm + end + + nil + end + + def read_forwarded_ports(uuid=nil, active_only=false) + uuid ||= @uuid + + @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") + + results = [] + current_nic = nil + info = execute("showvminfo", uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + # This is how we find the nic that a FP is attached to, + # since this comes first. + if nic = line[/^nic(\d+)=".+?"$/, 1] + current_nic = nic.to_i + end + + # If we care about active VMs only, then we check the state + # to verify the VM is running. + if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") + return [] + end + + # Parse out the forwarded port information + if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) + result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] + @logger.debug(" - #{result.inspect}") + results << result + end + end + + results + end + + def read_bridged_interfaces + execute("list", "bridgedifs").split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Return the info to build up the results + info + end + end + + def read_guest_additions_version + output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", + :retryable => true) + if value = output[/^Value: (.+?)$/, 1] + # Split the version by _ since some distro versions modify it + # to look like this: 4.1.2_ubuntu, and the distro part isn't + # too important. + return value.split("_").first + end + + return nil + end + + def read_host_only_interfaces + dhcp = {} + execute("list", "dhcpservers", :retryable => true).split("\n\n").each do |block| + info = {} + + block.split("\n").each do |line| + if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] + info[:network] = network + elsif ip = line[/^IP:\s+(.+?)$/, 1] + info[:ip] = ip + elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] + info[:lower] = lower + elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] + info[:upper] = upper + end + end + + # Set the DHCP info + dhcp[info[:network]] = info + end + + execute("list", "hostonlyifs", :retryable => true).split("\n\n").collect do |block| + info = {} + + block.split("\n").each do |line| + if name = line[/^Name:\s+(.+?)$/, 1] + info[:name] = name + elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] + info[:ip] = ip + elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] + info[:netmask] = netmask + elsif status = line[/^Status:\s+(.+?)$/, 1] + info[:status] = status + end + end + + # Set the DHCP info if it exists + info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] + + info + end + end + + def read_mac_address + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if mac = line[/^macaddress1="(.+?)"$/, 1] + return mac + end + end + + nil + end + + def read_mac_addresses + macs = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + mac = matcher[2].to_s + macs[adapter] = mac + end + end + macs + end + + def read_machine_folder + execute("list", "systemproperties", :retryable => true).split("\n").each do |line| + if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] + return folder + end + end + + nil + end + + def read_network_interfaces + nics = {} + info = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + info.split("\n").each do |line| + if matcher = /^nic(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + type = matcher[2].to_sym + + nics[adapter] ||= {} + nics[adapter][:type] = type + elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:hostonly] = network + elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) + adapter = matcher[1].to_i + network = matcher[2].to_s + + nics[adapter] ||= {} + nics[adapter][:bridge] = network + end + end + + nics + end + + def read_state + output = execute("showvminfo", @uuid, "--machinereadable", :retryable => true) + if output =~ /^name=""$/ + return :inaccessible + elsif state = output[/^VMState="(.+?)"$/, 1] + return state.to_sym + end + + nil + end + + def read_used_ports + ports = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if uuid = line[/^".+?" \{(.+?)\}$/, 1] + # Ignore our own used ports + next if uuid == @uuid + + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + end + end + + ports + end + + def read_vms + results = [] + execute("list", "vms", :retryable => true).split("\n").each do |line| + if vm = line[/^".+?" \{(.+?)\}$/, 1] + results << vm + end + end + + results + end + + def set_mac_address(mac) + execute("modifyvm", @uuid, "--macaddress1", mac) + end + + def set_name(name) + execute("modifyvm", @uuid, "--name", name) + end + + def share_folders(folders) + folders.each do |folder| + args = ["--name", + folder[:name], + "--hostpath", + folder[:hostpath]] + args << "--transient" if folder.has_key?(:transient) && folder[:transient] + execute("sharedfolder", "add", @uuid, *args) + end + end + + def ssh_port(expected_port) + @logger.debug("Searching for SSH port: #{expected_port.inspect}") + + # Look for the forwarded port only by comparing the guest port + read_forwarded_ports.each do |_, _, hostport, guestport| + return hostport if guestport == expected_port + end + + nil + end + + def start(mode) + command = ["startvm", @uuid, "--type", mode.to_s] + r = raw(*command) + + if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ + # Some systems return an exit code 1 for some reason. For that + # we depend on the output. + return true + end + + # If we reached this point then it didn't work out. + raise Vagrant::Errors::VBoxManageError, :command => command.inspect + end + + def suspend + execute("controlvm", @uuid, "savestate") + end + + def verify! + # This command sometimes fails if kernel drivers aren't properly loaded + # so we just run the command and verify that it succeeded. + execute("list", "hostonlyifs") + end + + def verify_image(path) + r = raw("import", path.to_s, "--dry-run") + return r.exit_code == 0 + end + + def vm_exists?(uuid) + raw("showvminfo", uuid).exit_code == 0 + end + end + end + end +end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb new file mode 100644 index 000000000..d136209c0 --- /dev/null +++ b/plugins/providers/virtualbox/plugin.rb @@ -0,0 +1,28 @@ +require "vagrant" + +module VagrantPlugins + module ProviderVirtualBox + class Plugin < Vagrant.plugin("1") + name "VirtualBox provider" + description <<-EOF + The VirtualBox provider allows Vagrant to manage and control + VirtualBox-based virtual machines. + EOF + + provider("virtualbox") do + require File.expand_path("../provider", __FILE__) + Provider + end + end + + autoload :Action, File.expand_path("../action", __FILE__) + + # Drop some autoloads in here to optimize the performance of loading + # our drivers only when they are needed. + module Driver + autoload :Meta, File.expand_path("../driver/meta", __FILE__) + autoload :Version_4_0, File.expand_path("../driver/version_4_0", __FILE__) + autoload :Version_4_1, File.expand_path("../driver/version_4_1", __FILE__) + end + end +end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb new file mode 100644 index 000000000..44407c031 --- /dev/null +++ b/plugins/providers/virtualbox/provider.rb @@ -0,0 +1,83 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + class Provider < Vagrant.plugin("1", :provider) + attr_reader :driver + + def initialize(machine) + @logger = Log4r::Logger.new("vagrant::provider::virtualbox") + @machine = machine + + # This method will load in our driver, so we call it now to + # initialize it. + machine_id_changed + end + + # @see Vagrant::Plugin::V1::Provider#action + def action(name) + # Attempt to get the action method from the Action class if it + # exists, otherwise return nil to show that we don't support the + # given action. + action_method = "action_#{name}" + return Action.send(action_method) if Action.respond_to?(action_method) + nil + end + + # If the machine ID changed, then we need to rebuild our underlying + # driver. + def machine_id_changed + id = @machine.id + + begin + @logger.debug("Instantiating the driver for machine ID: #{@machine.id.inspect}") + @driver = Driver::Meta.new(id) + rescue Driver::Meta::VMNotFound + # The virtual machine doesn't exist, so we probably have a stale + # ID. Just clear the id out of the machine and reload it. + @logger.debug("VM not found! Clearing saved machine ID and reloading.") + id = nil + retry + end + end + + # Returns the SSH info for accessing the VirtualBox VM. + def ssh_info + # If the VM is not created then we cannot possibly SSH into it, so + # we return nil. + return nil if state == :not_created + + # Return what we know. The host is always "127.0.0.1" because + # VirtualBox VMs are always local. The port we try to discover + # by reading the forwarded ports. + return { + :host => "127.0.0.1", + :port => @driver.ssh_port(@machine.config.ssh.guest_port) + } + end + + # Return the state of VirtualBox virtual machine by actually + # querying VBoxManage. + # + # @return [Symbol] + def state + # XXX: What happens if we destroy the VM but the UUID is still + # set here? + return :not_created if !@driver.uuid + state = @driver.read_state + return :unknown if !state + state + end + + # Returns a human-friendly string version of this provider which + # includes the machine's ID that this provider represents, if it + # has one. + # + # @return [String] + def to_s + id = @machine.id ? @machine.id : "new VM" + "VirtualBox (#{id})" + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index d5325b4cd..30f3d4513 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -85,6 +85,10 @@ en: You specified: %{home_path} interrupted: |- Vagrant exited after cleanup due to external interrupt. + machine_guest_not_ready: |- + Guest-specific operations were attempted on a machine that is not + ready for guest communication. This should not happen and a bug + should be reported. multi_vm_required: |- A multi-vm environment is required for name specification to this command. multi_vm_target_required: |- @@ -156,6 +160,14 @@ en: The private key you're attempting to use with this Vagrant box uses an unsupported encryption type. The SSH library Vagrant uses does not support this key type. Please use `ssh-rsa` or `ssh-dss` instead. + ssh_not_ready: |- + The provider for this Vagrant-managed machine is reporting that it + is not yet ready for SSH. Depending on your provider this can carry + different meanings. Make sure your machine is created and running and + try again. Additionally, check the output of `vagrant status` to verify + that the machine is in the state that you expect. If you continue to + get this error message, please view the documentation for the provider + you're using. ssh_port_not_detected: |- Vagrant couldn't determine the SSH port for your VM! Vagrant attempts to automatically find a forwarded port that matches your `config.ssh.guest_port` @@ -182,6 +194,11 @@ en: a TTY. Most actions in Vagrant that require a TTY have configuration switches to disable this requirement. Please do that or run Vagrant with TTY. + unimplemented_provider_action: |- + Vagrant attempted to call the action '%{action}' on the provider + '%{provider}', but this provider doesn't support this action. This + is probably a bug in either the provider or the plugin calling this + action, and should be reported. vagrantfile_exists: |- `Vagrantfile` already exists in this directory. Remove it before running `vagrant init`. diff --git a/test/unit/support/shared/base_context.rb b/test/unit/support/shared/base_context.rb index 947fd01ed..734a9c5d8 100644 --- a/test/unit/support/shared/base_context.rb +++ b/test/unit/support/shared/base_context.rb @@ -5,11 +5,23 @@ require "unit/support/isolated_environment" shared_context "unit" do before(:each) do + # State to store the list of registered plugins that we have to + # unregister later. + @_plugins = [] + # Create a thing to store our temporary files so that they aren't # unlinked right away. @_temp_files = [] end + after(:each) do + # Unregister each of the plugins we have may have temporarily + # registered for the duration of this test. + @_plugins.each do |plugin| + plugin.unregister! + end + end + # This creates an isolated environment so that Vagrant doesn't # muck around with your real system during unit tests. # @@ -22,6 +34,20 @@ shared_context "unit" do env end + # This registers a Vagrant plugin for the duration of a single test. + # This will yield a new plugin class that you can then call the + # public plugin methods on. + # + # @yield [plugin] Yields the plugin class for you to call the public + # API that you need to. + def register_plugin + plugin = Class.new(Vagrant.plugin("1")) + plugin.name("Test Plugin #{plugin.inspect}") + yield plugin if block_given? + @_plugins << plugin + plugin + end + # This helper creates a temporary file and returns a Pathname # object pointed to it. # diff --git a/test/unit/vagrant/action/builder_test.rb b/test/unit/vagrant/action/builder_test.rb index 046166cfa..ee00e0818 100644 --- a/test/unit/vagrant/action/builder_test.rb +++ b/test/unit/vagrant/action/builder_test.rb @@ -10,6 +10,18 @@ describe Vagrant::Action::Builder do Proc.new { |env| env[:data] << data } end + context "build" do + it "should provide build as a shortcut for basic sequences" do + data = {} + proc = Proc.new { |env| env[:data] = true } + + instance = described_class.build(proc) + instance.call(data) + + data[:data].should == true + end + end + context "basic `use`" do it "should add items to the stack and make them callable" do data = {} @@ -50,18 +62,6 @@ describe Vagrant::Action::Builder do two.call(data) data[:one].should == true end - - it "should be able to set additional variables when using" do - data = { } - proc1 = Proc.new { |env| env[:data] += 1 } - - # Build the first builder - one = described_class.new - one.use proc1, :data => 5 - one.call(data) - - data[:data].should == 6 - end end context "inserting" do diff --git a/test/unit/vagrant/action/builtin/call_test.rb b/test/unit/vagrant/action/builtin/call_test.rb new file mode 100644 index 000000000..c2ef76bdc --- /dev/null +++ b/test/unit/vagrant/action/builtin/call_test.rb @@ -0,0 +1,55 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::Call do + let(:app) { lambda { |env| } } + let(:env) { {} } + + it "should yield the env to the block" do + received = nil + + callable = lambda do |env| + env[:result] = "value" + end + + described_class.new(app, env, callable) do |env, builder| + received = env[:result] + end.call({}) + + received.should == "value" + end + + it "should call the callable with the original environment" do + received = nil + callable = lambda { |env| received = env[:foo] } + + described_class.new(app, env, callable) do |_env, _builder| + # Nothing. + end.call({ :foo => :bar }) + + received.should == :bar + end + + it "should call the next builder" do + received = nil + callable = lambda { |env| } + next_step = lambda { |env| received = "value" } + + described_class.new(app, env, callable) do |_env, builder| + builder.use next_step + end.call({}) + + received.should == "value" + end + + it "should call the next builder with the original environment" do + received = nil + callable = lambda { |env| } + next_step = lambda { |env| received = env[:foo] } + + described_class.new(app, env, callable) do |_env, builder| + builder.use next_step + end.call({ :foo => :bar }) + + received.should == :bar + end +end diff --git a/test/unit/vagrant/action/builtin/confirm_test.rb b/test/unit/vagrant/action/builtin/confirm_test.rb new file mode 100644 index 000000000..7c290e172 --- /dev/null +++ b/test/unit/vagrant/action/builtin/confirm_test.rb @@ -0,0 +1,21 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::Confirm do + let(:app) { lambda { |env| } } + let(:env) { { :ui => double("ui") } } + let(:message) { "foo" } + + ["y", "Y"].each do |valid| + it "should set the result to true if '#{valid}' is given" do + env[:ui].should_receive(:ask).with(message).and_return(valid) + described_class.new(app, env, message).call(env) + env[:result].should be + end + end + + it "should set result to false if anything else is given" do + env[:ui].should_receive(:ask).with(message).and_return("nope") + described_class.new(app, env, message).call(env) + env[:result].should_not be + end +end diff --git a/test/unit/vagrant/action/builtin/env_set_test.rb b/test/unit/vagrant/action/builtin/env_set_test.rb new file mode 100644 index 000000000..0a551ec2b --- /dev/null +++ b/test/unit/vagrant/action/builtin/env_set_test.rb @@ -0,0 +1,20 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::EnvSet do + let(:app) { lambda { |env| } } + let(:env) { {} } + + it "should set the new environment" do + described_class.new(app, env, :foo => :bar).call(env) + + env[:foo].should == :bar + end + + it "should call the next middleware" do + callable = lambda { |env| env[:called] = env[:foo] } + + env[:called].should be_nil + described_class.new(callable, env, :foo => :yep).call(env) + env[:called].should == :yep + end +end diff --git a/test/unit/vagrant/action/builtin/ssh_exec_test.rb b/test/unit/vagrant/action/builtin/ssh_exec_test.rb new file mode 100644 index 000000000..a6ea69f47 --- /dev/null +++ b/test/unit/vagrant/action/builtin/ssh_exec_test.rb @@ -0,0 +1,55 @@ +require File.expand_path("../../../../base", __FILE__) + +require "vagrant/util/ssh" + +describe Vagrant::Action::Builtin::SSHExec do + let(:app) { lambda { |env| } } + let(:env) { { :machine => machine } } + let(:machine) do + result = double("machine") + result.stub(:ssh_info).and_return(machine_ssh_info) + result + end + let(:machine_ssh_info) { {} } + let(:ssh_klass) { Vagrant::Util::SSH } + + before(:each) do + # Stub the methods so that even if we test incorrectly, no side + # effects actually happen. + ssh_klass.stub(:check_key_permissions) + ssh_klass.stub(:exec) + end + + it "should raise an exception if SSH is not ready" do + not_ready_machine = double("machine") + not_ready_machine.stub(:ssh_info).and_return(nil) + + env[:machine] = not_ready_machine + expect { described_class.new(app, env).call(env) }. + to raise_error(Vagrant::Errors::SSHNotReady) + end + + it "should check key permissions then exec" do + ssh_klass.should_receive(:check_key_permissions). + with(machine_ssh_info[:private_key_path]). + once. + ordered + + ssh_klass.should_receive(:exec). + with(machine_ssh_info, nil). + once. + ordered + + described_class.new(app, env).call(env) + end + + it "should exec with the options given in `ssh_opts`" do + ssh_opts = { :foo => :bar } + + ssh_klass.should_receive(:exec). + with(machine_ssh_info, ssh_opts) + + env[:ssh_opts] = ssh_opts + described_class.new(app, env).call(env) + end +end diff --git a/test/unit/vagrant/action/runner_test.rb b/test/unit/vagrant/action/runner_test.rb index fedd89fc4..2f5ab95dd 100644 --- a/test/unit/vagrant/action/runner_test.rb +++ b/test/unit/vagrant/action/runner_test.rb @@ -1,13 +1,7 @@ require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Runner do - let(:registry) do - d = double("registry") - d.stub(:get) - d - end - - let(:instance) { described_class.new(registry) } + let(:instance) { described_class.new } it "should raise an error if an invalid callable is given" do expect { instance.run(7) }.to raise_error(ArgumentError, /must be a callable/) @@ -31,6 +25,18 @@ describe Vagrant::Action::Runner do expect { instance.run(callable) }.to raise_error(Exception, "BOOM") end + it "should return the resulting environment" do + callable = lambda do |env| + env[:data] = "value" + + # Return nil so we can make sure it isn't using this return value + nil + end + + result = instance.run(callable) + result[:data].should == "value" + end + it "should pass options into hash given to callable" do result = nil callable = lambda do |env| @@ -47,7 +53,7 @@ describe Vagrant::Action::Runner do result = env["data"] end - instance = described_class.new(registry, "data" => "bar") + instance = described_class.new("data" => "bar") instance.run(callable) result.should == "bar" end @@ -58,7 +64,7 @@ describe Vagrant::Action::Runner do result = env["data"] end - instance = described_class.new(registry) { { "data" => "bar" } } + instance = described_class.new { { "data" => "bar" } } instance.run(callable) result.should == "bar" end diff --git a/test/unit/vagrant/downloaders/base_test.rb b/test/unit/vagrant/downloaders/base_test.rb index 963578d96..43e27d8b7 100644 --- a/test/unit/vagrant/downloaders/base_test.rb +++ b/test/unit/vagrant/downloaders/base_test.rb @@ -8,10 +8,6 @@ describe Vagrant::Downloaders::Base do described_class.match?("foo").should_not be end - it "should implement `prepare`" do - instance.prepare("foo").should be_nil - end - it "should implement `download!`" do instance.download!("foo", "bar").should be_nil end diff --git a/test/unit/vagrant/downloaders/file_test.rb b/test/unit/vagrant/downloaders/file_test.rb index 170fe8989..ad4fb3f03 100644 --- a/test/unit/vagrant/downloaders/file_test.rb +++ b/test/unit/vagrant/downloaders/file_test.rb @@ -3,9 +3,15 @@ require File.expand_path("../../../base", __FILE__) require "tempfile" describe Vagrant::Downloaders::File do + include_context "unit" + let(:ui) { double("ui") } let(:instance) { described_class.new(ui) } + before(:each) do + ui.stub(:info) + end + describe "matching" do it "should match an existing file" do described_class.match?(__FILE__).should be @@ -19,33 +25,37 @@ describe Vagrant::Downloaders::File do old_home = ENV["HOME"] begin # Create a temporary file - temp = Tempfile.new("vagrant") + temp = temporary_file # Set our home directory to be this directory so we can use # "~" paths - ENV["HOME"] = File.dirname(temp.path) + ENV["HOME"] = File.dirname(temp.to_s) # Test that we can find the temp file - described_class.match?("~/#{File.basename(temp.path)}").should be + described_class.match?("~/#{File.basename(temp.to_s)}").should be ensure ENV["HOME"] = old_home end end end - describe "preparing" do + describe "downloading" do + let(:destination_file) { temporary_file.open("w") } + it "should raise an exception if the file does not exist" do path = File.join(__FILE__, "nopenopenope") File.exist?(path).should_not be - expect { instance.prepare(path) }.to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) + expect { instance.download!(path, destination_file) }. + to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) end it "should raise an exception if the file is a directory" do path = File.dirname(__FILE__) File.should be_directory(path) - expect { instance.prepare(path) }.to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) + expect { instance.download!(path, destination_file) }. + to raise_error(Vagrant::Errors::DownloaderFileDoesntExist) end it "should find files that use shell expansions" do @@ -59,15 +69,13 @@ describe Vagrant::Downloaders::File do ENV["HOME"] = File.dirname(temp.path) # Test that we can find the temp file - expect { instance.prepare("~/#{File.basename(temp.path)}") }. + expect { instance.download!("~/#{File.basename(temp.path)}", destination_file) }. to_not raise_error ensure ENV["HOME"] = old_home end end - end - describe "downloading" do it "should copy the source to the destination" do pending "setup paths" end diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index bba1fa57b..9a7c227af 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -8,8 +8,18 @@ require "support/tempdir" describe Vagrant::Environment do include_context "unit" - let(:home_path) { Pathname.new(Tempdir.new.path) } - let(:instance) { described_class.new(:home_path => home_path) } + let(:env) do + isolated_environment.tap do |e| + e.box2("base", :virtualbox) + e.vagrantfile <<-VF + Vagrant.configure("1") do |config| + config.vm.box = "base" + end + VF + end + end + + let(:instance) { env.create_vagrant_env } describe "current working directory" do it "is the cwd by default" do @@ -93,16 +103,6 @@ describe Vagrant::Environment do end end - describe "action registry" do - it "has an action registry" do - instance.action_registry.should be_kind_of(Vagrant::Registry) - end - - it "should have the built-in actions in the registry" do - instance.action_registry.get(:provision).should_not be_nil - end - end - describe "primary VM" do it "should be the only VM if not a multi-VM environment" do instance.primary_vm.should == instance.vms.values.first @@ -112,6 +112,7 @@ describe Vagrant::Environment do environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant::Config.run do |config| + config.vm.box = "base" config.vm.define :foo config.vm.define :bar, :primary => true end diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb new file mode 100644 index 000000000..ecf056c75 --- /dev/null +++ b/test/unit/vagrant/machine_test.rb @@ -0,0 +1,388 @@ +require File.expand_path("../../base", __FILE__) + +describe Vagrant::Machine do + include_context "unit" + + let(:name) { "foo" } + let(:provider) { double("provider") } + let(:provider_cls) do + obj = double("provider_cls") + obj.stub(:new => provider) + obj + end + let(:box) { Object.new } + let(:config) { env.config.global } + let(:env) do + # We need to create a Vagrantfile so that this test environment + # has a proper root path + test_env.vagrantfile("") + + # Create the Vagrant::Environment instance + test_env.create_vagrant_env + end + + let(:test_env) { isolated_environment } + + let(:instance) { new_instance } + + # Returns a new instance with the test data + def new_instance + described_class.new(name, provider_cls, config, box, env) + end + + describe "initialization" do + describe "provider initialization" do + # This is a helper that generates a test for provider intialization. + # This is a separate helper method because it takes a block that can + # be used to have additional tests on the received machine. + # + # @yield [machine] Yields the machine that the provider initialization + # method received so you can run additional tests on it. + def provider_init_test + received_machine = nil + + provider_cls = double("provider_cls") + provider_cls.should_receive(:new) do |machine| + # Store this for later so we can verify that it is the + # one we expected to receive. + received_machine = machine + + # Sanity check + machine.should be + + # Yield our machine if we want to do additional tests + yield machine if block_given? + end + + # Initialize a new machine and verify that we properly receive + # the machine we expect. + instance = described_class.new(name, provider_cls, config, box, env) + received_machine.should eql(instance) + end + + it "should initialize with the machine object" do + # Just run the blank test + provider_init_test + end + + it "should have the machine name setup" do + provider_init_test do |machine| + machine.name.should == name + end + end + + it "should have the machine configuration" do + provider_init_test do |machine| + machine.config.should eql(config) + end + end + + it "should have the box" do + provider_init_test do |machine| + machine.box.should eql(box) + end + end + + it "should have the environment" do + provider_init_test do |machine| + machine.env.should eql(env) + end + end + + it "should have access to the ID" do + # Stub this because #id= calls it. + provider.stub(:machine_id_changed) + + # Set the ID on the previous instance so that it is persisted + instance.id = "foo" + + provider_init_test do |machine| + machine.id.should == "foo" + end + end + + it "should NOT have access to the provider" do + provider_init_test do |machine| + machine.provider.should be_nil + end + end + end + end + + describe "attributes" do + it "should provide access to the name" do + instance.name.should == name + end + + it "should provide access to the configuration" do + instance.config.should eql(config) + end + + it "should provide access to the box" do + instance.box.should eql(box) + end + + it "should provide access to the environment" do + instance.env.should eql(env) + end + + it "should provide access to the provider" do + instance.provider.should eql(provider) + end + end + + describe "actions" do + it "should be able to run an action that exists" do + action_name = :up + called = false + callable = lambda { |_env| called = true } + + provider.should_receive(:action).with(action_name).and_return(callable) + instance.action(:up) + called.should be + end + + it "should provide the machine in the environment" do + action_name = :up + machine = nil + callable = lambda { |env| machine = env[:machine] } + + provider.stub(:action).with(action_name).and_return(callable) + instance.action(:up) + + machine.should eql(instance) + end + + it "should pass any extra options to the environment" do + action_name = :up + foo = nil + callable = lambda { |env| foo = env[:foo] } + + provider.stub(:action).with(action_name).and_return(callable) + instance.action(:up, :foo => :bar) + + foo.should == :bar + end + + it "should return the environment as a result" do + action_name = :up + callable = lambda { |env| env[:result] = "FOO" } + + provider.stub(:action).with(action_name).and_return(callable) + result = instance.action(action_name) + + result[:result].should == "FOO" + end + + it "should raise an exception if the action is not implemented" do + action_name = :up + + provider.stub(:action).with(action_name).and_return(nil) + + expect { instance.action(action_name) }. + to raise_error(Vagrant::Errors::UnimplementedProviderAction) + end + end + + describe "communicator" do + it "should always return the SSH communicator" do + instance.communicate.should be_kind_of(VagrantPlugins::CommunicatorSSH::Communicator) + end + + it "should memoize the result" do + obj = instance.communicate + instance.communicate.should eql(obj) + end + end + + describe "guest implementation" do + let(:communicator) do + result = double("communicator") + result.stub(:ready?).and_return(true) + result + end + + before(:each) do + instance.stub(:communicate).and_return(communicator) + end + + it "should raise an exception if communication is not ready" do + communicator.should_receive(:ready?).and_return(false) + + expect { instance.guest }. + to raise_error(Vagrant::Errors::MachineGuestNotReady) + end + + it "should return the configured guest" do + test_guest = Class.new(Vagrant.plugin("1", :guest)) + + register_plugin do |p| + p.guest(:test) { test_guest } + end + + config.vm.guest = :test + + result = instance.guest + result.should be_kind_of(test_guest) + end + + it "should raise an exception if it can't find the configured guest" do + config.vm.guest = :bad + + expect { instance.guest }. + to raise_error(Vagrant::Errors::VMGuestError) + end + + it "should distro dispatch to the most specific guest" do + # Create the classes and dispatch the parent into the child + guest_parent = Class.new(Vagrant.plugin("1", :guest)) do + def distro_dispatch + :child + end + end + + guest_child = Class.new(Vagrant.plugin("1", :guest)) + + # Register the classes + register_plugin do |p| + p.guest(:parent) { guest_parent } + p.guest(:child) { guest_child } + end + + # Test that the result is the child + config.vm.guest = :parent + instance.guest.should be_kind_of(guest_child) + end + + it "should protect against loops in the distro dispatch" do + # Create the classes and dispatch the parent into the child + guest_parent = Class.new(Vagrant.plugin("1", :guest)) do + def distro_dispatch + :parent + end + end + + # Register the classes + register_plugin do |p| + p.guest(:parent) { guest_parent } + end + + # Test that the result is the child + config.vm.guest = :parent + instance.guest.should be_kind_of(guest_parent) + end + end + + describe "setting the ID" do + before(:each) do + provider.stub(:machine_id_changed) + end + + it "should not have an ID by default" do + instance.id.should be_nil + end + + it "should set an ID" do + instance.id = "bar" + instance.id.should == "bar" + end + + it "should notify the machine that the ID changed" do + provider.should_receive(:machine_id_changed).once + + instance.id = "bar" + end + + it "should persist the ID" do + instance.id = "foo" + new_instance.id.should == "foo" + end + + it "should delete the ID" do + instance.id = "foo" + + second = new_instance + second.id.should == "foo" + second.id = nil + + third = new_instance + third.id.should be_nil + end + end + + describe "ssh info" do + describe "with the provider returning nil" do + it "should return nil if the provider returns nil" do + provider.should_receive(:ssh_info).and_return(nil) + instance.ssh_info.should be_nil + end + end + + describe "with the provider returning data" do + let(:provider_ssh_info) { {} } + + before(:each) do + provider.should_receive(:ssh_info).and_return(provider_ssh_info) + end + + [:host, :port, :username].each do |type| + it "should return the provider data if not configured in Vagrantfile" do + provider_ssh_info[type] = "foo" + instance.config.ssh.send("#{type}=", nil) + + instance.ssh_info[type].should == "foo" + end + + it "should return the Vagrantfile value over the provider data if given" do + provider_ssh_info[type] = "foo" + instance.config.ssh.send("#{type}=", "bar") + + instance.ssh_info[type].should == "bar" + end + end + + it "should set the configured forward agent settings" do + provider_ssh_info[:forward_agent] = true + instance.config.ssh.forward_agent = false + + instance.ssh_info[:forward_agent].should == false + end + + it "should set the configured forward X11 settings" do + provider_ssh_info[:forward_x11] = true + instance.config.ssh.forward_x11 = false + + instance.ssh_info[:forward_x11].should == false + end + + it "should return the provider private key if given" do + provider_ssh_info[:private_key_path] = "foo" + + instance.ssh_info[:private_key_path].should == "foo" + end + + it "should return the configured SSH key path if set" do + provider_ssh_info[:private_key_path] = "foo" + instance.config.ssh.private_key_path = "bar" + + instance.ssh_info[:private_key_path].should == "bar" + end + + it "should return the default private key path if provider and config doesn't have one" do + provider_ssh_info[:private_key_path] = nil + instance.config.ssh.private_key_path = nil + + instance.ssh_info[:private_key_path].should == instance.env.default_private_key_path + end + end + end + + describe "state" do + it "should query state from the provider" do + state = :running + + provider.should_receive(:state).and_return(state) + instance.state.should == state + end + end +end diff --git a/test/unit/vagrant/plugin/v1/communicator_test.rb b/test/unit/vagrant/plugin/v1/communicator_test.rb new file mode 100644 index 000000000..a10e5a88d --- /dev/null +++ b/test/unit/vagrant/plugin/v1/communicator_test.rb @@ -0,0 +1,9 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Plugin::V1::Communicator do + let(:machine) { Object.new } + + it "should not match by default" do + described_class.match?(machine).should_not be + end +end diff --git a/test/unit/vagrant/plugin/v1/plugin_test.rb b/test/unit/vagrant/plugin/v1/plugin_test.rb index e79f75b9a..df1d5ccc2 100644 --- a/test/unit/vagrant/plugin/v1/plugin_test.rb +++ b/test/unit/vagrant/plugin/v1/plugin_test.rb @@ -100,6 +100,34 @@ describe Vagrant::Plugin::V1::Plugin do end end + describe "communicators" do + it "should register communicator classes" do + plugin = Class.new(described_class) do + communicator("foo") { "bar" } + end + + plugin.communicator[:foo].should == "bar" + end + + it "should lazily register communicator classes" do + # Below would raise an error if the value of the class was + # evaluated immediately. By asserting that this does not raise an + # error, we verify that the value is actually lazily loaded + plugin = nil + expect { + plugin = Class.new(described_class) do + communicator("foo") { raise StandardError, "FAIL!" } + end + }.to_not raise_error + + # Now verify when we actually get the configuration key that + # a proper error is raised. + expect { + plugin.communicator[:foo] + }.to raise_error(StandardError) + end + end + describe "configuration" do it "should register configuration classes" do plugin = Class.new(described_class) do @@ -195,6 +223,34 @@ describe Vagrant::Plugin::V1::Plugin do end end + describe "providers" do + it "should register provider classes" do + plugin = Class.new(described_class) do + provider("foo") { "bar" } + end + + plugin.provider[:foo].should == "bar" + end + + it "should lazily register provider classes" do + # Below would raise an error if the value of the config class was + # evaluated immediately. By asserting that this does not raise an + # error, we verify that the value is actually lazily loaded + plugin = nil + expect { + plugin = Class.new(described_class) do + provider("foo") { raise StandardError, "FAIL!" } + end + }.to_not raise_error + + # Now verify when we actually get the configuration key that + # a proper error is raised. + expect { + plugin.provider[:foo] + }.to raise_error(StandardError) + end + end + describe "provisioners" do it "should register provisioner classes" do plugin = Class.new(described_class) do diff --git a/test/unit/vagrant/plugin/v1/provider_test.rb b/test/unit/vagrant/plugin/v1/provider_test.rb new file mode 100644 index 000000000..2950d9bbf --- /dev/null +++ b/test/unit/vagrant/plugin/v1/provider_test.rb @@ -0,0 +1,18 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Plugin::V1::Provider do + let(:machine) { Object.new } + let(:instance) { described_class.new(machine) } + + it "should return nil by default for actions" do + instance.action(:whatever).should be_nil + end + + it "should return nil by default for ssh info" do + instance.ssh_info.should be_nil + end + + it "should return nil by default for state" do + instance.state.should be_nil + end +end diff --git a/test/unit/vagrant/ssh_test.rb b/test/unit/vagrant/ssh_test.rb deleted file mode 100644 index 268222d95..000000000 --- a/test/unit/vagrant/ssh_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require File.expand_path("../../base", __FILE__) - -describe Vagrant::SSH do - context "check_key_permissions" do - let(:key_path) do - # We create a tempfile to guarantee some level of uniqueness - # then explicitly close/unlink but save the path so we can re-use - temp = Tempfile.new("vagrant") - result = Pathname.new(temp.path) - temp.close - temp.unlink - - result - end - - let(:ssh_instance) { Vagrant::SSH.new(double) } - - it "should not raise an exception if we set a keyfile permission correctly" do - # Write some stuff to our key file and chmod it to some - # incorrect permissions. - key_path.open("w") { |f| f.write("hello!") } - key_path.chmod(0644) - - # This should work! - expect { ssh_instance.check_key_permissions(key_path) }. - to_not raise_error - end - end -end - diff --git a/test/unit/vagrant/util/ssh_test.rb b/test/unit/vagrant/util/ssh_test.rb new file mode 100644 index 000000000..30bfe0d5e --- /dev/null +++ b/test/unit/vagrant/util/ssh_test.rb @@ -0,0 +1,30 @@ +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/platform" +require "vagrant/util/ssh" + +describe Vagrant::Util::SSH do + include_context "unit" + + describe "checking key permissions" do + let(:key_path) { temporary_file } + + it "should do nothing on Windows" do + Vagrant::Util::Platform.stub(:windows?).and_return(true) + + key_path.chmod(0700) + + # Get the mode now and verify that it is untouched afterwards + mode = key_path.stat.mode + described_class.check_key_permissions(key_path) + key_path.stat.mode.should == mode + end + + it "should fix the permissions" do + key_path.chmod(0644) + + described_class.check_key_permissions(key_path) + key_path.stat.mode.should == 0100600 + end + end +end diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index 4985761fa..5c61a3af2 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -13,9 +13,11 @@ describe Vagrant do it "returns the proper components for version 1" do described_class.plugin("1", :command).should == Vagrant::Plugin::V1::Command + described_class.plugin("1", :communicator).should == Vagrant::Plugin::V1::Communicator described_class.plugin("1", :config).should == Vagrant::Plugin::V1::Config described_class.plugin("1", :guest).should == Vagrant::Plugin::V1::Guest described_class.plugin("1", :host).should == Vagrant::Plugin::V1::Host + described_class.plugin("1", :provider).should == Vagrant::Plugin::V1::Provider described_class.plugin("1", :provisioner).should == Vagrant::Plugin::V1::Provisioner end end